使用 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.fn()
函数,就像在 Jest 中一样。它的行为完全相同。
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()
创建一个间谍;这些间谍可以传递给 .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"]
如果我模拟一个已经导入的模块,会发生什么?
如果您模拟一个已经导入的模块,该模块将在模块缓存中更新。这意味着任何导入该模块的模块都将获得模拟版本,但是原始模块仍将已求值。这意味着原始模块的任何副作用仍将发生。
如果您想阻止原始模块被求值,您应该使用 --preload
在测试运行之前加载模拟。
__mocks__
目录和自动模拟
尚不支持自动模拟。如果这阻碍您切换到 Bun,请提交一个问题。
实现细节
模块模拟对 ESM 和 CommonJS 模块有不同的实现。对于 ES 模块,我们已对 JavaScriptCore 进行了修补,允许 Bun 在运行时覆盖导出值并递归更新实时绑定。
从 Bun v1.0.19 开始,Bun 会自动将 mock.module()
的 specifier
参数解析为好像您执行了 import
一样。如果解析成功,则解析后的 specifier 字符串将用作模块缓存中的键。这意味着您可以使用相对路径、绝对路径甚至模块名称。如果 specifier
无法解析,则原始 specifier
将用作模块缓存中的键。
解析后,模拟模块将存储在 ES 模块注册表**和** CommonJS require 缓存中。这意味着您可以对模拟模块互换使用 import
和 require
。
回调函数是惰性调用的,仅当模块被导入或要求时才会调用。这意味着您可以使用 mock.module()
模拟尚不存在的模块,并且这意味着您可以使用 mock.module()
模拟被其他模块导入的模块。
模块模拟实现细节
了解 mock.module()
的工作原理有助于您更有效地使用它
缓存交互:模块模拟与 ESM 和 CommonJS 模块缓存进行交互。
惰性求值:模拟工厂回调仅在模块实际导入或要求时才会被求值。
路径解析:Bun 会自动解析模块说明符,就像您正在执行导入一样,支持
- 相对路径 (
'./module'
) - 绝对路径 (
'/path/to/module'
) - 包名 (
'lodash'
)
- 相对路径 (
导入时机效果:
- 在首次导入之前模拟时:不会发生原始模块的副作用
- 在导入之后模拟时:原始模块的副作用已经发生
- 因此,对于需要阻止副作用的模拟,建议使用
--preload
实时绑定:模拟的 ESM 模块维护实时绑定,因此更改模拟将更新所有现有导入
全局模拟函数
使用 mock.clearAllMocks()
清除所有模拟
重置所有模拟函数状态(调用、结果等),但不恢复其原始实现
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("clearing all mocks", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// Note: implementations are preserved
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});
这将重置所有模拟的 .mock.calls
、.mock.instances
、.mock.contexts
和 .mock.results
属性,但与 mock.restore()
不同,它不会恢复原始实现。
使用 mock.restore()
恢复所有函数模拟
不必使用 mockFn.mockRestore()
手动单独恢复每个模拟,而是通过调用 mock.restore()
一次性恢复所有模拟。这样做不会重置使用 mock.module()
覆盖的模块的值。
通过将其添加到每个测试文件中的 afterEach
块甚至您的 测试预加载代码中,使用 mock.restore()
可以减少测试代码量。
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");
});
Vitest 兼容性
为了更好地兼容为 Vitest 编写的测试,Bun 提供了 vi
全局对象作为 Jest 模拟 API 部分的别名
import { test, expect } from "bun:test";
// Using the 'vi' alias similar to Vitest
test("vitest compatibility", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// The following functions are available on the vi object:
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});
这使得将测试从 Vitest 移植到 Bun 变得更容易,而无需重写所有模拟。