1 为什么要集成单元测试
测试最重要的是提升代码质量。通过测试用例,在上线发布前跑一下,可以杜绝各种疏忽而引起的功能bug。对于稳定持续发展的项目(比如组件库、封装库、共用方法库)集成单元测试是非常必要的,比开发完让QA测试来说开发效率更高。
2 什么是单元测试
对软件中的最小可测试单元进行测试,比如跑一个方法或API看是否符合预期
3 单元测试好处
1)预见性:保证代码是想要的结果 2)早期性:尽早发现问题 3)说明性:单元测试可以说明代码功能 4)保障性:对已有功能进行重构时,不会影响历史功能
4 选什么测试工具
前端测试工具和前端框架一样非常多,只有适合的才是最好的。目前常用的测试工具大致分为测试框架、断言库、测试覆盖率工具等几类。每一个又有很多工具,比如
- 测试框架:Jest、Mocha,测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组
- 断言库:chai、should、expect等等,断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断
- test runner 测试运行环境: karma
- mock库 sinon
- 代码覆盖率:用于统计测试用例对代码的测试情况,生成相应的报表,比如Istanbul
5 流行测试框架对比
衡量指标:运行环境的支持、并发的支持、库的集成度和社区活跃度。
- Jest 它是 Facebook 出品的一个测试框架,最大的特点就是就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。因此使用非常简单,安装依赖少,而且基本零配置
- Mocha 强大的测试框架,常见的describe,beforeEach就来自这里,需要配合断言库等,安装依赖库及配置相对也比较多。
6 结论
通过对比,选择使用Jest
- Jest测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度
- 安装配置简单,非常容易上手,几乎是零配置的,通过npm 命令安装就可以直接运行了
- Jest 内置了测试覆盖率工具istanbul,可以通过命令开启或者在 package.json 文件进行更详细的配置。运行 istanbul 除了会再终端展示测试覆盖率情况,还会在项目下生产一个 coverage 目录,内附一个测试覆盖率的报告,让我们可以清晰看到分支的代码的测试情况
- 内部集成了断言库,不需要再引入第三方的断言库
总之, jest 包含了 karma + mocha + chai + sinon 的所有常用功能,零配置开箱即用,是一个很好的选择。
7 使用说明
- 前提环境准备:node环境(10+以上版本)
- 安装jest:
npm install jest -D
- package.json修改脚本命令
"scripts": {
"test": "jest"
}
- 开始写测试之前的准备 4.1 需要明确的事
- 测试对象是什么
- 要测试什么功能
- 预期想得到什么结果
- 是否有外部依赖 4.2 执行步骤
- 构造参数,创建外部依赖的mock,进行必要的设置
- 用构造好的参数执行被测试代码
- 用实际得到的结果与期望的结果比较
- 移除在准备阶段创建的外部依赖一级其他影响 4.3 测试用例写法
- jest支持三种方式写测试代码
- 放到__tests__目录文件夹下
- 使用.spec.js后缀
- 创建.test.js后缀 如下所示:
一般为了统一目录采用第一种方式,将所有的测试用例统一放到__tests__目录中。
- 测试代码结构
- describe:将几个相关的测试放到一个组中,非必须
- test:(别名it)测试用例,是测试的最小单位
- expect:提供很多matcher来判断方法的返回值是否符合特定条件
- 异步方法测试
- expect.assertions():确保异步方法被调用
- done参数:参数.jest会等到done执行的时候才结束测试
- promise的resolves
- Async await支持
const {delay, delayPromise} = require('../index');
test('callback被执行', done => {
expect.assertions(1);
const callback = () => {
console.log('callback expect');
expect(true).toBe(true);
done();
};
delay(callback);
});
test('delayPromise 测试 被执行', () => {
expect.assertions(1);
const callback = () => 1;
return expect(delayPromise(callback)).resolves.toBe(1);
});
test('delayPromise async 测试 被执行', async () => {
expect.assertions(1);
const callback = () => 1;
const res = await delayPromise(callback);
expect(res).toBe(1);
})
- 依赖外部方法的测试
- jest.fn()创建一个mock方法
const delayPromise = require('../index');
test('delayPromise 测试 被执行', () => {
expect.assertions(1);
const callback = jest.fn().mockReturnValue(1);
return expect(delayPromise(callback)).resolves.toBe(1);
});
- jest.mock()
// 通过mock方法对getData模块进行mock各自数据
jest.mock('../getData');
const normalizeData = require('../normalizeData');
const getData = require('../getData');
getData.mockReturnValue({
name: 'xx'
})
test('normalizeData.js 测试', () => {
const status = normalizeData().status;
expect(status).toBe(0);
});
test('normalizeData.js 测试', () => {
const status = normalizeData().data;
expect(status).toEqual({
name: 'xx'
});
});
- jest.spyOn():可以针对对象的属性进行监听
- beforeEach/afterEach:在每个用例开始前/后都进行预处理
- beforeAll/afterAll:对测试文件中所有的用例开始前/后进行统一的预处理
随机数测试demo
const getRandom = require('../getRandom');
// 每个测试用例执行前执行
let mockRandom = null;
beforeEach(() => {
// 对Math的random方法进行监听
mockRandom = jest.spyOn(Math, 'random');
});
// 每个测试用例执行后执行
afterEach(() => {
// 恢复现场,防止其他测试用例执行失败
mockRandom.mockRestore();
});
test('getRandom 小于10' ,() => {
expect(getRandom()).toBeLessThan(10);
});
test('getRandom 大于等于0' ,() => {
expect(getRandom()).toBeGreaterThanOrEqual(0);
});
test('Math.random 返回0.11,结果为1', () => {
mockRandom.mockReturnValue(0.11);
expect(getRandom()).toBe(1);
});
test('Math.random 返回1,结果为10', () => {
mockRandom.mockReturnValue(1);
expect(getRandom()).toBe(10);
})
执行npm run test结果如下
更多测试用例demo参考:github.com/mengmengzp/…