[Jest] Mocking module, file and function
模拟函数
在 Jest 中提供了一个全局对象名为 jest,这个对象上面有非常多的方法,有关该对象的方法,可以参阅文档:
https://jestjs.io/docs/jest-object
jest 对象上面的方法大致分为四类:
- 模拟模块
- 模拟函数
- 模拟计时器
- 其他方法
通过 jest.fn 方法可以创建一个模拟函数(mock fucntion)
jest.fn(implementation?)
implementation 是一个可选参数,代表着模拟函数的实现,如果没有传入,那么创建的是一个空的模拟函数。
来看一个快速入门示例:
test("基本演示",()=>{
// 创建一个模拟函数
const mock = jest.fn();
// 设置这个模拟函数的返回值为 42
mock.mockReturnValue(42);
expect(mock()).toBe(42);
});
在上面的代码中,我们使用 jest.fn 方法创建了一个空的模拟函数,然后通过调用 mockReturnValue 方法来指定该模拟函数的返回值为 42.之后通过 expect 调用对该模拟函数进行一个测试。
在使用 jest.fn 创建模拟函数的时候,也可以传入一个函数来代表模拟函数的实现,一般通过传入的函数能够明确所生成的模拟函数接收几个参数,返回值是多少。
test("内置实现",()=>{
const mock = jest.fn(x => 100 + x);
expect(mock(1)).toBe(101);
})
调用 jest.fn 方法后返回的是一个模拟函数,之所以可以在函数的基础上调用方法,是因为在 js 中函数也是一种对象,这里的模拟函数类似于如下的表达:
function a(){}
a.b = function(){}
a.c = function(){}
a.d = function(){}
可以在官方文档 https://jestjs.io/docs/mock-function-api 看到模拟函数所对应的方法,举例如下:
test("基本演示",()=>{
// 创建一个模拟函数
const mock = jest.fn();
mock.mockReturnValue(30) // 设置返回值为 30
.mockReturnValueOnce(10) // 第一次调用模拟函数对应的返回值
.mockReturnValueOnce(20) // 第二次调用模拟函数对应的返回值
expect(mock()).toBe(10);
expect(mock()).toBe(20);
expect(mock()).toBe(30);
// 设置这个模拟函数的返回值为 42
mock.mockReturnValue(42);
expect(mock()).toBe(42);
});
通过模拟函数身上的这些方法,可以控制模拟函数的行为,例如上面我们通过 mockReturnValueOnce 控制函数不同次数的调用对应的返回值。
接下来我们来看两个模拟函数具体的应用场景。
首先第一个,假设我们书写了一个 forEach 函数,这个 forEach 就类似于数组里面的 forEach 方法,该函数会遍历数组里面的每一项,然后针对每一项执行对应的回调函数:
const arr = [1,2,3,4,5];
arr.forEach((item)=>{
// item....
})
function forEach(arr, callback){
for(let index = 0;i< arr.length; index++){
callback(arr[index]);
}
}
forEach(arr, (item)=>{});
接下来我们想要测试这个 forEach 函数的实现是否有问题,那么这里涉及到了这个 forEach 依赖了 callback 这个函数,因此我们就可以通过模拟函数的方式来对其进行屏蔽
const arr = [1, 2, 3];
function forEach(arr, callback) {
for (let index = 0; index < arr.length; index++) {
callback(arr[index]);
}
}
test("测试forEach是否正确", () => {
// 由于 forEach 中依赖了 callback,因此我们可以创建一个模拟函数来模拟这个 callback
const mockCallback = jest.fn((x) => 100 + x);
forEach(arr, mockCallback);
// 接下来就进入到测试环节,我们可以利用模拟函数上面的诸多方法来进行一个验证
// [
// [ 1 ],
// [ 2 ],
// [ 3 ]
// ];
expect(mockCallback.mock.calls).toHaveLength(3);
expect(mockCallback.mock.calls.length).toBe(3);
// 测试每一次调用 callback 的时候传入的参数是否符合预期
expect(mockCallback.mock.calls[0][0]).toBe(1);
expect(mockCallback.mock.calls[1][0]).toBe(2);
expect(mockCallback.mock.calls[2][0]).toBe(3);
// 针对每一次 callback 被调用后的返回值进行测试
expect(mockCallback.mock.results[0].value).toBe(101);
expect(mockCallback.mock.results[1].value).toBe(102);
expect(mockCallback.mock.results[2].value).toBe(103);
// 模拟函数是否被调用过
expect(mockCallback).toHaveBeenCalled();
// 前面在调用的时候是否有参数为 1 以及参数为 2 的调用
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenCalledWith(2);
// 还可以对模拟函数的参数进行一个边界判断,判断最后一次调用是否传入的参数为 3
expect(mockCallback).toHaveBeenLastCalledWith(3);
});
接下来我们来看第二例子,我们来模拟一个异步请求的场景。假设有如下的异步请求函数:
async function fetchData(){
const res = await fetch("https://www.example.com/data");
const data = await res.json();
return data;
}
在测试这个异步函数的时候,会发送真实的请求进行测试,但是有一些时候,我们知道这个没问题,或者说想要在那时屏蔽这一个异步,假设一个异步是能够正常返回数据的,这种情况下我们就可以针对这个异步请求函数来书写一个模拟函数来代替真实的 fetchData 函数。
// 创建了一个空的模拟函数
const fetchDataMock = jest.fn();
const fakeData = { id: 1, name: "xiejie" };
// 设置该模拟函数的实现
fetchDataMock.mockImplementation(() => Promise.resolve(fakeData));
// 通过模拟函数的一些方法来设置该模拟函数的行为
test("模拟网络请求正常", async () => {
const data = await fetchDataMock();
expect(data).toEqual({ id: 1, name: "xiejie" });
});
test("模拟网络请求出错", async () => {
// 模拟网络请求第一次请求失败,之后请求没问题
fetchDataMock.mockImplementationOnce(() =>
Promise.reject(new Error("network error"))
);
await expect(fetchDataMock()).rejects.toThrow("network error");
await expect(fetchDataMock()).resolves.toEqual({ id: 1, name: "xiejie" });
});
总结
当在 Jest 测试框架中编写测试用例时,我们通常需要模拟一些函数或者对象以便在测试中控制其行为。
Jest 提供了 jest.fn( ) 方法来创建模拟函数,它有以下几个特点:
- 模拟函数可以接受任何参数,并且返回任何值。
- 可以使用 mockImplementation( ) 方法或者 mockImplementationOnce( ) 方法来设置模拟函数的实现。
- 可以使用 expect( ).toHaveBeenCalled( ) 或者 expect( ).toHaveBeenCalledWith( ) 等函数来断言模拟函数是否被调用,并且被调用的方式是否符合预期。
- 可以使用 mockFn.mockReturnValue( ) 或者 mockFn.mockResolvedValue( ) 等方法来设置模拟函数的返回值或者 Promise 对象的解析值。
总的来说,jest.fn( ) 是 Jest 中非常重要的一个功能,它可以帮助我们在测试中模拟函数或对象的行为,并且方便地进行断言。在编写测试时,我们可以根据需要使用它来代替真实的依赖或者桩数据,从而使测试更加可控、可靠、可维护。
模拟模块
模块可以分为两种模块:
- 第三方模块
- 文件模块
模拟第三方模块
在 jest 对象上面有一个名为 mock 的方法。
下面是一个快速入门示例:
/**
* 和请求相关的
*/
const axios = require("axios");
class User {
/**
* 获取所有的用户
*/
static all() {
return axios.get("/users.json").then((resp) => resp.data);
}
}
module.exports = User;
假设现在项目中有如上的一个方法,现在我们需要对这个模块的方法进行一个测试,但是会涉及到一个问题,要测试这个模块就必然会涉及到使用 axios 发送真实的 http 请求,这个时候我们想要屏蔽这个真实的请求。
其中一种方案就是像上一小节一样,实现一个 all 方法的模拟方法,屏蔽内部的实现,但是这种方法会有一个问题,我们无法测试 all 方法内部的实现是否正确,如果是这种情况,我们就可以采取模拟 axios 模块的方式来屏蔽 axios 发送请求这个部分。
这个时候我们可以使用 jest.mock 来模拟 axios 这个模块,如下:
const axios = require('axios');
const User = require('../api/userApi');
const userData = require("./user.json");
// 模拟 axios 模块
jest.mock('axios');
// 测试用例
test("测试获取用户数据", async ()=>{
// 模拟响应数据
const resp = {
data : userData
};
// 现在我们已经模拟了 axios
// 但是目前的 axios 没有书写任何的行为
// 因此我们需要在这里进行一个 axios 模块行为的指定
// 指定了在使用 axios.get 的时候返回 resp 响应
axios.get.mockImplementation(()=>Promise.resolve(resp));
await expect(User.all()).resolves.toEqual(userData);
});
在上面的测试套件中,我们首先使用 jest.mock 方法模拟了 axios 这个模块。
之后书写了一个测试用例,在测试用例里面,我们指定了 axios.get 方法的行为,之后对 User.all 方法进行测试。在 User.all 方法里面使用到 axios.get 方法,这个时候就会使用模拟的 axios 模块。
在上面的示例中,我们也可以传入第二个参数,第二个参数可以指定模块的一些实现,如下:
// const axios = require("axios");
const User = require("../api/userApi");
const userData = require("./user.json");
// 模拟 axios 模块
jest.mock("axios", () => {
const userData = require("./user.json");
// 模拟响应数据
const resp = {
data: userData,
};
return {
get: jest.fn(() => Promise.resolve(resp)),
};
});
// 测试用例
test("测试获取用户数据", async () => {
// 现在我们已经模拟了 axios
// 但是目前的 axios 没有书写任何的行为
// 因此我们需要在这里进行一个 axios 模块行为的指定
// 指定了在使用 axios.get 的时候返回 resp 响应
// axios.get.mockImplementation(()=>Promise.resolve(resp));
await expect(User.all()).resolves.toEqual(userData);
});
在上面的方法中,我们使用 jest.mock 模拟 axios 模块时,传入了第二个参数,第二个参数是一个工厂函数,指定了模块的一些行为,之后,我们就不用在单独使用诸如 mockImplementation 之类的方法来指定模块的实现了。
除了替换模块本身,还可以为这个模块添加一些额外的方法:
// 模拟 axios 模块
jest.mock("axios", () => {
const userData = require("./user.json");
// 模拟响应数据
const resp = {
data: userData,
};
return {
get: jest.fn(() => Promise.resolve(resp)),
// 这个方法本身 axios 是没有的
// 我们通过模拟 axios 这个模块,然后给 axios 这个模块添加了这么一个 test方法
// 这里在实际开发中没有太大意义,仅做演示
test : jest.fn(() => Promise.resolve("this is a test")),
};
});
模拟文件模块
通过 jest.mock,我们还可以模拟整个文件模块:
const { sum, sub, mul, div } = require("../utils/tools");
jest.mock("../utils/tools", () => {
// 在这里来改写文件模块的实现
// 拿到 ../utils/tools 路径所对应的文件原始模块
const originalModule = jest.requireActual("../utils/tools");
// 这里相当于是替换了原始的模块
// 一部分方法使用原始模块中的方法
// 一部分方法(sum、sub)被替换掉了
return {
...originalModule,
sum: jest.fn(() => 100),
sub: jest.fn(() => 50),
};
});
test("对模块进行测试", () => {
expect(sum(1, 2)).toBe(100);
expect(sub(10, 3)).toBe(50);
expect(mul(10, 3)).toBe(30);
expect(div(10, 2)).toBe(5);
});
在上面的例子中,我们引入了路径为 ../utils/tools 的文件模块,并且我们对这个文件模块进行了一个模拟,替换掉了这个文件模块里面的部分方法。
在今天的例子中,我们第一次创建了两个测试套件,可以看到在运行的时候,没有再像之前一样显示出测试用例的描述。如果想要显示,可以添加如下的配置:
"test": "jest --verbose=true"
这个配置实际上就是 jest cli 的配置选项,关于配置我们后面专门拿一节课来进行介绍。
总结
本节课我们介绍了非常有用的 jest.mock( ) 方法,通过该方法可以模拟导入的模块,从而方便地测试被测试模块的行为,而不需要真正地执行模块的代码。
除了上面介绍的示例以外,下面罗列了一些在实际开发中可能会使用到 jest.mock( ) 方法的例子:
- 模拟外部依赖
当您的被测试模块依赖于外部模块时,您可以使用 jest.mock( ) 方法来模拟这些模块的行为,以便更好地控制测试环境。例如,当您的代码依赖于一个需要连接到数据库的模块时,您可以使用 jest.mock( ) 方法来模拟这个模块的行为,以便在测试时避免连接到真实的数据库。 - 模拟函数的行为
当您的被测试模块调用其他函数时,您可以使用 jest.mock( ) 方法来模拟这些函数的行为,以便更好地控制测试环境。例如,当您的代码调用一个外部第三方库中的函数时,您可以使用 jest.mock( ) 方法来模拟这个函数的行为,以便在测试时避免调用真实的库函数,同时确保您的代码正确处理了这个函数的返回值和参数。 - 模拟组件
当您的被测试模块是一个 React 组件时,您可以使用 jest.mock( ) 方法来模拟这个组件的行为,以便更好地控制测试环境。例如,当您测试一个依赖于其他组件的组件时,您可以使用 jest.mock( ) 方法来模拟这些组件的行为,以便在测试时避免真正地渲染这些组件。
总之,使用 Jest 的 jest.mock( ) 方法,可以帮助您轻松地模拟各种依赖项和操作的行为,从而使测试更加简单和可靠。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2023-01-24 [SolidJS] Build a simple version of reactivity
2019-01-24 [TypeScript] Infer the Return Type of a Generic Function Type Parameter
2018-01-24 [MST] Derive Information from Models Using Views
2018-01-24 [TypeScript] Asynchronous Iteration using for-await-of
2018-01-24 [Typescript] Promise based delay function using async / await
2017-01-24 [Vue] Dynamic Vue.js Components with the component element
2017-01-24 [Vue] Parent and Child component communcation