写在前面
- 也许前端学了很久也不会接触到jest
- 比如我就是。。。
- 面对新事物,内心还是会很害怕的
- 今天,我终于鼓起勇气,学习了
jest- 希望通过学习,以后能在开源项目提一些单元测试的PR
- 然后,大家也要鼓起勇气,jest没有那么难
快速开始
安装
- 初始化
package.json,npm init - 安装
jest
# npm
npm install --save-dev jest
# yarn
yarn add --dev jest
完成首个jest测试
- 1.先写一个两数相加的函数,创建
sum.js文件:
function sum(a, b) {
return a + b;
}
module.exports = sum;
- 2.创建
sum.test.js测试文件- test函数第一个参数是测试名字
- test函数第二个参数是一个函数
- expect表示测试执行
- toBe表示结果应该是多少
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
- 3.在
package.json配置命令
{
"scripts": {
"test": "jest"
}
}
- 4.运行命令就完成了第一个测试
yarn test
# or
npm run test
匹配器的使用
- 我们在前面看到了测试用了
expect和toBe函数就完成了测试 - 但是这两个函数并不能覆盖所有的测试情况
- 下面我们认识一些其他的匹配器吧
- 匹配器很多,也只能介绍一些常用的
常用的匹配器
toBe一般用来精确匹配toEqual用来匹配对象
test('对象赋值', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
not表示不是
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
真值
- 代码中的
undefined,null,false有不同的含义,你可以使用真值判断来搞定它们 toBeNull只匹配nulltoBeUndefined只匹配undefinedtoBeDefined与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('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
字符串
- 可以检查是否具有正则表达式的字符串
expect('Christoph').toMatch(/stop/);
数组和可迭代对象
- 可以通过
toContain来检查一个数组或可迭代对象是否包含某个特定项
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
test('shoppingList数组中包含milk', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
错误抛出
- 如果想测试某函数在调用时是否抛出了错误,你需要使用
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);
// 你可以自己定义确切的错误消息内容或者使用正则表达式
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
});
- 注意:抛出错误的函数需要在expect的包装函数中调用,否则
toThrow断言总是会失败(不会捕获到前面的错误)。
测试异步代码
- 假设
fetchData()返回的是一个promise对象
Promise
- 为你的测试返回一个Promise,则Jest会等待Promise的resove状态 If the promise is rejected, the test will fail.
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Async/Await
- 您可以在测试中使用
async和await。 写异步测试用例时,可以在传递给test的函数前面加上async。
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和.rejects一起使用
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.toMatch('error');
});
安装和移除
- 写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些整理工作
Repeating Setup
- 比如我们要进行一些数据库的交互,在每个测试之前调用方法
initializeCityDatabase(),同时必须在每个测试后,调用方法clearCityDatabase()
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
一次性设置
- 同样的,我们可以一次性设置所有的,jest提供了对应的api-
beforeAll和afterAll
作用域
-
当然我们可能希望这个设置只对某几个测试有效
-
我们就可以使用作用域
describe
-
比如说,我们不仅有一个城市的数据库,还有一个食品数据库。 我们可以为不同的测试做不同的设置︰
// Applies to all tests in this file
beforeEach(() => {
return initializeCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
describe('matching cities to foods', () => {
// Applies only to tests in this describe block
beforeEach(() => {
return initializeFoodDatabase();
});
test('Vienna <3 veal', () => {
expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
});
test('San Juan <3 plantains', () => {
expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
});
});
模拟函数
- Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用
new实例化时捕获构造函数的实例、允许测试时配置返回值。
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);
// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
组件测试实战准备
- 其实要测试(比如react组件),其实主要分为两类吧
- 快照测试
- dom操作测试
- 下面就这两点,我们来详细看一看
快照测试
- 在测试 React 组件时可以采用类似的方法。您可以使用测试渲染器为您的 React 树快速生成可序列化的值,而不是渲染需要构建整个应用程序的图形 UI。
import renderer from 'react-test-renderer';
import Link from '../Link';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
属性匹配器
- Jest 允许为任何属性提供不对称匹配器。在写入或测试快照之前检查这些匹配器,然后将其保存到快照文件而不是接收到的值:
it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
// Snapshot
exports[`will check the matchers and pass 1`] = `
Object {
"createdAt": Any<Date>,
"id": Any<Number>,
"name": "LeBron James",
}
`;
DOM操作
- 另一类通常被认为难以测试的函数是直接操作 DOM 的代码。让我们看看我们如何测试以下 jQuery 代码片段,它监听点击事件,异步获取一些数据并设置 span 的内容。
displayUser.js
'use strict';
const $ = require('jquery');
const fetchCurrentUser = require('./fetchCurrentUser.js');
$('#button').click(() => {
fetchCurrentUser(user => {
const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out');
$('#username').text(user.fullName + ' - ' + loggedText);
});
});
displayUser-test.js
'use strict';
jest.mock('../fetchCurrentUser');
test('displays a user after a click', () => {
// Set up our document body
document.body.innerHTML =
'<div>' +
' <span id="username" />' +
' <button id="button" />' +
'</div>';
// This module has a side-effect
require('../displayUser');
const $ = require('jquery');
const fetchCurrentUser = require('../fetchCurrentUser');
// Tell the fetchCurrentUser mock function to automatically invoke
// its callback with some data
fetchCurrentUser.mockImplementation(cb => {
cb({
fullName: 'Johnny Cash',
loggedIn: true,
});
});
// Use jquery to emulate a click on our button
$('#button').click();
// Assert that the fetchCurrentUser function was called, and that the
// #username span's inner text was updated as we'd expect it to.
expect(fetchCurrentUser).toBeCalled();
expect($('#username').text()).toEqual('Johnny Cash - Logged In');
});