概念#
- 单元测试,只对某个功能点进行测试
- 集成测试,对单元以及单元里面包含的其它东西统一的测试,包括其它的依赖
中文官网:jest
umitest:umi-test 单元测试使用记录
所有示例为测试通过,目的举一反三
测试文件#
jest运行时会遍历项目中*.test.js文件,所以测试文件取名可以参照这种格式。
测试文件名举例:
index.test.js
mark.test.js
link.test.js
comments.test.js
在jest环境中文件内容默认只支持common.js书写规范,如需es6写法需要npm或yarn安装@bable/core @bable/preset-env 新增配置文件babel.config.json和配置
{
"presets": ["@babel/preset-env"]
}
基础函数#
test() 或 it()#
测试函数,每一项测试都需要一个test函数。it是简写
expect() 和 toBe()#
expect(预期)某项事物函数或者值和toBe(绝对匹配),和===作用相同
//用jest验证add函数中1+2=3
//原函数add
function add(a,b){
return a+b;
}
//jest验证
test('验证add函数',()=>{
expect(add(1,2)).toBe(3);
});
浮点运算请看下面toBeCloseTo()
toEqual()#
预期,正确值匹配
const a = {
name:"iqszlong",
};
const b = {
name:"iqszlong",
};
test('验证对象相同',()=>{
expect(a).toEqual(b); //a与b是相同的对象
});
toBeNull()、toBeUndefined()、toBeDefined()、toBeTruthy()、toBeFalsy()#
Null值匹配、undefined匹配、已定义匹配,真匹配、假匹配
const a = null;
const b = undefined;
const c = '';
const d = true;
const e = false;
test('验证null,undefined、已定义、真和假',()=>{
expect(a).toBeNull(); //a === null
expect(b).toBeUndefined(); //b === undefined
expect(c).toBeDefined(); //c !== undefined
expect(d).toBeTruthy(); //d === true
expect(e).toBeFalsy(); //e === false
});
toBeGreaterThan()、toBeGreaterThanOrEqual()、toBeLessThan()、toBeLessThanOrEqual()#
大于、大于等于、小于、小于等于
const a = 1;
const b = 2;
const c = 3;
test('验证大于、大于等于、小于、小于等于',()=>{
expect(b).toBeGreaterThan(a); //b > a
expect(a).toBeGreaterThanOrEqual(a);//a >= a
expect(a).toBeLessThan(c); //a < c
expect(b).toBeLessThanOrEqual(c); //b <= c
});
toBeCloseTo()#
接近,近似值
function add(a,b){
return a+b;
}
test('验证add函数',()=>{
expect(add(1.2,2.1)).toBeCloseTo(3.3); //js浮点计算误差 (1.2+2.1 != 3.3)
});
toMatch()#
检查字符串是否含有toMatch函数的参数,可使用正则表达式。
const a = '鲁迅说:我没说过';
test('验证是否包含字符串 鲁迅',()=>{
expect(a).toMatch(/鲁迅/); //a匹配到字符串‘鲁迅’
});
test('验证是否包含字符串 没说过',()=>{
expect(a).toMatch('没说过'); //a匹配到字符串‘没说过’
});
toContain()#
检查一个数组或可迭代对象是否包含某个特定项
const a = ['胡不笑','鲁迅','大禹'];
const b = new Set(a);
test('验证是否包含 鲁迅',()=>{
expect(a).toContain('鲁迅'); //a数组包含‘鲁迅’
});
test('验证是否包含 大禹',()=>{
expect(b).toContain('大禹'); //b包含‘大禹’
});
toThrow()#
抛出错误,验证函数是否抛出错误以及错误提示是否匹配。参数可使用正则表达式。
function hasError() {
throw new Error('错误');
}
test('验证抛出错误', () => {
expect(()=>hasError()).toThrow(); //函数是否抛出错误
expect(()=>hasError()).toThrow('错误'); //函数是否抛出相同的错误提示
expect(()=>hasError()).toThrow(/错/); // 错误提示是否匹配到 ‘错’
});
异步测试#
//等待时间函数
function sleep(delay) {
var start = (new Date()).getTime();
while ((new Date()).getTime() - start < delay) {
// 使用 continue 实现;
continue;
}
}
//主体函数
function main(fn) {
sleep(1000);//等待1秒
console.log('等待结束');
fn('getData');
}
//开始测试
test('异步测试', (done) => {
function get(res) {
try {
expect(res).toBe('getData');//验证等待1秒后得到的值
done();
} catch (error) {
done(error);
}
}
main(get);//调用main函数,传入get方法
});
异步请求#
function getPost() {
return fetch('https://v1.hitokoto.cn');
}
test('请求测试', async () => {
try {
const response = await getPost();
const data = await response.json();
expect(data.id).not.toBeNull();
} catch (error) {
expect(error.toString().indexof('404') > -1).toBeTruthy();
}
});
test('请求测试2', () => {
return getPost().then((response) => response.json()).then((data) => {
expect(data.id).not.toBeNull();
}).catch((e) => {
expect(e.toString().indexof('404') > -1).toBeTruthy();
});
});
强制执行数次#
增加expect断言,可以强制执行expect。
expect.assertions(2); //强制执行2次expect
test('请求测试2', () => {
return getPost().then((response) => response.json()).then((data) => {
expect(data.id).not.toBeNull();
}).catch((e) => {
expect(e.toString().indexof('404') > -1).toBeTruthy();
});
});
钩子#
beforeAll(() => {
// 在所有测试用例开始前需要做的工作
})
let counter
beforeEach(() => {
// 保证每次执行都创建新的 Counter 实例
// 避免不同测试用例之间产生影响 导致测试结果出现偏差
counter = new Counter()
})
afterEach(() => {
// 每个测试用例执行之后
})
afterAll(() => {
// 在所有测试用例结束之后
})
作用域(分组)#
describe可嵌套
describe('简单测试', () => {
function getPost() {
return fetch('https://v1.hitokoto.cn');
}
test('请求测试', async () => {
try {
const response = await getPost();
const data = await response.json();
expect(data.id).not.toBeNull();
} catch (error) {
expect(error.toString().indexof('404') > -1).toBeTruthy();
}
});
});
执行顺序:先执行外层的 beforeEach 再执行内层的 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
只运行当前测试#
const a = '鲁迅说:我没说过';
//只运行这个测试
test.only('验证是否包含字符串 鲁迅',()=>{
expect(a).toMatch(/鲁迅/); //a匹配到字符串‘鲁迅’
});
//该测试不会运行
test('验证是否包含字符串 没说过',()=>{
expect(a).toMatch('没说过'); //a匹配到字符串‘没说过’
});
Mock函数模拟#
describe('模拟函数基本', () => {
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
});
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
});
describe('简单模拟函数测试', () => {
//原函数add
function add(a, b) {
return a + b;
}
//模拟函数
const mockadd = jest.fn((a, b) => {
return add(a, b)
});
test('模拟测试', () => {
let result = mockadd(1, 2);
expect(mockadd).toBeCalled(); //模拟函数已被调用
expect(mockadd).toBeCalledTimes(1); //模拟函数被调用过1次
expect(mockadd).toHaveBeenCalledWith(1, 2); //模拟函数的参数为1,2
expect(result).toBe(3); //验证模拟函数结果
});
});
使用外部文件测试#
data.js
const mockFn = {
getData: async function (fn) {
const res = await this.getPost();
const data = await res.json();
return fn(data);
},
getPost: function () {
return fetch('https://v1.hitokoto.cn');
},
}
export default mockFn;
测试文件
describe('外部函数测试', () => {
test('代理测试', async () => {
expect.assertions(1);
let mockReq = jest.fn();
await mockFn.getData(mockReq);
expect(mockReq).toBeCalled();
});
});
模拟模块#
users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
测试文件
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
React组件测试#
首先定义一个组件,作用是输出一个按比例显示的盒子RatioBox。传入属性宽(width:16)和高(height:9)计算出比例值放在style中作为样式输出。注意:宽高不带单位只有数值
等比输出样式参照Bootstrap 3 的一个组件写法:响应式媒体内容
import React from 'react';
import Style from './style.css';
class RatioBox extends React.Component {
constructor(props) {
super(props);
this.state = {
ratio: (9 / 16) * 100,
};
}
static getDerivedStateFromProps(props, state) {
const data = state;
//渲染之前修改state值
if (props.width !== undefined && props.height !== undefined) {
data.ratio = (props.height / props.width) * 100;
return data;
}
return null;
}
shouldComponentUpdate(newProps, newState) {
return this.props !== newProps;
}
render() {
return (
<>
<div
className={`${Style.layout} ${this.props.className || ''}`}
style={{ paddingTop: `${this.state.ratio}%` }}
>
<div className={Style.inner}>{this.props.children}</div>
</div>
</>
);
}
}
export default RatioBox;
组件有了,现在来编写测试。
使用原始React测试#
React测试请参考官网:测试技巧-数据获取
由于测试环境需要act()来渲染,dom结构和属性都需要使用javaScript原生支持写法才能获取。
获取dom元素属性:MDN-WebAPI-attribut
选择dom元素:MDN-WebAPI-querySelector
import RatioBox from '..';
import renderer from 'react-test-renderer';
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// 退出时进行清理
unmountComponentAtNode(container);
container.remove();
container = null;
});
describe('React 渲染测试', () => {
test('渲染默认值', () => {
act(() => {
render(<RatioBox />, container);
});
const wrapper = container.querySelectorAll('div')[0]; //获取最外层div
const cssName = wrapper.attributes[0]; //获取第一个属性 className
const styleName = wrapper.attributes[1];//获取第二个属性style
expect(cssName.value).toMatch("layout");//验证 className 的值
expect(styleName.value).toMatch("56.25%");//验证计算后的 style 的值
});
});s
使用enzym测试#
组件测试需要包 enzym
yarn add enzyme enzyme-to-json enzyme-adapter-react-16 -D
在jest.setup.js中增加enzym配置,使jest使用enzym
const Enzyme = require('enzyme');
const { shallow, render, mount } = Enzyme;
const Adapter = require('enzyme-adapter-react-16');
Enzyme.configure({ adapter: new Adapter() });
global.shallow = shallow;
global.render = render;
global.mount = mount;
简单组件测试
import RatioBox from '..';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
describe('属性测试', () => {
test('className', () => {
const wrapper = shallow(<RatioBox className="test_css" width="10" height="5" />);
console.log(wrapper.debug());//调试html输出
const outerLayer = wrapper.find('.layout');
console.log(outerLayer.debug());//调试html输出
expect(outerLayer.prop('className')).toMatch('test_css');
expect(outerLayer.prop('style')).toEqual({paddingTop:"50%"});
});
});
- shallow()浅渲染器
- 官方介绍 shallow
enzym选择器,类似jQuery选择器,- 官方介绍 Selector
- render静态渲染器,生成html树和分析代码
- 官方介绍 render
- mount()挂载到dom中,多个内容会互相影响,需要
.unmount()进行清理。- 官方介绍 mount
生成测试覆盖率报告#
npx jest --coverage
umi#
umi test --coverage
测试报告会生成/coverage目录,/coverage/lcov-report/index.html就是报告文件。
package.json#
在文件package.json中的scripts下增加运行命令
"scripts": {
"coverage": "umi test --coverage",
// 或者
"coverage": "jest --coverage"
},
自动监听测试#
在package.json中的scripts`下增加运行命令
"scripts": {
"autotest": "jest --watchAll"
},
问题集锦#
fetch、Request测试报undefined#
参考github:fetch 用 jest 运行测试时未定义
在jest.config.js文件中增加代码:
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'],
};
再jest.setup.js文件中增加代码:
const {Response, Request, Headers, fetch} = require('whatwg-fetch');
global.Response = Response;
global.Request = Request;
global.Headers = Headers;
global.fetch = fetch;
注意:文件都在项目根目录中
jest.config.js文件可以在项目新建(umi下存在jest模块,新建文件即可被读取),也可以使用命令行自动创建。(自动创建会包含默认配置,影响umi已存在配置)
npx jest --init
-> jsdom //web环境用jsdom
-> coverage report //覆盖率报告,umi已有输入n,其他环境请自行考虑
-> Auto mock clear //自动清除mock临时数据,选n
-> 生成文件jest.config.js