测试是软件开发工做的重要一环,甚至有一种测试驱动开发(Test-Driven Development)的研发模式,要求整个研发工做是从编写测试用例开始。测试根据不一样的维度有多种分类方式,按照测试阶段主要有单元测试、集成测试和系统测试,而单元测试是保障程序基本正确性的重中之重。html
单元测试(Unit Tesing)是针对程序的最小部件,检查代码是否会按照预期工做的一种测试手段。在过程式编程中最小就是一个函数,在面向对象编程中最小部件就是对象方法。前端
下文介绍使用 jest 对 Node.js 程序进行单元测试node
单元测试的执行一般须要测试规范、断言、mock、覆盖率工具等支持,上述工具在繁荣的 Node.js 生态中有不少优秀实现,但组合起来使用会带来两个问题react
多种工具的选择和学习有必定的成本ios
把多个工具组合成特定测试解决方案的配置复杂git
而 Jest 是用来建立、执行和构建测试用例的 JavaScript 测试库,自身包含了 驱动、断言库、mock 、代码覆盖率等多种功能,配置使用至关简单github
$ npm i --save-dev jest
复制代码
把 jest 安装到项目的 devDepecencies 后,在 package.json 添加配置正则表达式
"scripts": {
"test": "jest"
}
复制代码
这样就可使用命令 npm test
执行测试代码了typescript
根目录下的 jest.config.js
文件能够自定义 jest 的详细配置,虽然 jest 相关配置也能够在 package.json 内,但为了可读性推荐在独立文件配置npm
.
├── src
│ └── sum.js
├── test
│ └── sum.test.js
├── .gitignore
├── jest.config.js
├── README.md
└── package.json
复制代码
function sum(a, b) {
return a + b;
}
module.exports = sum;
复制代码
const sum = require('../src/sum');
test('1 + 2 = 3', () => {
expect(sum(1, 2)).toBe(3);
});
复制代码
在测试用例中使用 expect(x).toBe(y)
的方式表达 x 与 y 相同,相似 Node.js 提供的 assert(x, y) 断言,相对而言 jest 提供的语法有更好的语义性和可读性
$ npm test
复制代码
jest 会自动运行
sum.test.js
文件,其默认匹配规则
匹配 __test__
文件夹下的 .js 文件(.jsx .ts .tsx 也能够)
匹配全部后缀为 .test.js
或 .spec.js
的文件(.jsx .ts .tsx 也能够)
能够经过根目录下的 jest.config.js
文件自定义测试文件匹配规则
module.exports = {
testMatch: [ // glob 格式
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[jt]s?(x)"
],
// 正则表达式格式,与 testMatch 互斥,不能同时声明
// testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
};
复制代码
jest 提供了 BDD 风格的断言支持,功能十分丰富,介绍几个最经常使用的
.toBe()
使用 Object.is 来测试两个值精准相等
expect(2 + 2).toBe(4);
复制代码
若是测试对象可使用 toEqual()
,递归检查数组或对象的每一个字段
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
复制代码
添加 not
能够表达相反匹配
expect(a + b).not.toBe(0);
复制代码
toBeNull
只匹配 null
toBeUndefined
只匹配 undefined
toBeDefined
与 toBeUndefined
相反
toBeTruthy
匹配任何 if
语句为真
toBeFalsy
匹配任何 if
语句为假
test('null', () => { const n = null; expect(n).toBeNull(); expect(n).toBeDefined(); expect(n).not.toBeUndefined(); expect(n).not.toBeTruthy(); expect(n).toBeFalsy(); });
test('zero', () => { const z = 0; expect(z).not.toBeNull(); expect(z).toBeDefined(); expect(z).not.toBeUndefined(); expect(z).not.toBeTruthy(); expect(z).toBeFalsy(); });
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
});
复制代码
对于比较浮点数相等,使用 toBeCloseTo
而不是 toEqual
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
expect(value).toBe(0.3); // 这句会报错,由于浮点数有舍入偏差
expect(value).toBeCloseTo(0.3); // 这句能够运行
});
复制代码
能够经过 toContain
来检查一个数组或可迭代对象是否包含某个特定项
expect(shoppingList).toContain('beer');
复制代码
jest 对几种常见的异步方法提供了测试支持
src/async.js
module.exports = {
cb: fn => {
setTimeout(() => {
fn('peanut butter');
}, 300);
},
pm: () => {
return new Promise(resolve => {
setTimeout(() => {
resolve('peanut butter');
}, 300);
});
},
aa: async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve('peanut butter');
}, 300);
});
}
};
复制代码
test/async.test.js
const { cb, pm, aa } = require('../src/async');
复制代码
test 方法的第二个函数传入 done
能够用来标识回调执行完成
test('callback data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
cb(callback);
});
复制代码
test('promise then data is peanut butter', () => {
return pm().then(data => {
expect(data).toBe('peanut butter');
});
});
复制代码
必定要把 Promise 作为返回吃,不然测试用例会在异步方法执行完以前结束,若是但愿单独测试 resolve 可使用另一种书写方式
test('promise resolve data is peanut butter', () => {
return expect(pm()).resolves.toBe('peanut butter');
});
复制代码
async/await 测试比较简单,只要外层方法声明为 async 便可
test('async/await data is peanut butter', async () => {
const data = await aa();
expect(data).toBe('peanut butter');
});
复制代码
写测试用例的时候常常须要在运行测试前作一些预执行,和在运行测试后进行一些清理工做,Jest 提供辅助函数来处理这个问题
若是在每一个测试任务开始前须要执行数据初始化工做、结束后执行数据清理工做,可使用 beforeEach 和 afterEach
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
复制代码
若是相关任务全局只须要执行一次,可使用 beforeAll 和 afterAll
beforeAll(() => {
return initializeCityDatabase();
});
afterAll(() => {
return clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
复制代码
默认状况下,before 和 after 的块能够应用到文件中的每一个测试。 此外能够经过 describe
块来将测试分组。 当 before 和 after 的块在 describe
块内部时,则其只适用于该 describe
块内的测试
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
复制代码
在不少时候测试用例须要在相关环境下才能正常运行,jest 提供了丰富的环境模拟支持
使用 jest.fn() 就能够 mock 一个函数,mock 函数有 .mock
属性,标识函数被调用及返回值信息
const mockFn = jest.fn();
mockFn
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
复制代码
使用 jest.mock(模块名) 能够 mock 一个模块,好比某些功能依赖了 axios 发异步请求,在实际测试的时候咱们但愿直接返回既定结果,不用发请求,就能够 mock axios
// src/user.js
const axios = require('axios');
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
module.exports = Users;
// /src/user.test.js
const axios = require('axios');
const Users = require('../src/user');
jest.mock('axios'); // mock axios
test('should fetch users', () => {
const users = [{ name: 'Bob' }];
const resp = { data: users };
// 修改其 axios.get 方法,直接返回结果,避免发请求
axios.get.mockResolvedValue(resp);
// 也能够模拟其实现
// axios.get.mockImplementation(() => Promise.resolve(resp));
return Users.all().then(data => expect(data).toEqual(users));
});
复制代码
如今不少前端代码直接使用了 ES6 和 Typescript,jest 能够经过简单配置支持
$ npm i -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
复制代码
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};
复制代码
这样测试用例也能够用 ES6 + TypeScript 了
$ npm i -S react react-dom
$ npm i -D @babel/preset-react enzyme enzyme-adapter-react-16
复制代码
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
'@babel/preset-react',
],
};
复制代码
// src/checkbox-with-label.js
import React, { useState } from 'react';
export default function CheckboxWithLabel(props) {
const [checkStatus, setCheckStatus] = useState(false);
const { labelOn, labelOff } = props;
function onChange() {
setCheckStatus(!checkStatus);
}
return (
<label>
<input
type="checkbox"
checked={checkStatus}
onChange={onChange}
/>
{checkStatus ? labelOn : labelOff}
</label>
);
}
复制代码
react 测试有多种方式,在 demo 中使用最好理解的 enzyme
// test/checkbox-with-label.test.js
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import CheckboxWithLabel from '../src/checkbox-with-label';
beforeAll(() => {
// enzyme 初始化
Enzyme.configure({ adapter: new Adapter() });
})
test('CheckboxWithLabel changes the text after click', () => {
// 渲染组件
const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
expect(checkbox.text()).toEqual('Off');
// 触发事件
checkbox.find('input').simulate('change');
expect(checkbox.text()).toEqual('On');
});
复制代码
jest 还提供了测试覆盖率的支持,执行命令 npm test -- --coverage
或者配置 package.json
"scripts": {
"test": "jest",
"coverage": "jest --coverage"
}
复制代码
执行命令 npm run coverage
便可
命令执行完成会在项目根目录添加 coverage
文件夹,使用浏览器打开 coverage/lcov-report/index.html
文件,有可视化的测试报告
项目完整代码:github.com/Samaritan89…