通俗的讲,在前端,单元可以理解为一个独立的模块文件,单元测试就是对这样一个模块文件的测试。
1、更快的发现BUG
2、减少BUG的产出
3、有利于项目维护、升级、重构
4、方便调试
https://github.com/avajs/ava-docs/blob/main/zh_CN/readme.md
https://zhaoqize.github.io/puppeteer-api-zh_CN/
创建工程
mkdir jest-start
cd jest-start
初始化
yarn init || npm init
安装依赖
yarn add --dev jest || npm install --save-dev jest
初始化Jest默认配置
npx jest --init
安装babel
yarn add babel-jest @babel/core @babel/preset-env -D
配置.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
根目录创建src/sum.js
function sum(a, b) {
return a + b
}
module.exports = sum
根目录创建tests/sum.test.js
const sum = require('../src/sum')
// 期望sum(1, 2)执行后结果为3
test('add 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
运行yarn test
或npm run test
(1)修改jest.config.js
中的coverageDirectory
的值为coverage
(自定义)
(2)终端运行npx jest --coverage
,终端输出代码覆盖率报告
(3)项目根目录自动生成coverage
文件夹,进入\coverage\lcov-report
, 打开index.html
可以看到对应的代码覆盖率报告
// 精确匹配 判断基本类型数据 ===
test('2 + 2 等于 4', () => {
expect(2+2).toBe(4)
})
// 判断引用类型 ==
test('对象赋值', () => {
const data = {one: 1}
data['two'] = 2
// expect(data).toBe({one: 1, two: 2}) //测试不匹配u
expect(data).toEqual({one: 1, two: 2})
})
// not就是对matcher的否定
test('not修饰符', () => {
const a = 0
expect(a).not.toBe(1)
})
toBeNull
只匹配 null
toBeUndefined
只匹配 undefined
toBeDefined
与toBeUndefined
相反
toBeTruthy
匹配任何if
语句为真
toBeFalsy
匹配任何 if
语句为假
test('变量a是否为null', () => {
const a = null
expect(a).toBeNull()
})
test('变量a是否为undefined', () => {
const a = undefined
expect(a).toBeUndefined()
})
test('变量a是否为defined', () => {
const a = null
expect(a).toBeDefined()
})
test('变量a是否为true', () => {
const a = 1
expect(a).toBeTruthy()
})
test('变量a是否为false', () => {
const a = 0
expect(a).toBeFalsy()
})
test('two plus two', () => {
const value = 2 + 2;
// 判断数value是否大于某个数
expect(value).toBeGreaterThan(3);
// 判断数value是否大于等于某个数
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeGreaterThanOrEqual(4);
// 判断数value是否小于某个数
expect(value).toBeLessThan(5);
// 判断数value是否小于等于某个数
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeLessThanOrEqual(4);
// toBe 和 toEqual 对数值的判断是等效的
expect(value).toBe(4);
expect(value).toEqual(4);
});
// 测试浮点数使用toBeCloseTo
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',
'milk',
];
test('the shopping list has milk on it', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
// 判断数组的长度
test('toHaveLength', () => {
expect(shoppingList).toHaveLength(5)
})
// 判断抛出的异常是否符合预期
function throwError() {
throw new Error('I throw a error')
}
test('toThrow', () => {
expect(() => throwError()).toThrow(/I throw a error/)
})
export default (fn) => {
setTimeout(() => {
fn()
console.log('1111111');
}, 2000)
}
import timeout from '../src/timeout'
test('测试定时器', () => {
timeout(() => {
expect(1+1).toBe(2)
})
})
// ----------------------------------
test('测试定时器', (done) => {
timeout(() => {
expect(1+1).toBe(2)
done()
})
})
fakeTimers
模拟真实的定时器。这个fakeTimers
在遇到定时器时,允许我们立即跳过定时器等待时间,执行内部逻辑
因此,上面的例子可以修改成为
//首先,我们使用jest.fn()生成一个jest提供的用来测试的函数,这样我们之后回调函数不需要自己去写一个
//其次,我们使用jest.useFakeTimers()方法启动fakeTimers
//最后,我们可以通过jest.advanceTimersByTime()方法,参数传入毫秒时间,jest会立即跳过这个时间值,还可以通过toHaveBeenCalledTimes()这个mathcer来测试函数的调用次数。
test('测试timer', () => {
jest.useFakeTimers()
// 使用jest.fn()生成测试函数
const fn = jest.fn()
timeout(fn)
// 时间快进2秒
jest.advanceTimersByTime(2000)
expect(fn).toHaveBeenCalledTimes(1)
})
可以通过调用jest.runAllTimes()
执行所有的定时器
待测试文件timeoutNest.js
export default (fn) => {
setTimeout(() => {
fn()
console.log('this is timeout outside!')
setTimeout(() => {
fn()
console.log('this is timeout inside!')
}, 3000)
}, 2000)
}
测试文件timeoutNest.test.js
test('测试timer', () => {
jest.useFakeTimers()
const fn = jest.fn()
timeoutNest(fn)
jest.runAllTimers()
expect(fn).toHaveBeenCalledTimes(2)
})
使用传统的Promise
获取数据,可以在测试中返回一个Promise
,Jest
会等待Promise
中的resolve
,如果 Promise 被拒绝,则测试将自动失败。
request.js
import axios from 'axios'
export const request = () => {
return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
request.test.js
import { request } from '../src/request'
test('测试request', () => {
return request().then(data => {
expect(data.data).toEqual({
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
})
})
})
可以在测试中使用 async
和 await
。 写异步测试用例时,可以在传递给test
的函数前面加上async
requestAsyncAwait.test.js
import { request } from '../src/request'
// 写法一
test('测试request', async () => {
const res = await request()
expect(res.data).toEqual({
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
})
})
// 写法二 将 async and await和 .resolves or .rejects一起使用。
test('测试request', async () => {
await expect(request()).resolves.toMatchObject({
data: {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
})
})
在日常项目中,我们请求接口有时会报错,这时候需要对这种接口请求做错误处理,同样,也需要对异常情况编写测试代码。
在request.js
新增一个方法,该方法请求一个不存在的接口地址,因此会返回404
export const requestErr = fn => {
return axios.get('https://jsonplaceholder.typicode.com/sda')
}
request.test.js
test('测试request 404', () => {
return expect(requestErr()).rejects.toThrow(/404/)
})
或者可以用async/await
test('测试request 404', async () => {
await expect(requestErr()).rejects.toThrow(/404/)
})
// 或者可以使用try catch语句写的更完整
test('测试request 404', async () => {
try {
await requestErr()
} catch (e) {
expect(e.toString()).toBe('Error: Request failed with status code 404')
}
})
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
jest.fn()
是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()
会返回undefined
作为返回值。
// jestFn.test.js
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
jest.fn()
所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise
对象。
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
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]");
})
很多时候,我们在前端开发过程中,后端接口还没有提供,我们需要去mock
接口返回的数据。
首先在mock.js
中编写一个简单的请求数据的代码:
import axios from 'axios'
export const request = fn => {
return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
在mock.test.js
中,使用jest.mock()
方法模拟axios
,使用mockResolvedValue
和mockResolvedValueOnce
方法模拟返回的数据,同样的,mockResolvedValueOnce
方法只会改变一次返回的数据:
import axios from 'axios'
import { request } from '../src/mock'
jest.mock('axios')
test('测试request', async () => {
axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' })
axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' })
await request().then((res) => {
expect(res.data).toBe('Jordan')
})
await request().then((res) => {
expect(res.data).toBe('kobe')
})
})
所谓分组测试,核心在于,将不同的测试进行分组,再结合勾子函数(生命周期函数),完成不同分组的定制化测试,以满足测试过程重的复杂需求。
hook.js
export default class Count {
constructor () {
this.count = 2
}
increase () {
this.count ++
}
decrease () {
this.count --
}
double () {
this.count *= this.count
}
half () {
this.count /= this.count
}
}
我们想要对Count
类的四个方法单独测试,数据互相不影响,当然我们可以自己去直接实例化4
个对象,不过,jest
给了我们更优雅的写法---分组,我们使用describe
函数分组,如下:
describe('分别测试Count的4个方法', () => {
test('测试increase', () => {
})
test('测试decrease', () => {
})
test('测试double', () => {
})
test('测试half', () => {
})
})
这样我们就使用describe
函数配合test
将测试分为了四组,接下来,为了能更好的控制每个test
组,我们就要用到jest
的勾子函数。 我们这里要介绍的是jest
里的四个勾子函数:
beforeEach
:是在每一个test函数执行之前,会被调用
beforeAll
:是在所有test函数执行之前调用
afterEach
:每一个test函数执行之后调用
afterAll
:所有test函数执行之后调用
hook.test.js
import Count from "../src/hook"
describe('分别测试Count的4个方法', () => {
let count
beforeAll(() => {
console.log('before all tests!')
})
beforeEach(() => {
console.log('before each test!')
// 每个test执行之前,beforeEach里面重新实例化了count
count = new Count()
})
afterAll(() => {
console.log('after all tests!')
})
afterEach(() => {
console.log('after each test!')
})
test('测试increase', () => {
count.increase()
console.log(count.count)
})
test('测试decrease', () => {
count.decrease()
console.log(count.count)
})
test('测试double', () => {
count.double()
console.log(count.count)
})
test('测试half', () => {
count.half()
console.log(count.count)
})
})