持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用方法的执行过程和结果,只想知道它是否被正确调用;再有,我们经常会依赖于外部接口获取数据,但测试时我们并不需要发送真实的网络请求。此时,使用Mock函数是十分有必要的。
Mock 函数可以轻松地测试代码之间的连接——这通过擦除函数的实际实现,捕获对函数的调用 ( 以及在这些调用中传递的参数) ,在使用 new 实例化时捕获构造函数的实例,或允许测试时配置返回值的形式来实现。Jest中有两种方式的Mock Function,一种是利用Jest提供的Mock Function创建,另外一种是手动创建来覆写本身的依赖实现。
模拟函数
假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一个回调函数,代码如下:
// index.js
export const forEach = (items, callback) => {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
测试这个函数时,我们不关心callback的内部逻辑,只想知道在forEach函数执行时它有没有被调用,所以在编写测试用例时,我们可以使用一个mock函数来代替callback,然后通过检查 mock 函数来确保回调函数是否如期调用。
// index.spec.js
import { forEach } from './index.js'
test('模拟函数', () => {
const mockCallback = jest.fn()
forEach([0, 1], mockCallback)
expect(mockCallback.mock.calls.length).toBe(2) // 此模拟函数被调用了两次
expect(mockCallback.mock.calls[0][0]).toBe(0) // 第一次调用时,第一个参数是 0
expect(mockCallback.mock.calls[1][0]).toBe(1) // 第二次调用时,第一个参数是 1
})
-
这里
jest.fn()就是用来Mock函数的,下面我们再展开 -
几乎所有的
Mock函数都带有.mock的属性,它保存了关于此函数如何被调用、调用时的传入参数值和返回值的信息,这在断言的时候很有用处。我们也可以通过console.log(mockCallback.mock)来查看:-
calls数组中保存了该函数被调用的情况,instances表示它实例this的指向,results表示这次执行输出的结果 - 在本测试用例中,
mock被调用了两次,因此calls中有两个数组,每个数组的内容表示调用函数时传入的参数
-
模拟内部实现
jest.fn()可以传入一个函数,从而生成带逻辑的函数:
test('模拟内部实现', () => {
const fun = jest.fn();
fun.mockImplementation((num1, num2) => {
return num1 + num2;
});
fun.mockImplementationOnce((num1, num2) => {
return num1 * num2;
});
expect(fun(3, 5)).toBe(15); // 执行mockImplementationOnce
expect(fun(3, 5)).toBe(8); // 执行mockImplementation
});
- 调用生成的
mock函数的mockImplementation或者mockImplementationOnce方法可以改变mock函数的内容,两者的区别是:mockImplementationOnce方法会在第一次调用时被执行,它可以链式调用,从而每次执行不同的逻辑 - 当需要多个函数调用产生不同的结果时,使用
mockImplementationOnce方法会很有用
模拟返回值
jest.fn()如果没有定义函数内部的实现, 默认情况下会返回 undefined :
test('默认返回undefined', () => {
const fun = jest.fn();
const res = fun(1, 2, 3);
expect(res).toBeUndefined(); // 返回undefined
expect(fun).toBeCalledWith(1, 2, 3); // 传入的参数为 1, 2, 3
});
此外,jest.fn()可以为函数设置返回值,包括Promise对象:
test('返回固定值 ', () => {
const func = jest.fn();
func.mockReturnValue('Mocked Return');
func.mockReturnValueOnce('Mocked Return by Once');
expect(func()).toBe('Mocked Return by Once');
expect(func()).toBe('Mocked Return');
});
test('返回Promise对象', async () => {
const func = jest.fn().mockResolvedValue('Mocked Promise');
let res = await func();
expect(res).toBe('Mocked Promise'); // func通过await关键字执行后返回值为Mocked Promise
});
- 与模拟内部实现相同,改变
mock函数的返回值也有两个方法,mockReturnValue和mockReturnValueOnce,第一调用返回的是mockReturnValueOnce方法设定的值,它也可以链式调用,从而多次返回不同的值 mockReturnValue()和mockImplementationOnce()的区别在于mockReturnValue只能写返回值,而mockImplementationOnce里可以写更多的逻辑再returnmockResolvedValue方法是从Jest v23开始引入的mock异步函数的语法糖,它与下面两种写法是一个意思:jest.fn().mockImplementation(() => Promise.resolve(value))jest.fn(() => Promise.resolve(value))
模拟axios
实际测试异步函数的时候,我们不会真正的发送ajax请求去请求这个接口,最好的方式还是mock数据,让它不用发送请求也能测试我们的接口调用是否正确。
我们首先在index.js中编写一个简单的请求数据的代码:
// index.js
import axios from 'axios';
export const axiosRequest = () => {
return axios.get('/api').then(res => res.data);
};
这里url的内容可以随便写,有效无效都行,因为我们不会真的发送请求。
// index.spec.js
import axios from 'axios';
import { axiosRequest } from './index.js';
jest.mock('axios');
test('模拟axios', async () => {
axios.get.mockResolvedValue({
data: {
name: 'Yiler',
year: 2022
},
status: 'success'
});
await axiosRequest().then(data => {
expect(data).toEqual({
name: 'Yiler',
year: 2022
});
});
});
jest.mock('axios')模拟了axios模块,并且我们自定义了请求数据,从而将异步获取数据转变为同步准备数据,避免了向后台去请求接口。- 注意·:
jest.mock('axios')必须写在最外层
测试顺利通过