持续创作,加速成长!这是我参与「掘金日新计划 · 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
里可以写更多的逻辑再return
mockResolvedValue
方法是从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')
必须写在最外层
测试顺利通过