Jest 中的模拟函数 Mock

1,435 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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)来查看:

    image.png

    •  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函数的返回值也有两个方法,mockReturnValuemockReturnValueOnce第一调用返回的是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')必须写在最外层

测试顺利通过

image.png