原文: Unit Testing Beginner's Guide - Part 1 - Testing Functions
作者:jstweetster
你已经决定开始对代码进行单元测试,但是不知道从哪里开始或者围绕该代码的最佳实践是什么。
在这个系列中,我计划在从基本原理开始,并使用您可能直到现在才知道的高级技术,在单元测试的领域中引导你。
开始之前,你需要安装如下依赖:
请注意下面事项:
node -v
。确保版本号 >= 6.x,如果不是请安装unit-testing-functions
目录cd unit-testing-functions
并且初始化该JavaScript项目:npm init--yes
package.json
文件nom i jest --save-dev
./node_modules/.bin/jest -v
一切准备就绪,我们开始进入单元测试啦。
我们将从简单的函数开始,并且,随着我们进行一系列的单元测试,我们将会继续探索更复杂的数据结构和设置。
让我们开始定义一个简单的函数:
在unit-testing-functions
项目中创建一个sum.js
文件。定义如下函数:
module.export = function sum (a, b) {
return a + b;
}
这个函数我们将会进行测试。单元测试背后的想法是提供尽可能多的输入类型,以涵盖所有条件分支。
现在,没有任何条件分支,但我们应该改变我们对函数的输入,以确保它继续正确运行,即使代码在将来被更改。
每一个你写的代码文件都应该有一个对应的Spec
文件,它通常在代码文件旁边。例如:touch sum.spec.js
或者手动创建。
在spec文件中,我们将会写测试方法
Jest和其他测试框架将测试组织到测试用例中,为了简单管理和记录,每个测试用例包含多个单独的测试。
让我们添加我们的第一个测试(在sum.spec.js
中):
const sum = require('./sum.js');
describe('sum suite', function () {
test('should add 2 positive numbers together and return the result', function () {
expect(sum(1, 2)).toBe(3);
});
});
如果这看起来令人生畏或不清楚,请不要担心,它会在少数情况下有意义。
那么,这里发生了什么?
const sum = require('./sum.js');
我们引入要测试的函数。我们使用 module.exports
从模块中暴露函数,并且使用 require
引入到要测试的文件中。这是因为Jest在能识别这些结构的NodeJS上运行我们的测试。
此代码不是在浏览器中运行,不使用像Webpack这样的模块打包器,但另一篇文章将会介绍。
接下来,我们定义了测试单元,它将包含所有有关 sum
函数的测试方法。
describe('sum suite', function () {
// define here the individual tests
})
最后我们添加我们第一个测试(我们将会在上面的测试单元中添加更多的测试):
test('should add 2 positive numbers together and return the result', function () {
expect(sum(1, 2)).toBe(3);
});
下面的代码可能也不是很清楚
expect(sum(1, 2)).toBe(3)
这是任何单元测试的构建块,被称作为“断言(assertion)”。断言基本上是一种表达对事物应该如何表现的期望的方式。在我们的示例中,我们期望调用 sum(1, 2)
应该返回的结果是 3
。
toBe
被称作为“匹配器(matcher)”。在 Jest 里有很多的匹配器,每一个匹配器都有助于验证一个特定的方面:比如测试对象是否相等等。
那么,expect
从哪里来的呢?我们也没有从任何地方导入进来。
事实证明,Jest作为全局变量,提供了 describe
、test
、 expect
和一些其他函数,因此你无需导入他们。你可以到 官方文档 查看完整列表
你可以在我们的项目根目录中直接调用Jest来运行单元测试 ./node_modules/.bin/jest
更好,更跨平台的方式是定义一个NPM脚本来运行这些测试。例如,打开 package.json
文件并且编辑下面的部分:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
改为:
"scripts": {
"test": "jest"
}
运行 npm run test
,你将会看到成功的输出:
➜ unit-testing-functions npm run test
> unit-testing-functions@1.0.0 test /Users/zouyuwei/code/web/_SELF/unit-testing-functions
> jest
PASS __tests__/sum.spec.js
sum suite
✓ should add 2 positive numbers together and return the result (8ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.74s
Ran all test suites.
很好,你的第一个测试通过了!
现在,快进几周或者几个月,假设你的同事正在研究 sum
函数并且决定更改它的实现方法如下所示:
module.exports = function sum(a, b) {
return a - b;
}
请更改它(这仅仅只是为了演示)。现在你的同事在提交这些改变之前运行单元测试。输出将会如下所示:
➜ unit-testing-functions npm run test
> unit-testing-functions@1.0.0 test /Users/zouyuwei/code/web/_SELF/unit-testing-functions
> jest
FAIL __tests__/sum.spec.js
sum suite
✕ should add 2 positive numbers together and return the result (30ms)
● sum suite › should add 2 positive numbers together and return the result
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: -1
2 | describe('sum suite', function () {
3 | test('should add 2 positive numbers together and return the result', function () {
> 4 | expect(sum(1, 2)).toBe(3);
| ^
5 | });
6 | });
at Object.toBe (__tests__/sum.spec.js:4:23)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 3.303s
Ran all test suites.
通过查看上面的输出信息,能够很容易的得出结论:在 __tests__/sum.spec.js
文件的第四行出错了,如跟踪堆栈所示 at Object.toBe (__tests__/sum.spec.js:4:23)
。因此可以得出结论 expect(sum(1,2)).toBe(3);
这一行出错了。通过检查控制台的输出,我们能够看到,期望的值是 ‘3’,而接收的值是 ‘-1’。
因此,单元测试既是防止回归的一种方式,也是一种活文档。
最后,请更改 a - b
为 a + b
。
我们进行了第一次测试,并且它涵盖了sum函数中的所有分支,有许多场景我们还没有测试过。
考虑一下测试中的功能,不仅要考虑今天的实现,还要考虑它如何随着时间的推移而发展。我们希望捕获函数停止工作的情况,即使有人修改了它的实现,并添加了额外的检查和分支。
因此,让我们通过创建额外的单元测试来扩展测试范围。在 sum.spec.js
文件中添加如下代码:
const sum = require('../sum.js');
describe('sum suite', function () {
test('should add 2 positive numbers together and return the result', function () {
expect(sum(1, 2)).toBe(3);
});
test('Should add 2 negative numbers together and return the result', function() {
expect(sum(-1, -2)).toBe(-3);
});
test('Should add 1 positive and 1 negative numbers together and return the result', function() {
expect(sum(-1, 2)).toBe(1);
});
test('Should add 1 positive and 0 together and return the result', function() {
expect(sum(0, 2)).toBe(2);
});
test('Should add 1 negative and 0 together and return the result', function() {
expect(sum(0, -2)).toBe(-2);
});
});
除了最初的测试用例之外,我们刚添加了4个测试用例。注意我们如何改变函数的输入以及我们如何尝试击中边缘情况(例如,通过添加0)。
运行单元测试,你将会看到:
➜ unit-testing-functions npm run test
> unit-testing-functions@1.0.0 test /Users/zouyuwei/code/web/_SELF/unit-testing-functions
> jest
PASS __tests__/sum.spec.js
sum suite
✓ should add 2 positive numbers together and return the result (7ms)
✓ Should add 2 negative numbers together and return the result (1ms)
✓ Should add 1 positive and 1 negative numbers together and return the result (1ms)
✓ Should add 1 positive and 0 together and return the result (1ms)
✓ Should add 1 negative and 0 together and return the result
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 2.575s
Ran all test suites.
虽然我们在扩展单元测试覆盖率方面做得很好,但测试可以为我们做更多的事情。
如果我们认为我们还没有涉及的其他方案真的很好,你能想出一些目前代码处理不当的方法吗?
如何传递除数字以外的输入?
填写如下测试用例:
test('Should raise an error if one of the inputs is not a number', function() {
expect(() => sum('0', -2)).toThrowError('Both arguments must be numbers');
});
首先,我们用一个匿名函数包装我们要测试的代码:() => sum('0', -2)
。
这是必需的,因为在测试一段代码时抛出的任何未捕获的异常都会触发测试失败。
在这个例子当中,当参数不是数据的时候,我们期望 sum
函数抛出异常,但是我们不希望这是被认为是测试失败的: 相反的,这是预期的行为,应该被认为是通过的测试用例。
因此,我们将其包装在一个匿名函数中,并引入一个新的匹配器:toThrowError
。
运行测试,观察如下:
FAIL __tests__/sum.spec.js
sum suite
✕ Should raise an error if one of the inputs is not a number (18ms)
● sum suite › Should raise an error if one of the inputs is not a number
expect(function).toThrowError(string)
Expected the function to throw an error matching:
"Both arguments must be numbers"
But it didn't throw anything.
22 |
23 | test('Should raise an error if one of the inputs is not a number', function() {
> 24 | expect(() => sum("0", -2)).toThrowError('Both arguments must be numbers');
| ^
25 | });
26 | });
at Object.toThrowError (__tests__/sum.spec.js:24:32)
此时要抵制修改测试代码的诱惑。
这个测试很清楚的说明了该函数实现上的问题:
to throw an error matching:“Both arguments must be numbers
。实际上它没有抛出任何异常。at Object.toThrowError (__tests__/sum.spec.js:24:32)
。在指定的行和列上,您可以看到对应的断言: expect(() => sum("0", -2)).toThrowError('Both arguments must be numbers');
。所以我们的单元测试刚刚发现了一个bug,该是修复的时候了!
更改 sum.js
的代码,以考虑错误的输入类型,并在这种情况下抛出适当的异常:
module.exports = function sum (a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a + b;
}
再次运行测试脚本,观察所有的测试都通过了。好样的!
请注意:我们首先添加了一个单元测试,然后再进入并添加代码,这表明 sum
函数在某些条件下无法正常运行。
我们看到了测试FAILING,我们添加了修复bug的代码并观看了测试PASSING。
在开发新代码/修复现有代码时,您应始终遵循此过程。
到目前为止,您可能已经注意到,每次添加代码或更新单元测试时,我们都必须不断重新运行单元测试。
这很快就会变得烦人并妨碍实际的开发工作流程。幸运的是,大多数测试运行程序允许设置文件监视模式,当磁盘上的文件发生更改时,它会重新运行单元测试。
修改 package.json
文件,将 “script” 部分改为:
"script": {
"test": "jest --watch"
}
运行单元测试: npm run test
观察现在测试运行器没有退出而是等待命令。
改变 sum.js
或者 sum.spec.js
文件,会看到测试正在重新运行。
.spec
文件,通常位于代码文件的旁边(译者觉得:或者是放到统一的测试文件夹中)。这使得某人能够快速浏览与组件相关的测试并了解其工作原理。test
语句的文本说明非常重要:确保它们非常清晰,可读并且能够确定哪些条件下的预期行为。描述文本通常应该遵循如下模板:“[在什么情况下] 应该 [期望什么] ” (Should [what's to be expected] when [under which circumstances])。 test
单元,并且清晰的命名、描述和运用该场景。这就是我们对单元测试的介绍。
扫码关注w3ctech微信公众号
共收到0条回复