测试
为什么项目需要测试
测试是完善的研发体系中不可或缺的一环,前端同样需要测试。一个项目最终会经过快速迭代走向以维护为主的状态,在合理的时机以合适的方式引入自动化能够让我们提前发现 bug,此时定位和修复的速度比开发完再被叫去修改 bug 要快许多;在项目重构或者开发人员发生变化也能保障预期功能的实现。
可测试方向
- 界面回归测试: 测试界面是否正常,这是前端测试最基础的环节
- 功能测试: 测试功能操作是否正常,由于涉及交互,这部分测试比界面测试会更复杂
- 性能测试: 页面性能越来越受到关注,并且性能需要在开发过程中持续关注,否则很容易随着业务迭代而下降
- 页面特征检测: 有些动态区域无法通过界面对比进行测试、也没有功能上的异常,但可能不符合需求。例如性能测试中移动端大图素材检测就是一种特征检测,另外常见的还有页面区块静态资源是否符合预期等等。
前端测试框架
前端测试工具也和前端的框架一样纷繁复杂,其中常见的测试工具,大致可分为测试框架、断言库、测试覆盖率工具等几类。常见的测试框架有Jasmine
, Mocha
, 以及要介绍的 Jest
。
测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组。
测试框架可分为两种,TDD (测试驱动开发)和 BDD (行为驱动开发)。
前端是一种特殊的GUI软件.
断言库
所谓断言,即提供语义化的方法,用于对参与测试的值做各种各样的判断,如不一致就抛出错误。常见的断言库有 should.js
, Chai.js
所有的测试用例(it块)都应该含有一句或多句的断言。它是编写测试用例的关键
1
|
expect(add(1, 1)).to.be.equal(2);
|
Jest
Jest 内置了常用的测试工具,如断言、测试覆盖率。
命名惯例
测试文件有如下常见的命名惯例。
__tests__
目录下以.js
为后缀的文件。- 以
.test.js(x)
或者.spec.js(x)
为后缀的文件。
测试文件可以位于项目根目录下任何位置,可以通过 testMatch
修改默认配置。
编写测试
Jest 的作用是运行测试脚本。通常,测试脚本与所要测试的源码脚本同名,但是后缀名为 .test.js
。测试脚本可以独立运行。
测试脚本里包含一个或多个 describe
块, 每个 describe
里应该包含一个或多个 test
块。
1
2
3
4
5
6
7
8
9
10
11
12
|
/* add.js */
function add(x, y) {
return x + y;
}
/* add.test.js */
const add = require('./add.js');
describe('加法函数的测试', function() {
it('1 加 1 应该等于 2', function() {
expect(add(1, 1)).toBe(2);
});
});
|
Jest 提供了内置的全局函数 expect
进行断言。
describe
称为测试套件(test suite),表示一组相关的测试。第一个参数是测试套件的名称,第二个参数是实际执行的函数。
test
称为测试用例(test case),表示一个单独的测试,是测试的最小单位。第一个参数是测试用例的名称,第二个参数是实际执行的函数。
1
2
3
|
it('work without done', () => {}); // 同步执行
it('work with done', (done) => {}); // 触发异步,执行 done() 通知 Jest 之行完毕
|
异步测试
使用单个参数调用 done
, Jest 会等 done
回调函数执行结束后,结束测试。如果 done()
永远不被调用,这个测试将失败。
1
2
3
4
5
6
7
8
9
|
it('works with done', (done) => {
var x = true;
var f = function() {
x = false;
expect(x).toBeFalsy();
done(); // 通知 Jest 测试结束
};
setTimeout(f, 4000);
});
|
Jest 支持使用 Promise, 从测试返回一个 Promise, Jest 会等待这一 Promise resolve。 如果 Promise 被拒绝,则测试将自动失败。
1
2
3
4
5
6
7
8
9
10
11
12
|
const requestFn = (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('name');
}, 1000);
})
}
it('works with promises', () => {
expect.assertions(1); // 当前测试中执行断言的次数
return requestFn('mxl').then(data => expect(data).toBe('name'));
});
|
Jest 也支持 async/await 语法的测试,无需多余的操作,只要在 await 后进行断言即可。
可以使用 expect.assertions
来验证一定数量的断言被调用,以判断异步代码是否如预期一般执行。
测试组件
冒烟测试 验证一个组件渲染没有抛出异常,浅渲染并且测试一些输出,完整渲染测试组件的生命周期和状态的改变。
1
2
3
4
5
6
7
8
|
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});
|
初始化测试环境
使用 browser API 需要 mock,或者在测试前运行全局的配置,可以在 setup.js
文件里配置。
测试用例钩子
有时我们想在测试开始之前进行下环境的检查、或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
describe('hooks', function() {
beforeAll(function() {
// 在本区块的所有测试用例之前执行
});
afterAll(function() {
// 在本区块的所有测试用例之后执行
});
beforeEach(function() {
// 在本区块的每个测试用例之前执行
});
afterEach(function() {
// 在本区块的每个测试用例之后执行
});
// test cases
});
|
测试用例管理
项目中测试用例很多,但希望只运行其中的几个,可以用only方法,describe
块和 test
块都允许调用 only
方法,表示只运行某个测试套件或测试用例。
1
2
|
it.only('1 加 1 应该等于 2', () => { ... });
fit('1 加 1 应该等于 2', () => { ... });
|
此外,还有 skip
方法,表示跳过指定的测试套件或测试用例。
1
2
|
it.skip('1 加 1 应该等于 2', () => { ... });
xit('1 加 1 应该等于 2', () => { ... });
|
覆盖率报告
Jest 匹配文件生成测试报告,不需要额外的配置。
除了会再终端展示测试覆盖率情况,还会在项目下生产一个 coverage 目录。
1
|
npm test -- --coverage
|
小结
对于一些需求频繁变更、复用性较低的内容,编写测试用例得不偿失,适合引入测试用例的场景如下:
- 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性
- 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低
- 被多次复用的部分,比如一些通用组件和库函数。因为多处复用,更要保障质量
参考文献
react-test-demo(git上挺好的中文讲解)
jest(中文)
jest(英文)
Jest Snapshots and Beyond - React Conf 2017
The Difference Between TDD and BDD