自动化测试将用于进一步的任务中,并且在实际项目中也广泛使用。
我们为什么要进行测试?
当我们编写一个函数时,我们通常可以想象它应该做什么:哪些参数给出哪些结果。
在开发过程中,我们可以通过运行函数并将其结果与预期结果进行比较来检查函数。例如,我们可以在控制台中执行此操作。
如果出现问题,那么我们将修复代码,再次运行,检查结果,依此类推,直到它正常工作。
但这种手动“重新运行”是不完美的。
通过手动重新运行来测试代码时,很容易遗漏一些东西。
例如,我们正在创建一个函数 f
。编写了一些代码,进行测试:f(1)
正常工作,但 f(2)
不正常工作。我们修复代码,现在 f(2)
正常工作了。看起来完整了吗?但我们忘记重新测试 f(1)
了。这可能会导致错误。
这是很典型的。当我们开发一些东西时,我们会在头脑中保留很多可能的用例。但是很难期望程序员在每次更改后手动检查所有这些用例。因此,很容易修复一件事而破坏另一件事。
自动化测试意味着除了代码之外,还会单独编写测试。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。
行为驱动开发 (BDD)
让我们从一种名为 行为驱动开发 或简称 BDD 的技术开始。
BDD 三者合一:测试、文档和示例。
为了理解 BDD,我们将研究一个实际的开发案例。
开发“pow”:规范
假设我们想要创建一个函数 pow(x, n)
,它将 x
提升到整数幂 n
。我们假设 n≥0
。
该任务只是一个示例:JavaScript 中有 **
运算符可以执行此操作,但这里我们专注于可以应用于更复杂任务的开发流程。
在创建 pow
的代码之前,我们可以想象该函数应该做什么并对其进行描述。
这样的描述称为规范,简称规范,其中包含用例的描述以及它们的测试,如下所示
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});
});
规范有三个主要的组成部分,您可以在上面看到
describe("title", function() { ... })
-
我们描述的是什么功能?在我们的例子中,我们描述的是函数
pow
。用于对“工作者”进行分组 -it
块。 it("use case description", function() { ... })
-
在
it
的标题中,我们以人类可读的方式描述特定用例,而第二个参数是测试它的函数。 assert.equal(value1, value2)
-
如果实现正确,
it
块内的代码应该在没有错误的情况下执行。函数
assert.*
用于检查pow
是否按预期工作。我们在这里使用其中一个 -assert.equal
,它比较参数,如果不相等则产生错误。这里它检查pow(2, 3)
的结果是否等于8
。还有其他类型的比较和检查,我们稍后会添加。
该规范可以执行,它将运行在it
块中指定的测试。我们稍后会看到。
开发流程
开发流程通常如下所示
- 编写初始规范,其中包含最基本功能的测试。
- 创建初始实现。
- 为了检查它是否有效,我们运行测试框架Mocha(稍后会详细介绍),它运行规范。虽然该功能尚未完成,但会显示错误。我们进行更正,直到一切正常为止。
- 现在,我们有了带有测试的工作初始实现。
- 我们向规范中添加更多用例,这些用例可能尚未得到实现的支持。测试开始失败。
- 转到 3,更新实现,直到测试没有错误为止。
- 重复步骤 3-6,直到功能就绪。
因此,开发是迭代的。我们编写规范,实现它,确保测试通过,然后编写更多测试,确保它们有效等。最后,我们既有了工作实现,也有了针对它的测试。
让我们在实际案例中了解此开发流程。
第一步已经完成:我们为pow
制定了初始规范。现在,在进行实现之前,让我们使用一些 JavaScript 库来运行测试,只是为了查看它们是否有效(它们都将失败)。
规范的实际应用
在本教程中,我们将使用以下 JavaScript 库进行测试
- Mocha – 核心框架:它提供常见的测试函数,包括
describe
和it
以及运行测试的主函数。 - Chai – 具有许多断言的库。它允许使用许多不同的断言,现在我们只需要
assert.equal
。 - Sinon – 一个用于监视函数、模拟内置函数等的库,我们稍后会需要它。
这些库适用于浏览器内和服务器端测试。这里我们将考虑浏览器变体。
包含这些框架和pow
规范的完整 HTML 页面
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the script with tests (describe, it...) -->
<script src="test.js"></script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
<!-- run tests! -->
<script>
mocha.run();
</script>
</body>
</html>
该页面可以分为五个部分
<head>
– 为测试添加第三方库和样式。- 包含要测试的函数的
<script>
,在我们的案例中 – 包含pow
的代码。 - 测试 – 在我们的案例中,外部脚本
test.js
具有上述describe("pow", ...)
。 - HTML 元素
<div id="mocha">
将由 Mocha 用于输出结果。 - 测试通过命令
mocha.run()
启动。
结果
现在,测试失败了,出现了错误。这是合乎逻辑的:我们在 pow
中有一个空函数代码,所以 pow(2,3)
返回 undefined
而不是 8
。
对于未来,让我们注意,还有更高级别的测试运行器,比如 karma 和其他,它们可以轻松地自动运行许多不同的测试。
初始实现
让我们对 pow
进行一个简单的实现,以便通过测试
function pow(x, n) {
return 8; // :) we cheat!
}
哇,现在它可以工作了!
改进规范
我们所做的绝对是一种作弊。该函数不起作用:尝试计算 pow(3,4)
会得到不正确的结果,但测试通过了。
…但这种情况很典型,在实践中会发生。测试通过,但函数工作错误。我们的规范是不完美的。我们需要向其中添加更多用例。
让我们添加另一个测试来检查 pow(3, 4) = 81
。
我们可以选择两种方法来组织这里的测试
-
第一个变体——在同一个
it
中添加一个assert
describe("pow", function() { it("raises to n-th power", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); });
-
第二个变体——进行两个测试
describe("pow", function() { it("2 raised to power 3 is 8", function() { assert.equal(pow(2, 3), 8); }); it("3 raised to power 4 is 81", function() { assert.equal(pow(3, 4), 81); }); });
主要区别在于当 assert
触发错误时,it
块会立即终止。因此,在第一个变体中,如果第一个 assert
失败,那么我们永远不会看到第二个 assert
的结果。
将测试分开很有用,可以获取有关正在发生的事情的更多信息,因此第二个变体更好。
除此之外,还有一条规则值得遵循。
一个测试检查一件事。
如果我们查看测试并看到其中有两个独立的检查,最好将其拆分为两个更简单的检查。
因此,让我们继续第二个变体。
结果
正如我们所料,第二个测试失败了。当然,我们的函数总是返回 8
,而 assert
则期望 81
。
改进实现
让我们编写更真实的东西以通过测试
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
为了确保函数正常工作,让我们对更多值进行测试。我们可以使用 for
生成 it
块,而不是手动编写它们
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
结果
嵌套描述
我们将添加更多测试。但在那之前,让我们注意辅助函数 makeTest
和 for
应该组合在一起。我们在其他测试中不需要 makeTest
,它只在 for
中需要:它们的共同任务是检查 pow
如何提升到给定的幂。
分组使用嵌套 describe
完成
describe("pow", function() {
describe("raises x to power 3", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
// ... more tests to follow here, both describe and it can be added
});
嵌套的 describe
定义了一个新的测试“子组”。在输出中,我们可以看到标题缩进
将来,我们可以在顶级添加更多 it
和 describe
,并使用它们自己的辅助函数,它们不会看到 makeTest
。
before/after
和 beforeEach/afterEach
我们可以设置 before/after
函数,它们在运行测试之前/之后执行,还可以设置 beforeEach/afterEach
函数,它们在每个 it
之前/之后执行。
例如
describe("test", function() {
before(() => alert("Testing started – before all tests"));
after(() => alert("Testing finished – after all tests"));
beforeEach(() => alert("Before a test – enter a test"));
afterEach(() => alert("After a test – exit a test"));
it('test 1', () => alert(1));
it('test 2', () => alert(2));
});
运行顺序将是
Testing started – before all tests (before)
Before a test – enter a test (beforeEach)
1
After a test – exit a test (afterEach)
Before a test – enter a test (beforeEach)
2
After a test – exit a test (afterEach)
Testing finished – after all tests (after)
通常,beforeEach/afterEach
和 before/after
用于在测试(或测试组)之间执行初始化、清零计数器或执行其他操作。
扩展规范
pow
的基本功能已完成。开发的第一次迭代已完成。当我们庆祝并喝香槟时 - 让我们继续改进它。
如前所述,函数 pow(x, n)
旨在使用正整数 n
。
为了指示数学错误,JavaScript 函数通常返回 NaN
。让我们对 n
的无效值执行相同的操作。
让我们首先将行为添加到规范中(!)
describe("pow", function() {
// ...
it("for negative n the result is NaN", function() {
assert.isNaN(pow(2, -1));
});
it("for non-integer n the result is NaN", function() {
assert.isNaN(pow(2, 1.5));
});
});
包含新测试的结果
新添加的测试失败,因为我们的实现不支持它们。BDD 就是这样完成的:首先,我们编写失败的测试,然后为它们实现。
请注意断言 assert.isNaN
:它检查 NaN
。
在 Chai 中还有其他断言,例如
assert.equal(value1, value2)
– 检查相等性value1 == value2
。assert.strictEqual(value1, value2)
– 检查严格相等性value1 === value2
。assert.notEqual
、assert.notStrictEqual
– 对上述内容进行反向检查。assert.isTrue(value)
– 检查value === true
assert.isFalse(value)
– 检查value === false
- …完整列表在 文档 中
因此,我们应该向 pow
添加几行
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
现在它可以工作了,所有测试都通过
摘要
在 BDD 中,规范优先,然后是实现。最后,我们同时拥有规范和代码。
规范可以用三种方式使用
- 作为测试 – 它们保证代码正常工作。
- 作为文档 –
describe
和it
的标题说明了函数的作用。 - 作为示例 – 测试实际上是工作示例,展示了如何使用函数。
有了规范,我们可以安全地改进、更改,甚至从头重写函数,并确保它仍然正常工作。
这在大型项目中尤其重要,因为函数在许多地方使用。当我们更改此类函数时,根本无法手动检查使用它的每个地方是否仍然正常工作。
如果没有测试,人们有两种方法
- 无论如何执行更改。然后我们的用户遇到错误,因为我们可能无法手动检查某些内容。
- 或者,如果错误的惩罚很严厉,因为没有测试,人们就会害怕修改此类函数,然后代码就会过时,没有人愿意参与其中。不利于开发。
自动化测试有助于避免这些问题!
如果项目包含测试,则根本不存在此类问题。在进行任何更改后,我们可以运行测试,并在几秒钟内看到很多检查结果。
此外,经过良好测试的代码具有更好的架构。
自然,这是因为自动测试的代码更容易修改和改进。但还有另一个原因。
要编写测试,代码应按以下方式组织:每个函数都有一个明确描述的任务、定义明确的输入和输出。这意味着从一开始就有一个良好的架构。
在现实生活中,有时并不那么容易。有时在编写实际代码之前很难编写规范,因为尚不清楚它应该如何表现。但总体而言,编写测试可以使开发更快、更稳定。
在教程的后面,您将遇到许多包含内置测试的任务。因此,您将看到更多实际示例。
编写测试需要良好的 JavaScript 知识。但我们才刚刚开始学习它。因此,为了解决所有问题,从现在开始,您不必编写测试,但您应该已经能够阅读它们,即使它们比本章中的内容复杂一些。
评论
<code>
标记,对于多行 - 将其包装在<pre>
标记中,对于超过 10 行 - 使用沙箱 (plnkr、jsbin、codepen…)