Mock
Mock
在测试中,mock 可以让你更方便的去测试依赖于数据库、网络请求、文件等外部系统的函数。Jest 内置了 mock 机制,提供了多种 mock 方式已应对各种需求。
Mock 函数
函数的 mock 非常简单,调用 jest.fn() 即可获得一个 mock 函数。Mock 函数有一个特殊的 .mock 属性,保存着函数的调用信息。.mock 属性还会追踪每次调用时的 this。
// mocks/forEach.js
export default (items, callback) => {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
};
// test.js
import forEach from "./forEach";
it("test forEach function", () => {
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});
除了 .mock 之外,Jest 还未我们提供了一些匹配器用来断言函数的执行,它们本身只是检查 .mock 属性的语法糖:
// The mock function was called at least once
expect(mockFunc).toBeCalled();
使用 mockReturnValue 和 mockReturnValueOnce 可以 mock 函数的返回值。当我们需要为 mock 函数增加一些逻辑时,可以使用 jest.fn()、mockImplementation 或者 mockImplementationOnce mock 函数的实现。还可以使用 mockName 还给 mock 函数命名,如果没有命名,输出的日志默认就会打印 jest.fn()。
Mock 定时器
Jest 可以 Mock 定时器以使我们在测试代码中控制“时间”。调用 jest.useFakeTimers() 函数可以伪造定时器函数,定时器中的回调函数不会被执行,使用 setTimeout.mock 等可以断言定时器执行情况。当在测试中有多个定时器时,执行 jest.useFakeTimers() 可以重置内部的计数器。
-
执行 jest.runAllTimers(); 可以“快进”直到所有的定时器被执行;
-
执行 jest.runOnlyPendingTimers() 可以使当前正在等待的定时器被执行,用来处理定时器中设置定时器的场景,如果使用 runAllTimers 会导致死循环;
-
执行 jest.advanceTimersByTime(msToRun:number),可以“快进”执行的毫秒数。
监控 setTimeout 的调用次数
// timerGame.js
"use strict";
function timerGame(callback) {
console.log("Ready....go!");
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
module.exports = timerGame;
// __tests__/timerGame-test.js
("use strict");
jest.useFakeTimers();
test("waits 1 second before ending the game", () => {
const timerGame = require("../timerGame");
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});
运行所有的计时器
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.runAllTimers() 之类的东西。对于这些情况,您可以使用 jest.runOnlyPendingTimers():
// infiniteTimerGame.js
"use strict";
function infiniteTimerGame(callback) {
console.log("Ready....go!");
setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();
// Schedule the next game in 10 seconds
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
module.exports = infiniteTimerGame;
// __tests__/infiniteTimerGame-test.js
("use strict");
jest.useFakeTimers();
describe("infiniteTimerGame", () => {
test("schedules a 10-second timer after 1 second", () => {
const infiniteTimerGame = require("../infiniteTimerGame");
const callback = jest.fn();
infiniteTimerGame(callback);
// At this point in time, there should have been a single call to
// setTimeout to schedule the end of the game in 1 second.
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
// Fast forward and exhaust only currently pending timers
// (but not any new timers that get created during that process)
jest.runOnlyPendingTimers();
// At this point, our 1-second timer should have fired it's callback
expect(callback).toBeCalled();
// And it should have created a new timer to start the game over in
// 10 seconds
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});
Mock 模块
模块的 mock 主要有两种方式:
-
使用 jest.mock(moduleName, factory, options) 自动 mock 模块,jest 会自动帮我们 mock 指定模块中的函数。其中,factory 和 options 参数是可选的。factory 是一个模块工厂函数,可以代替 Jest 的自动 mock 功能;options 用来创建一个不存在的需要模块。
-
如果希望自己 mock 模块内部函数,可以在模块平级的目录下创建 mocks 目录,然后创建相应模块的 mock 文件。对于用户模块和 Node 核心模块(如:fs、path),我们仍需要在测试文件中显示的调用 jest.mock(),而其他的 Node 模块则不需要。
此外,在 mock 模块时,jest.mock() 会被自动提升到模块导入前调用。对于类的 mock 基本和模块 mock 相同,支持自动 mock、手动 mock 以及调用带模块工厂参数的 jest.mock(),还可以调用 jest.mockImplementation() mock 构造函数。
ES6 类 Mock
// sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = "bar";
}
playSoundFile(fileName) {
console.log("Playing sound file " + fileName);
}
}
// sound-player-consumer.js
import SoundPlayer from "./sound-player";
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = "song.mp3";
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
Automatic mock
import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player"); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});
it("We can check if the consumer called the class constructor", () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it("We can check if the consumer called a method on the class instance", () => {
// Show that mockClear() is working:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Constructor should have been called again:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = "song.mp3";
soundPlayerConsumer.playSomethingCool();
// mock.instances is available with automatic mocks:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});