Jest简介
Jest 是 Facebook 开源的一款 JS 单元测试框架,它也是 React 目前使用的单元测试框架。 目前除了 Facebook 外,Twitter、Nytimes、Airbnb 也在使用 Jest。Jest 除了基本的断言和 Mock 功能外,还有快照测试、实时监控模式、覆盖度报告等实用功能。 同时 Jest 几乎不需要做任何配置便可使用。
在使用create-react-app开发的前端项目中,脚手架已经为我们集成了Jest的相关配置,我们可以在react的项目中直接使用。
先让我们在一个react工程目录中src下新建一个__tests__文件夹,在里面写我们的各种测试案例。
首先,创建一个 sum.js文件:
function sum(a, b){
return a + b;
}
export { sum };
然后在__tests__中,新建一个sum.test.js的文件:
import { sum } from './sum.js';
test('测试求和函数', () => {
expect(sum(2, 2)).toBe(4);
});
最后运行yarn test 或 npm run test,将打印出下面的消息,表示测试通过了。
PASS __test__/sum.test.js
√ 测试求和函数 (7ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.8s
匹配器
Jest测试通过expect来实现,expect函数返回一个期望值对象,该对象提供了大量的工具方法来做结果判定。详细可见Jest官方文档,查看expect相关API。
判断相等:
注意:
toBe使用Object.is来测试精确相等。 如果您想要检查对象的值,请使用toEqual代替,它会递归判断对象的每一个字段。对数值来说,toBe和toEqual都可以使用。
test('2加2等于4', () => {
expect(2+2).toBe(4);
});
// 测试对象相等
test('测试对象的值', () => {
const data = {a: 1};
expect(data).toEqual({a: 1});
});
还可以测试相反的匹配:
test('2加2不等于1', () => {
expect(2 + 2).not.toBe(1);
});
判断真假:
在测试中,你有时需要区分
undefined、null,和false,但有时你又不需要区分。
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('2加2', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3); // 大于3
expect(value).toBeGreaterThanOrEqual(4); // 大于或等于4
expect(value).toBeLessThan(5); // 小于5
expect(value).toBeLessThanOrEqual(4.5); // 小于或等于4.5
});
判断符点数:
可使用
toBeCloseTo来解决 JS 浮点精度带来的问题
如下示例:
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
判断字符串: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/);
});
判断数组或可迭代的对象:toContain()
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
});
判断异常:
function compileAndroidCode() {
throw new Error('这是一个错误消息');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
// 可以匹配异常消息的内容,也可以用正则来匹配内容
expect(compileAndroidCode).toThrow('这是一个错误消息');
expect(compileAndroidCode).toThrow(/消息/);
});
异步测试
Jest 提供了三种方式来支持异步测试:回调函数,Promise 以及 async 函数。
回调函数:
默认情况下,Jest测试一旦执行到末尾,那这个测试就完成了。
// fetchData.js
import axios from 'axios';
export const fetchData = (fn) => {
axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
fn(res.data);
})
}
// fetchData.test.js 无效测试案例
import { fetchData } from '../fetchData.js';
test('fetchData返回的数据是 {success: true}', () => {
fetchData(data => {
expect(data).toEqual({success: true});
});
});
此时,该测试案例会成功通过,但这里有个问题。如果我将接口地址改成错误的,该测试依然会通过。因为一旦fetchData函数执行完毕,此测试就在没有调用回调函数前结束了,并不会执行到断言部分。
解决办法,使用单个done参数并调用done,jest会等done回调函数执行结束后,结束测试。如果done没有被调用,那Jest会认为该测试没有结束,最后超时失败。
// 有效测试
import { fetchData } from '../fetchData.js';
test('the data is peanut butter', done => {
fetchData(data => {
expect(data).toEqual({success: true});
done();
});
});
超时测试失败打印出的信息:
Promise:
此时让fetchData不去调用回调函数,而是返回一个Promise。
// fetchData.js
import axios from 'axios';
export const fetchData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json');
};
一定不要忘记把 promise 作为返回值,如果你忘了 return 语句的话,在 fetchData 返回的这个 promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了。
请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个rejected态的 Promise 不会让测试失败︰
test('fetchData返回的数据是 {success: true}', () => {
expect.assertions(1);
return fetchData().then(res => {
expect(res.data).toEqual({ success: true });
});
});
如果你想要 Promise 被拒绝,使用 .catch 方法。
请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个fulfilled态的 Promise 不会让测试失败︰
test('fetchData返回错误 404', () => {
expect.assertions(1);
return fetchData().catch(e => {
expect(e.toString()).toMatch('404');
});
});
.resolves / .rejects 匹配器也可以测试以上返回Promise的2种情况:
// 成功时
test('fetchData返回的数据是 {success: true}', () => {
return expect(fetchData()).resolves.toMacthObject({
data: { success: true },
});
});
// 失败时
test('fetchData抛出错误', () => {
return expect(fetchData()).rejects.toThrow();
});
Async/Await:可以替代return
// 成功时
test('fetchData返回的数据是 {success: true}', async () => {
await expect(fetchData()).resolves.toMacthObject({
data: { success: true },
});
});
// 失败时
test('fetchData抛出错误', async () => {
await expect(fetchData()).rejects.toThrow();
});
或者采用另一种方式:
// 成功时
test('fetchData返回的数据是 {success: true}', async () => {
const res = await fetchData();
expect(res.data).toEqual({ success: true });
});
// 失败时,需要try catch来捕获异常
test('fetchData返回错误 404', () => {
expect.assertions(1);
try{
await fetchData();
}catch(e){
expect(e.toString()).toMatch('404');
}
});
Jest钩子函数
钩子函数是指在某一时刻,jest会自动调用的函数。如下:
- beforeAll:所有测试执行前的函数
- afterAll:所有测试执行完成后的函数
- beforeEach:每一个测试执行前的函数
- afterEach:每一个测试执行完成后的函数
beforeAll(() => {
// 所有测试前需要执行的代码
...
})
钩子函数的作用域
默认情况下,before 和 after 的块可以应用到文件中的每个测试。
此外可以通过 describe 块来将测试分组。 当 before 和 after 的块在 describe 块内部时,则其只适用于该 describe 块内的测试。
注意,顶级的 beforeEach 在 describe 块级的 beforeEach 之前被执行。
describe('测试第一层', () => {
beforeAll(() => console.log('第一层beforeAll'));
beforeEach(() => console.log('第一层beforeEach'));
afterAll(() => console.log('第一层afterAll'));
afterEach(() => console.log('第一层afterEach'));
describe('测试第二层', () => {
beforeAll(() => console.log('第二层beforeAll'));
beforeEach(() => console.log('第二层beforeEach'));
afterAll(() => console.log('第二层afterAll'));
afterEach(() => console.log('第二层afterEach'));
test('第二层测试1', () => console.log('第二层测试1'));
test('第二层测试2', () => console.log('第二层测试2'));
});
});
打印出来的顺序:
如果在一个测试文件中,测试案例比较多,有时只想测试单个测试案例时,只需要在test后添加.only即可或者单独测试一个describe块,只需要在后面添加.only即可。这样的话,此时的测试文件中就只会执行这一个测试案例,其它的案例都是被跳过skipped。 相反,想要跳过测试案例的话,添加.skip即可。 如:
test.only('xxx', () => {...})或者describe.skip('xxx', ()=>{...})
describe和test块的执行顺序
Jest 会在所有真正的测试开始之前执行测试文件里所有的 describe 处理程序(handlers)。 这是在 before* 和 after* 处理程序里面 (而不是在 describe 块中)进行准备工作和整理工作的另一个原因。 当 describe 块运行完后,默认情况下,Jest 会按照 test 出现的顺序依次运行所有测试,等待每一个测试完成并整理好,然后才继续往下走。
describe('outer', () => {
console.log('describe outer-a');
describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => {
console.log('test for describe inner 1');
expect(true).toEqual(true);
});
});
console.log('describe outer-b');
test('test 1', () => {
console.log('test for describe outer');
expect(true).toEqual(true);
});
describe('describe inner 2', () => {
console.log('describe inner 2');
test('test for describe inner 2', () => {
console.log('test for describe inner 2');
expect(false).toEqual(false);
});
});
console.log('describe outer-c');
});
// 打印出的顺序:
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2
Mock函数
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数内部实现
jest.fn()
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toHaveBeenCalled();
// 断言mockFn被调用了一次
expect(mockFn).toHaveBeenCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
.mock属性:
所有的 mock 函数都有这个特殊的 .mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
};
};
// 测试文件
test('测试forEach函数', () => {
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 此 mock 函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
})
这些 mock 成员变量在测试中非常有用,用于说明这些mock function 是如何被调用、实例化或返回的情况。打印mock函数的mock属性得到:
Mock的返回值:
Mock 函数也可以用于在测试期间将测试值注入代码。如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
const myMock = jest.fn();
console.log(myMock());
// undefined
myMock
.mockReturnValueOnce(10) // 第一个调用返回的值为10
.mockReturnValueOnce('x') // 第一个调用返回的值为'x'
.mockReturnValue(true); // 返回的值为true
console.log(myMock(), myMock(), myMock(), myMock());
// 10, 'x', true, true
jest.mock()
通常情况下,我们需要调用api,发送ajax请求,从后台获取数据。但是我们在做前端测试的时候,并不需要去调用真实的接口,所以此时我们需要模拟axios模块,让它不必调用api也能测试我们的接口调用是否正确。
一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想让 axios.get('/demo.json')有个假的 response。
// api
export const fetchData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json');
}
// 测试
import axios from 'axios';
jest.mock('axios');
test('测试模拟axios模块', () => {
axios.get.mockResolvedValue({
data: 'haha',
});
return fetchData().then(res => {
expect(res.data).toBe('haha');
});
});
Mock的内部实现
方式一:
test('', () => {
const myMockFn = jest.fn(() => 22);
myMockFn();
expect(myMockFn.mock.result[0].value).toBe(22);
});
方式二:使用mockImplementation
test('', () => {
const myMockFn = jest.fn();
myMockFn.mockImplementation(() => 44);
myMockFn();
expect(myMockFn.mock.result[0].value).toBe(44);
})
// true
推荐VS CODE中扩展插件:Jest
在vs code扩展商店中搜索 jest,安装该插件。
安装好后,在通过的测试案例前会出现绿色的√,未通过的前面出现红色的×,并且在下方面板中看到未通过的测试案例的错误信息。
安装该插件后,可以不必在终端中输入yarn test 命令进行测试了。