使用 mock
函数创建模拟。
import { test, expect, mock } from "bun:test";
const random = mock(() => Math.random());
test("random", async () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});
或者,您可以像在 Jest 中一样使用 jest.fn()
函数。它的行为是相同的。
import { test, expect, jest } from "bun:test";
const random = jest.fn(() => Math.random());
test("random", async () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});
mock()
的结果是一个新的函数,它被装饰了一些额外的属性。
import { mock } from "bun:test";
const random = mock((multiplier: number) => multiplier * Math.random());
random(2);
random(10);
random.mock.calls;
// [[ 2 ], [ 10 ]]
random.mock.results;
// [
// { type: "return", value: 0.6533907460954099 },
// { type: "return", value: 0.6452713933037312 }
// ]
以下属性和方法在模拟函数上实现。
.spyOn()
可以跟踪对函数的调用,而无需用模拟替换它。使用 spyOn()
创建一个 spy;这些 spy 可以传递给 .toHaveBeenCalled()
和 .toHaveBeenCalledTimes()
。
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Hello I'm ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
});
使用 mock.module()
的模块模拟
模块模拟允许您覆盖模块的行为。使用 mock.module(path: string, callback: () => Object)
来模拟一个模块。
import { test, expect, mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});
test("mock.module", async () => {
const esm = await import("./module");
expect(esm.foo).toBe("bar");
const cjs = require("./module");
expect(cjs.foo).toBe("bar");
});
像 Bun 的其余部分一样,模块模拟同时支持 import
和 require
。
覆盖已导入的模块
如果您需要覆盖已导入的模块,则无需执行任何特殊操作。只需调用 mock.module()
,模块将被覆盖。
import { test, expect, mock } from "bun:test";
// The module we're going to mock is here:
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// We update it here:
mock.module("./module", () => {
return {
foo: "baz",
};
});
// And the live bindings are updated.
expect(foo).toBe("baz");
// The module is also updated for CJS.
expect(cjs.foo).toBe("baz");
});
提升 & 预加载
如果您需要确保模块在导入之前被模拟,则应使用 --preload
在测试运行之前加载您的模拟。
// my-preload.ts
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});
bun test --preload ./my-preload
为了让您的生活更轻松,您可以将 preload
放入您的 bunfig.toml
中
[test]
# Load these modules before running tests.
preload = ["./my-preload"]
如果我 mock 一个已经被导入的模块会发生什么?
如果您 mock 一个已经被导入的模块,该模块将在模块缓存中更新。这意味着任何导入该模块的模块都将获得 mock 版本,**但是** 原始模块仍然会被执行。这意味着来自原始模块的任何副作用仍然会发生。
如果您想阻止原始模块被执行,您应该使用 --preload
在测试运行之前加载您的 mock。
__mocks__
目录和自动 mock
尚不支持自动 mock。如果这阻碍了您切换到 Bun,请提交 issue。
实现细节
模块 mock 对于 ESM 和 CommonJS 模块有不同的实现。对于 ES 模块,我们为 JavaScriptCore 添加了补丁,允许 Bun 在运行时覆盖导出值并递归更新实时绑定。
从 Bun v1.0.19 开始,Bun 会自动解析 specifier
参数到 mock.module()
,就像您执行了 import
一样。如果解析成功,则解析后的 specifier 字符串将用作模块缓存中的键。这意味着您可以使用相对路径、绝对路径,甚至模块名称。如果 specifier
未能解析,则原始 specifier
将用作模块缓存中的键。
解析后,mock 模块将存储在 ES 模块注册表**和** CommonJS require 缓存中。这意味着您可以互换使用 import
和 require
来 mock 模块。
回调函数是惰性调用的,仅当模块被导入或 require 时才调用。这意味着您可以使用 mock.module()
来 mock 尚不存在的模块,并且意味着您可以使用 mock.module()
来 mock 被其他模块导入的模块。
使用 mock.restore()
将所有函数 mock 恢复到其原始值
与其使用 mockFn.mockRestore()
手动单独恢复每个 mock,不如通过调用 mock.restore()
一次命令恢复所有 mock。这样做不会重置使用 mock.module()
覆盖的模块的值。
通过将 mock.restore()
添加到每个测试文件中的 afterEach
代码块,甚至添加到您的 测试预加载代码 中,可以减少测试中的代码量。
import { expect, mock, spyOn, test } from "bun:test";
import * as fooModule from './foo.ts';
import * as barModule from './bar.ts';
import * as bazModule from './baz.ts';
test('foo, bar, baz', () => {
const fooSpy = spyOn(fooModule, 'foo');
const barSpy = spyOn(barModule, 'bar');
const bazSpy = spyOn(bazModule, 'baz');
expect(fooSpy).toBe('foo');
expect(barSpy).toBe('bar');
expect(bazSpy).toBe('baz');
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy).toBe(42);
expect(barSpy).toBe(43);
expect(bazSpy).toBe(44);
mock.restore();
expect(fooSpy).toBe('foo');
expect(barSpy).toBe('bar');
expect(bazSpy).toBe('baz');
});