Jest 使用指南 - - Mock 篇
#jest
Jest Mock
为什么会用到 Mock? Mock 能帮我们解决什么问题?
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 擦除函数的实际实现(换句话说:改变函数的内部实现)
- 捕获函数调用情况( 包括:这些调用中传递的参数、new 的实例)
- 设置函数返回值
jest.fn()
Jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
Jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。
test("测试jest.fn()返回固定值", () => {
let mockFn = jest.fn().mockReturnValue("Felix");
// 断言 mockFn 执行后返回值为Felix
expect(mockFn()).toBe("Felix");
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
// > 10 'x' true
});
test("测试jest.fn()内部实现", () => {
let mockFn = jest.fn((x) => 42 + x);
// 断言mockFn执行后返回 52
expect(mockFn(10)).toBe(52);
});
test("测试jest.fn()返回Promise", async () => {
let mockFn = jest.fn().mockResolvedValue("Felix");
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为Felix
expect(result).toBe("Felix");
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
});
jest.mock()
ES6 Class Mocks · Jest
有下面几种方式来 mock 模块
- 使用 jest.mock 自动 mock
jest.mock('./utils.ts')
自动返回一个 mock ,可以使用它来监视对类构造函数及其所有方法的调用。方法调用保存在中theAutomaticMock.mock.instances[index].methodName.mock.calls
。
⚠️:如果您在类中使用箭头函数,则它们不会成为模拟的一部分。这样做的原因是,箭头函数不存在于对象的原型中,它们只是持有对该函数的引用的属性。 - jest.mock()直接在单元测试里面mock 模块
jest.mock(path, moduleFactory)
接受模块工厂参数。模块工厂是一个返回模拟的函数。为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是返回函数的函数-高阶函数(HOF)。
例如在 node 端会通过 fs 来读取文件,在单元测试中, 我们并不需要真去调用fs读取文件, 就可以考虑把fs模块mock掉, 如下代码:
jest.mock('fs', () => ({
readFileSync: jest.fn()
}))
⚠️:这么使用有个限制就是,因为调用 jest.mock() 被提升到了文件开头,因此不可以先定义一个变量,然后在工厂中使用它。以 "mock "开头的变量是一个例外。这取决于你是否能保证它们按时被初始化!
3. 在需要mock的模块目录临近建立目录__mocks__
对于用户目录下面的模块
例如我们需要mock目录models下面的user模块,那么我们就需要在models下面新建__mocks__目录(这里要区分大小写),然后新建文件user.js。
注意:用这种方式, 需要在单元测试文件中需添加下面的代码才能使此mock生效。jest.mock('./moduleName')
// mock.ts
import utils from "./utils";
export default {
test() {
return utils.add(1, 2);
},
};
// utils.ts
export default {
add(a: number, b: number): number {
console.log("----", a, b);
return a + b;
},
};
// mock.test.ts
import m from "./mock";
import utils from "./utils";
jest.mock("./utils.ts");
test("mock 整个 fetch.js模块", () => {
m.test();
expect(y).toBeCalledTimes(1);
});
终端结果
jest.spyOn()
Jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。
import m from "./mock";
import utils from "./utils";
test("mock 整个 fetch.js模块", () => {
const y = jest.spyOn(utils, "add");
m.test();
expect(y).toBeCalledTimes(1);
});
上面 jest.mock() 终端截图中可以看到,console 并没有打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。这时我们就需要使用jest.spyOn()。
Time mock
我们时常会用到 setTimeout 、setInterval、 clearTimeout 、clearInterval,针对这些定时器我们怎么去控制单测呢。
根据官方文旦time mock的说明,这里可以分为三种 Run All time(快进所有的定时时间)、Advance Timer by Time(快进指定的时间)、Run Pending Timer(针对嵌套了定时器的场景)
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
Run All Timer
jest.useFakeTimers();
test('calls the callback after 1 second', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled()
// Fast-forward until all timers have been executed
jest.runAllTimers();
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
注意:这里我们通过调用 jest.useFakeTimers(); 来启用假定时器。这用模拟函数模拟了 setTimeout 和其他计时器函数。
Advance TImer by Times
原先的方法名和上面的 runAllTimers 是呼应的,叫做 runTimersToTime,在 v22.0.0 后重命名为 advanceTimersByTime ,这个函数的作用就是快进指定的时间,
it('calls the callback after 1 second via advanceTimersByTime', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled();
// Fast-forward until all timers have been executed
jest.advanceTimersByTime(1000);
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
就上面这个而言,其实和 runAllTimer 是一样的,因为我们指定的时间就是全部的时间,下面我们可以尝试一下设置另外的时间
it("calls the callback after 999 millsecond via advanceTimersByTime", () => {
const callback = jest.fn();
timerGame(callback);
expect(callback).not.toBeCalled();
// 改为 999 毫秒,不到 1秒
jest.advanceTimersByTime(999);
expect(callback).not.toBeCalled();
expect(callback).toHaveBeenCalledTimes(0);
// 又快进了 1 毫秒
jest.advanceTimersByTime(1);
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
上面的例子我们将指定时间改为。999 毫秒,不到 1 秒,所以相应的回调函数还没执行,根据运行结果的确是的,之后我们又只快进了 1毫秒,对应的 callback 执行了,符合我们的预期
最后一种 Run Pending Timer 就不多讲了,遇到了再来看,相对来说上面两种使用的场景较多
总结
在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。