学习内容来源:Jest入门到TDD/BDD双实战_前端要学的测试课
相对原教程,我在学习开始时(2023.08)采用的是当前最新版本:
项 | 版本 |
---|---|
@babel/core | ^7.16.0 |
@pmmmwh/react-refresh-webpack-plugin | ^0.5.3 |
@svgr/webpack | ^5.5.0 |
@testing-library/jest-dom | ^5.17.0 |
@testing-library/react | ^13.4.0 |
@testing-library/user-event | ^13.5.0 |
babel-jest | ^27.4.2 |
babel-loader | ^8.2.3 |
babel-plugin-named-asset-import | ^0.3.8 |
babel-preset-react-app | ^10.0.1 |
bfj | ^7.0.2 |
browserslist | ^4.18.1 |
camelcase | ^6.2.1 |
case-sensitive-paths-webpack-plugin | ^2.4.0 |
css-loader | ^6.5.1 |
css-minimizer-webpack-plugin | ^3.2.0 |
dotenv | ^10.0.0 |
dotenv-expand | ^5.1.0 |
eslint | ^8.3.0 |
eslint-config-react-app | ^7.0.1 |
eslint-webpack-plugin | ^3.1.1 |
file-loader | ^6.2.0 |
fs-extra | ^10.0.0 |
html-webpack-plugin | ^5.5.0 |
identity-obj-proxy | ^3.0.0 |
jest | ^27.4.3 |
jest-enzyme | ^7.1.2 |
jest-resolve | ^27.4.2 |
jest-watch-typeahead | ^1.0.0 |
mini-css-extract-plugin | ^2.4.5 |
postcss | ^8.4.4 |
postcss-flexbugs-fixes | ^5.0.2 |
postcss-loader | ^6.2.1 |
postcss-normalize | ^10.0.1 |
postcss-preset-env | ^7.0.1 |
prompts | ^2.4.2 |
react | ^18.2.0 |
react-app-polyfill | ^3.0.0 |
react-dev-utils | ^12.0.1 |
react-dom | ^18.2.0 |
react-refresh | ^0.11.0 |
resolve | ^1.20.0 |
resolve-url-loader | ^4.0.0 |
sass-loader | ^12.3.0 |
semver | ^7.3.5 |
source-map-loader | ^3.0.0 |
style-loader | ^3.3.1 |
tailwindcss | ^3.0.2 |
terser-webpack-plugin | ^5.2.5 |
web-vitals | ^2.1.4 |
webpack | ^5.64.4 |
webpack-dev-server | ^4.6.0 |
webpack-manifest-plugin | ^4.0.2 |
workbox-webpack-plugin | ^6.4.1" |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、Jest 前端自动化测试框架基础入门
- 一、Jest 前端自动化测试框架基础入门(一)
- 一、Jest 前端自动化测试框架基础入门(二)
- 一、Jest 前端自动化测试框架基础入门(三)
10.Jest 中的 Mock
新建 lesson8.js
export const runCallback = callback => {
callback();
}
要测试 runCallback 一般思路都是创建一个函数,返回一个值,只要 expect 最终拿到这个值就说明没问题,但是 runCallback 的主要功能是执行 callback,若是和本示例这样没有将 callback 返回,岂不是测不了了。。。
因此最佳实践来了,如下。
(1)toBeCalled
新建 lesson8.test.js
import { runCallback } from "./lesson8";
test('测试 runCallback', () => {
const func = jest.fn();
runCallback(func);
expect(func).toBeCalled();
})
mock 一个函数,使用 toBeCalled 即可检测函数是否被调用
(2)func.mock
打印看一下 func.mock 里面有啥(console.log(func.mock)):
{
calls: [ [] ],
instances: [ undefined ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ]
}
- calls:func 每次执行的入参列表的列表
- instances:每次执行 func 时创建的实例的列表(即 函数中this的指向,默认 undefined)
- invocationCallOrder:每次 func 执行的顺序列表(例如:1, 2, 3 表示按顺序执行)
- results:每次执行 func 的返回值列表
编辑 lesson8.test.js(执行两次 func)
import { runCallback } from "./lesson8";
test('测试 runCallback', () => {
const func = jest.fn();
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
console.log(func.mock)
})
再看一下 func.mock 里面有啥:
{
calls: [ [], [] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results: [
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
}
可以看到每一次调用在 func.mock 中都是留有痕迹的
编辑 lesson8.test.js(给 func 传入一个函数,并执行三次)
test('测试 runCallback', () => {
const func = jest.fn(() => 123); // mock 函数,捕获函数的调用
// func.mockImplementation(() => 123); // 功能同上一句
// func.mockImplementationOnce(() => 123); // 只模拟一次
// func.mockImplementationOnce(() => 456); // 只模拟一次
// func.mockImplementation(() => this);
// func.mockReturnThis(); // 作用同上一句
runCallback(func);
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
// expect(func).toBeCalledWith(); // 断言每次调用的入参内容
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [] ],
instances: [ undefined, undefined, undefined ],
invocationCallOrder: [ 1, 2, 3 ],
results: [
{ type: 'return', value: 123 },
{ type: 'return', value: 123 },
{ type: 'return', value: 123 }
]
}
注意:带 Once 的要优先于不带的执行
(3)mockReturnValue & mockReturnValueOnce
编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce 来返回值)
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValueOnce('once')
runCallback(func);
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [] ],
instances: [ undefined, undefined, undefined ],
invocationCallOrder: [ 1, 2, 3 ],
results: [
{ type: 'return', value: 'once' },
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
}
可以看到 'once'
只返回了一次
编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce, mockReturnValueOnce 链式调用,mockReturnValue 三种方式来返回值)
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValueOnce('1');
func.mockReturnValueOnce('2');
func.mockReturnValueOnce('3').mockReturnValueOnce('4').mockReturnValueOnce('5');
func.mockReturnValue('6'); // 后续每次返回同样的值
[...new Array(8)].map(() => runCallback(func))
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [
[], [], [], [],
[], [], [], []
],
instances: [
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined, undefined
],
invocationCallOrder: [
1, 2, 3, 4,
5, 6, 7, 8
],
results: [
{ type: 'return', value: '1' },
{ type: 'return', value: '2' },
{ type: 'return', value: '3' },
{ type: 'return', value: '4' },
{ type: 'return', value: '5' },
{ type: 'return', value: '6' },
{ type: 'return', value: '6' },
{ type: 'return', value: '6' }
]
}
接下来通过示例理解一下 mock 里的 instances
编辑 lesson8.js(callback 运行一次,之后创建一个实例,并赋予一个属性作为标识)
export const runCallback = (callback, index) => {
callback();
let obj = new callback()
obj.name = 'callback_' + index
}
编辑 lesson8.test.js
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValue('6'); // 后续每次返回同样的值
[...new Array(3)].map((item, index) => runCallback(func, index))
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [], [], [], [] ],
instances: [
undefined,
mockConstructor { name: 'callback_0' },
undefined,
mockConstructor { name: 'callback_1' },
undefined,
mockConstructor { name: 'callback_2' }
],
invocationCallOrder: [ 1, 2, 3, 4, 5, 6 ],
results: [
{ type: 'return', value: '6' },
{ type: 'return', value: undefined },
{ type: 'return', value: '6' },
{ type: 'return', value: undefined },
{ type: 'return', value: '6' },
{ type: 'return', value: undefined }
]
}
可见每次运行默认不会有实例化对象,因此是 undefined,但是在实例化后,就会有一个命名为 mockConstructor 的构造对象
普通函数可以通过 jest.fn() 的方式来 mock,为保证当前部分功能的独立性,那接口请求也是需要 mock 的(避免网络和后端的影响)
编辑 lesson8.js(新增 getData 请求接口)
export const getData = () => {
return axios.get('/api').then(res => res.data)
}
编辑 lesson8.test.js
import { runCallback, getData } from "./lesson8";
import axios from 'axios'
jest.mock('axios');
...
test.only('测试 getData', async () => {
axios.get.mockResolvedValue({data: 'hello'})
await getData().then(data => {
expect(data).toBe('hello')
})
})
注意:jest.mock 必须放在文件的上面紧贴着 import,且外面不能嵌套任何内容否则对其mock的内容的作用域有影响(与被mock内容保持一致,且加载顺序挨着)
总结一下 mock 的几大功能:
- 捕获函数的调用和返回结果,以及 this 和调用顺序
- 可以自由设置返回结果
- 改变函数的内部实现
注意,从25版本开始,官方文档的结构发生了一些变化,最后一个变化前的版本:
- https://archive.jestjs.io/docs/en/24.x/api
本文仅作记录, 实战要点待后续专文总结,敬请期待。。。