前端测试框架—jest基本使用
前言
最近想着做一个UI组件库,然后发现做一个UI组件库所需要的技术还是比较多的,比如前端测试,自动化部署,打包构建等等。所以我首先定他一个小目标,先从前端测试开始学起,然后我在网上找了一些关于前端测试的视频和资料,学了三个星期算是入门了,所以打算整理一下我在学习过程中记录下来的笔记,方便日后查看,我这里学习的测试框架主要是jest
jest
常见匹配器
基本类型
- toBe :引用值相等
- toEqual :内容相等
- toBeNull :为空
- toBeUndefined :undefined
- toBeDefined :已定义
boolean
- toBeTruthy :为真
- toBeFalsy :为假
Number
- toBeGreaterThan :大于
- toBeLessThan :小于
- toBeGreaterThanOrEqual :大于等于
- toBeCloseTo :小数点运算
String
- toMatch :字符串匹配
Array,Set
- toContain :包含
异常
- toThrow : 会抛出异常
取反
- not :相反
命令行工具使用
- f模式 :只运行失败的测试用例
- o模式 :只运行被修改文件的测试用例(该模式必须结合git仓库才能使用,–watch)
- a模式 :重新运行所有测试用例(–watchAll)
- t模式 :只运行匹配的测试用例
- p模式 :只运行匹配的文件名下的所有测试用例
异步代码测试方法
- 回调类型异步函数
// fetch.js
import axios from 'axios'
const url = 'http://xxx/demo.json';
export function fetchData(fn) {
axios.get(url).then(res=>{
fn(res.data)
})
}
// fetch.test.js
import {fetchData} from "./fetch";
test('回调类型异步函数测试',(done)=>{
fetchData((data)=>{
expect(data).toEqual({success:true});
done();
})
});
- promise异步函数测试
// fetch.js
import axios from 'axios'
const url = 'http://xxx/demo.json';
export function fetchData() {
return axios.get(url)
}
// fetch.test.js
/**
* 注意2点
* 1、test的第二个函数不能写done参数
* 2、必须return
*/
test('promise异步函数测试',()=>{
return fetchData().then(res=>{
expect(res.data).toEqual({success:true})
})
});
test('promise异步函数测试,并且返回404', () => {
return expect(fetchData()).rejects.toThrow();
});
//=========================
test('promise异步函数测试', () => {
return expect(fetchData()).resolves.toMatchObject({
data: {success: true}
})
});
test('promise异步函数测试,并且返回404',()=>{
//下面expect必须被执行1次测试才能通过
expect.assertions(1);
return fetchData().catch(err=>{
expect(err.toString().indexOf('404')>-1).toBe(true)
})
});
//============================
test('promise异步函数测试', async () => {
await expect(fetchData()).resolves.toMatchObject({
data: {success: true}
})
});
test('promise异步函数测试,并且返回404', async () => {
await expect(fetchData()).rejects.toThrow();
});
钩子函数
- beforeAll :所有测试用例运行前执行
- afterAll :所有测试用例运行之后执行
- beforeEach :每个测试用例运行前执行
- afterEach :每个测试用例运行之后执行
分组(钩子函数从外往内执行)
describe('分组测试', () => {
beforeAll(() => {
console.log('beforeAll')
});
afterAll(() => {
console.log('AfterAll')
});
beforeEach(() => {
console.log('beforeEach')
});
afterEach(() => {
console.log('afterEach')
});
test('promise异步函数测试', async () => {
await expect(fetchData()).resolves.toMatchObject({
data: {success: true}
})
});
});
mock
1、捕获函数的调用和返回结果,以及this和调用顺序
2、自由设置函数返回结果
3、改变函数内部的实现
// mock.js
export function runCallback(fn) {
fn('abc')
}
export function getData() {
return axios.get('/api').then(res=>res.data);
}
// mock.test.js
import {runCallback,getData} from "./fetch";
// 1、捕获函数的调用和返回结果,以及this和调用顺序
// 2、自由设置函数返回结果
test('测试 runCallback',()=>{
// 以下三种方式都是一样,设置函数返回结果
// const fn = jest.fn(()=>{
// return 'hello'
// });
// fn.mockImplementation(()=>'hello');
// fn.mockReturnValueOnce(()=>'hello');
// fn.mockReturnValueOnce('hello'); // 返回值只返回一次,也就是第一次执行的时候有返回值,后面的没有
const fn = jest.fn(); // mock函数,1、捕获函数的调用和返回结果,以及this和调用顺序
fn.mockReturnValue('hello'); // 自由设置函数返回结果
runCallback(fn);
runCallback(fn);
expect(fn.mock.calls.length).toBe(2);
expect(fn.mock.results[0].value).toBe('hello');
expect(fn).toBeCalled();
expect(fn).toBeCalledWith('abc'); // 函数每次被调用的时候都传入abc这个参数
console.log(fn.mock);
// {
// calls: [ ['abc'], ['abc'] ], //函数被调用次数,以及每次被调用时的参数
// instances: [ undefined, undefined ], // 函数this指向
// invocationCallOrder: [ 1, 2 ],
// results: [ //每次执行的返回值
// { type: 'return', value: 'hello' },
// { type: 'return', value: 'hello' }
// ]
// }
});
// ====================
// 3、改变函数内部的实现
import axios from 'axios';
jest.mock('axios');
test.only('测试 改变函数内部的实现',async ()=>{
axios.get.mockResolvedValue({data:'hello'});
await getData().then(res=>{
expect(res).toBe('hello')
})
});
mock进阶
// fetch.js
import axios from 'axios';
export function getData() {
return axios.get('/').then(res=>res.data)
}
export function getNumber() {
return 123
}
// __mocks__/fetch.js
export function getData() {
return Promise.resolve("(function a(){return 123})()")
}
// fetch.test.js
/**
* 方法一:
* 1、在项目根目录新建__mocks__文件夹
* 2、在__mocks__文件夹下面新建一个跟需要mock的文件同名的文件,并重写需要进行测试的方法
* 3、jest在测试的时候会去寻找__mocks__下面的fetch.js,而不是真实引入的fetch.js
*
* 方法二:
* 在jest.config.js文件中开启 automock: true
*/
jest.mock('./fetch');
import {getData} from "./fetch";
// getNumber这个函数不需要mock
const {getNumber} = jest.requireActual('./fetch.js');
test('getData',()=>{
return getData().then(data=>{
expect(eval(data)).toEqual(123)
})
});
test('getNumber',()=>{
expect(getNumber()).toEqual(123)
});
snapshot 快照测试
- 一般用于测试配置文件
- 第一次测试的时候没有快照文件会先生成快照文件,第二次测试会用新生成的快照文件与旧的快照文件对比
- u模式可以更新全部快照文件
- i模式会逐个快照进行询问是否需要更新
// snapshot.js
export function getConfing() {
return {
name:'张三',
age:12,
sex:'nan1'
}
}
export function getOtherConfing() {
return {
name:'张三',
age:12,
sex:'nan1',
time:new Date()
}
}
// snapshot.test.js
import { getConfing, getOtherConfing } from "./fetch";
test("测试快照,toMatchSnapshot", () => {
expect(getConfing()).toMatchSnapshot();
});
test("测试快照,toMatchSnapshot", () => {
// toMatchInlineSnapshot(必须安装prettier) 把快照内容放到测试文件中,而不是生成快照文件
expect(getOtherConfing()).toMatchInlineSnapshot(
{
time: expect.any(Date), // 指定time为Date类型,不然每次生成的Date实例都不一样,导致测试不通过
},
`
Object {
"age": 12,
"name": "张三",
"sex": "nan1",
"time": Any<Date>,
}
`
);
});
mock – timers定时器
// timer.js
export function timer(fn) {
setTimeout(fn,3000)
}
export function timer1(fn) {
setTimeout(()=>{
fn();
setTimeout(fn,3000)
},3000)
}
// timer.test.js
import {timer,timer1} from "./fetch";
// mock定时器
jest.useFakeTimers(); // 最好在beforeEach钩子函数中调用,不然可能会导致在测试用例中相互影响
test('timer--runAllTimers',()=>{
const fn = jest.fn();
timer(fn);
jest.runAllTimers();
// 期望函数被执行一次
expect(fn).toHaveBeenCalledTimes(1);
});
test('timer1--runOnlyPendingTimers',()=>{
const fn = jest.fn();
timer1(fn);
// 只执行处于队列中的定时器
jest.runOnlyPendingTimers();
expect(fn).toHaveBeenCalledTimes(1);
jest.runOnlyPendingTimers();
expect(fn).toHaveBeenCalledTimes(2);
});
test('timer1--advanceTimersByTime',()=>{
const fn = jest.fn();
timer1(fn);
// 将定时器时间快进6秒
jest.advanceTimersByTime(6000);
expect(fn).toHaveBeenCalledTimes(2);
});
ES6 中类的测试
// Util.js
class Util {
a(){}
b(){}
}
export default Util;
// demoUtil.js
import Util from "./util";
export function demoUtil() {
const util = new Util();
util.a();
util.b();
}
// demoUtil.test.js
// 集成测试:把单元测试中的其他东西也进行测试了
// jest发现util导出的是一个类,会自动进行mock(或者可以手动mock)
// Util=jest.fn(),Util.prototype.a=jest.fn(),Util.prototype.b=jest.fn()
jest.mock('./util');
import {demoUtil} from './fetch';
import Util from './util'
test('测试',()=>{
demoUtil();
// Util是单元测试中的其他东西
expect(Util).toHaveBeenCalled();
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
});
总结
其实前端测试还是比较简单的,难点主要是在于测试用例的编写,你要去考虑不同的情况产生的效果。学完这个jest测试框架,我觉得自己最大的收获并不是学会了这个测试框架怎么使用,而是思维和思考上的收获,因为在编写测试用例的时候我需要思考每个数据传入空会产生什么效果,传入null或者undefined又会产生什么效果,我需要考虑到的边界情况非常多,通过思考这些问题,我在编写代码的时候就需要判断这些边界情况,从而提高代码质量。好了,这篇文章就写到这里吧,下一阶段的学习目标就是持续集成和持续部署(简称CICD)