什么是测试
维基百科的定义:
在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。
也可以这样理解:测试的作用是为了提高代码质量和可维护性。
- 提高代码质量:测试就是找 BUG,找出 BUG,然后解决它。BUG 少了,代码质量自然就高了。
- 可维护性:对现有代码进行修改、新增功能从而造成的成本越低,可维护性就越高。
什么时候写测试
如果你的程序非常简单,可以不用写测试。例如下面的程序,功能简单,只有十几行代码:
function add(a, b) { return a + b } function sum(data = []) { let result = 0 data.forEach(val => { result = add(result, val) }) return result } console.log(sum([1,2,3,4,5,6,7,8,9,10])) // 55
如果你的程序有数百行代码,但封装得很好,完美的践行了模块化的理念。每个模块功能单一、代码少,也可以不用写测试。
如果你的程序有成千上万行代码,数十个模块,模块与模块之间的交互错综复杂。在这种情况下,就需要写测试了。试想一下,在你对一个非常复杂的项目进行修改后,如果没有测试会是什么情况?你需要将跟这次修改有关的每个功能都手动测一边,以防止有 BUG 出现。但如果你写了测试,只需执行一条命令就能知道结果,省时省力。
测试类型与框架
测试类型有很多种:单元测试、集成测试、白盒测试...
测试框架也有很多种:Jest、Jasmine、LambdaTest...
本章将只讲解单元测试和 E2E 测试(end-to-end test 端到端测试)。其中单元测试使用的测试框架为 Jest,E2E 使用的测试框架为 Cypress。
Jest
安装
npm i -D jest
打开 package.json
文件,在 scripts
下添加测试命令:
"scripts": { "test": "jest", }
然后在项目根目录下新建 test
目录,作为测试目录。
单元测试
什么是单元测试?维基百科中给出的定义为:
单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
从前端角度来看,单元测试就是对一个函数、一个组件、一个类做的测试,它针对的粒度比较小。
单元测试应该怎么写呢?
- 根据正确性写测试,即正确的输入应该有正常的结果。
- 根据错误性写测试,即错误的输入应该是错误的结果。
对一个函数做测试
例如一个取绝对值的函数 abs()
,输入 1,2
,结果应该与输入相同;输入 -1,-2
,结果应该与输入相反。如果输入非数字,例如 "abc"
,应该抛出一个类型错误。
// main.js function abs(a) { if (typeof a != 'number') { throw new TypeError('参数必须为数值型') } if (a < 0) return -a return a } // test.spec.js test('abs', () => { expect(abs(1)).toBe(1) expect(abs(0)).toBe(0) expect(abs(-1)).toBe(1) expect(() => abs('abc')).toThrow(TypeError) // 类型错误 })
现在我们需要测试一下 abs()
函数:在 src
目录新建一个 main.js
文件,在 test
目录新建一个 test.spec.js
文件。然后将上面的两个函数代码写入对应的文件,执行 npm run test
,就可以看到测试效果了。
对一个类做测试
假设有这样一个类:
class Math { abs() { } sqrt() { } pow() { } ... }
我们必须把这个类的所有方法都测一遍。
test('Math.abs', () => { // ... }) test('Math.sqrt', () => { // ... }) test('Math.pow', () => { // ... })
对一个组件做测试
组件测试比较难,因为很多组件都涉及了 DOM 操作。
例如一个上传图片组件,它有一个将图片转成 base64 码的方法,那要怎么测试呢?一般测试都是跑在 node 环境下的,而 node 环境没有 DOM 对象。
我们先来回顾一下上传图片的过程:
- 点击
,选择图片上传。
- 触发
input
的change
事件,获取file
对象。 - 用
FileReader
将图片转换成 base64 码。
这个过程和下面的代码是一样的:
document.querySelector('input').onchange = function fileChangeHandler(e) { const file = e.target.files[0] const reader = new FileReader() reader.onload = (res) => { const fileResult = res.target.result console.log(fileResult) // 输出 base64 码 } reader.readAsDataURL(file) }
上面的代码只是模拟,真实情况下应该是这样使用:
document.querySelector('input').onchange = function fileChangeHandler(e) { const file = e.target.files[0] tobase64(file) } function tobase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (res) => { const fileResult = res.target.result resolve(fileResult) // 输出 base64 码 } reader.readAsDataURL(file) }) }
可以看到,上面的代码出现了 window 的事件对象 event
、FileReader
。也就是说,只要我们能够提供这两个对象,就可以在任何环境下运行它。所以我们可以在测试环境下加上这两个对象:
// 重写 File window.File = function () {} // 重写 FileReader window.FileReader = function () { this.readAsDataURL = function () { this.onload && this.onload({ target: { result: fileData, }, }) } }
然后测试可以这样写:
// 提前写好文件内容 const fileData = 'data:image/test' // 提供一个假的 file 对象给 tobase64() 函数 function test() { const file = new File() const event = { target: { files: [file] } } file.type = 'image/png' file.name = 'test.png' file.size = 1024 it('file content', (done) => { tobase64(file).then(base64 => { expect(base64).toEqual(fileData) // 'data:image/test' done() }) }) } // 执行测试 test()
通过这种 hack 的方式,我们就实现了对涉及 DOM 操作的组件的测试。我的 vue-upload-imgs 库就是通过这种方式写的单元测试,有兴趣可以了解一下(测试文件放在 test
目录)。
测试覆盖率
什么是测试覆盖率?用一个公式来表示:代码覆盖率 = 已执行的代码数 / 代码总数
。Jest 如果要开启测试覆盖率统计,只需要在 Jest 命令后面加上 --coverage
参数:
"scripts": { "test": "jest --coverage", }
现在我们用刚才的测试用例再试一遍,看看测试覆盖率。
// main.js function abs(a) { if (typeof a != 'number') { throw new TypeError('参数必须为数值型') } if (a < 0) return -a return a } // test.spec.js test('abs', () => { expect(abs(1)).toBe(1) expect(abs(0)).toBe(0) expect(abs(-1)).toBe(1) expect(() => abs('abc')).toThrow(TypeError) // 类型错误 })
上图表示每一项覆盖率都是 100%。
现在我们把测试类型错误的那一行代码注释掉,再试试。
// test.spec.js test('abs', () => { expect(abs(1)).toBe(1) expect(abs(0)).toBe(0) expect(abs(-1)).toBe(1) // expect(() => abs('abc')).toThrow(TypeError) })
可以看到测试覆盖率下降了,为什么会这样呢?因为 abs()
函数中判断类型错误的那个分支的代码没有执行。
// 就是这一个分支语句 if (typeof a != 'number') { throw new TypeError('参数必须为数值型') }