单元测试的意义
- 大规模代码重构时,能保证重构的正确性
- 保证代码的质量,验证功能完整性
单元测试分类
- TDD - (测试驱动开发)侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码
- BDD - (行为驱动开发) 由外到内的开发方式,从外部定义业务成果,再深入到能实现这些成果,每个成果会转化成为相应的包含验收标准
简单来说就是TDD先写测试模块,再写主功能代码,然后能让测试模块通过测试,而BDD是先写主功能模块,再 写测试模块
主流的前端测试框架
- **Karma **- 基于Node.js的JavaScript测试执行过程管理工具(Test Runner),让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行
- Mocha - Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试( Mocha+chai )
- **jest **-Jest 是 Facebook 开发的一款 JavaScript 测试框架。在 Facebook 内部广泛用来测试各种 JavaScript 代码
Jest简介
Jest是一个Javascript测试框架,由Facebook开源,致力于简化测试,降低前端测试成本,已被
create-react-app、@vue/cli等脚手架工具默认集成。Jest主打开箱即用、快照功能、独立并行测试以及良好的文档和Api.
Jest使用介绍
安装
// 初始化一个项目
mkdir jest-test && cd jest-test
npm init -y
npm install -D jest
//#在package.json的scripts中添加
"scripts": {
"test": "jest"
}
利用babel将代码转译为es5
npm install -D babel-jest @babel/core @babel/preset-env
编辑babel配置文件 .babelrc
{
"presets": [
[
"@babel/env",
{
"targets": {
"node": "current"
}
}
]
]
}
编写测试
// 新建一个目标文件 index.js
export function sum(a, b) {
return a + b;
}
//新建测试文件 index.test.js
import {sum} from './target'
test('测试sum函数 1 加 2 等于 3',()=>{
expect(sum(1,2)).toBe(3)
})
运行 npm run test
PASS ./index.spec.js
✓ 测试sum方法 1 加 2 等于 3 (3 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.544 s
Ran all test suites.
主要功能介绍
1、Matchers 匹配器
普通匹配器
// toBe 测试是否精确匹配
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
// toEqual检查对象的值
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
Truthiness
toBeNull只匹配nulltoBeUndefined只匹配undefinedtoBeDefined与toBeUndefined相反toBeTruthy匹配任何if语句为真toBeFalsy匹配任何if语句为假
数字
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);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual
字符串
可以检查对具有 toMatch 正则表达式的字符串
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
Arrays and iterables
通过 toContain来检查一个数组或可迭代对象是否包含某个特定项
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
expect(new Set(shoppingList)).toContain('beer');
});
异常
想要测试的特定函数抛出一个错误,在它调用时,使用 toThrow
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(Error);
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
expect(compileAndroidCode).toThrow(/JDK/);
});
2、测试异步代码
一、如果异步模式是回调函数
比如fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 你期望返回的数据是一个字符串 good
使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
test('the data is good', done => {
function callback(data) {
try {
expect(data).toBe('good');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
二、Promises
fetchData 不使用回调函数,而是返回一个 Promise,其resolve值为字符串 good
test('the data is good', () => {
return fetchData().then(data => {
expect(data).toBe('good');
});
});
一定不要忘记把 promise 作为返回值,如果你忘了
return语句的话,在fetchData返回的这个 promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了
如果fetchData() 返回额 Promise是rejected的,请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个fulfilled态的 Promise 不会让测试失败
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
三、.resolves / .rejects
可以在 expect 语句中使用 .resolves 匹配器,Jest 将等待此 Promise 解决。 如果承诺被拒绝,则测试将自动失败。
test('the data is good', () => {
return expect(fetchData()).resolves.toBe('good');
});
一定不要忘记把整个断言作为返回值返回⸺如果你忘了
return语句的话,在fetchData返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了。
如果 Promise 被拒绝,则使用.resolves
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
四、Async / Await
可以在测试中使用 async 和 await
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
可以用 async 和 await 联合.resolves or .reject一起使用
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toThrow('error');
});
3、测试hooks
Jest提供了`beforeEach`、`afterEach`、`beforeAll`、`afterAll`等钩子,主要用于一些测试之前等状态预设和测试完成后等重置、清理工作. 其中`beforeEach`和`afterEach`会分别在每个单元测试的运行前和运行结束后执行,`beforeAll`和`afterAll`则是在所有单元测试的执行前和执行完成后运行。
此外可以通过 `describe` 块来将测试分组。 当 `before` 和 `after` 的块在 `describe` 块内部时,则其只适用于该 `describe` 块内的测试。
顶级的 `beforeEach` 在 `describe` 块级的 `beforeEach` 之前被执行
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
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'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
4、Mock 函数
Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 `new` 实例化时捕获构造函数的实例、允许测试时配置返回值。
Jest 中有三个与 Mock函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()。
mock函数主要提供了三个功能用于测试
- 调用捕获
- 返回值设定
- 改变函数实现
jest.fn 是最简单的实现一个mock函数的方法
test("mock test", () => {
const mock = jest.fn(()=> 'jest.fn test');
expect(mock()).toBe('jest.fn test'); //函数返回结果
expect(mock).toHaveBeenCalled(); //函数被调用
expect(mock).toHaveBeenCalledTimes(1); //调用1次
});
test("mock 返回值", () => {
const mock = jest.fn();
mock.mockReturnValue("return value"); //mock 返回值
expect(mock()).toBe("return value");
});
test("mock promise", () => {
const mock = jest.fn();
mock.mockResolvedValue("promise resolve"); // mock promise
expect(mock("promise")).resolves.toBe("promise resolve");
expect(mock).toHaveBeenCalledWith("promise"); // 调用参数检验
});
//或者使用赋值的形式
function add(v1,v2){
return v1 + v2
}
add = jest.fn()
test("mock dependency", () => {
let result = add(1,2);
expect(reslut).toBeDefined();
expect(add).toHaveBeenCalledWith(1,2)
});
**jest.mock mock整个module, **擦除函数的实际实现
//mod.js
export function modTest(v1, v2) {
return v1 * v2
}
//index.test.js
import {modTest} from './mod'
jest.mock('./mod')
test('jest.mock test', () => {
let result = modTest(1,2)
expect(result).toBe(undefined);
expect(modTest).toHaveBeenCalledTimes(1);
expect(modTest).toHaveBeenCalledWith(1,2)
})
jest.spyOn
jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。
import * as mod from './mod'
test('jest.spyOn test', () => {
const modMock = jest.spyOn(mod,'modTest')
expect(mod.modTest(1, 2)).toBe(2);
// and the spy stores the calls to add
expect(modMock).toHaveBeenCalledWith(1, 2);
})
5、Mock 定时器
原生的定时器函数(如:setTimeout, setInterval, clearTimeout, clearInterval)并不是很方便测试,因为程序需要等待相应的延时。
我们通过jest.useFakeTimers();来模拟定时器函数
// timerGame.js
'use strict';
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
// __tests__/timerGame-test.js
'use strict';
jest.useFakeTimers();
test('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
使用 jest.runAllTimers(), 用于在测试中将时间“快进”到正确的时间点
test('calls the callback after 1 second', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// 在这个时间点,定时器的回调不应该被执行
expect(callback).not.toBeCalled();
// “快进”时间使得所有定时器回调被执行
jest.runAllTimers();
// 现在回调函数应该被调用了!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
VS Code编辑器中调试测试用例
可以在VS Code中安装Jest插件,对编写的测试用例调试,具体使用参考Jest插件使用