使用 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
为了让你的生活更轻松,你可以在 bunfig.toml
中放置 preload
[test]
# Load these modules before running tests.
preload = ["./my-preload"]
如果我模拟一个已经导入的模块会发生什么?
如果你模拟一个已经导入的模块,该模块将在模块缓存中更新。这意味着任何导入该模块的模块都将获得模拟版本,但是原始模块仍然会被评估。这意味着原始模块的任何副作用仍然会发生。
如果你想防止评估原始模块,你应该使用 --preload
在测试运行之前加载你的模拟。
__mocks__
目录和自动模拟
自动模拟尚不支持。如果这阻碍了你切换到 Bun,请提交一个问题。
实现细节
模块模拟对 ESM 和 CommonJS 模块有不同的实现。对于 ES 模块,我们已经为 JavaScriptCore 添加了补丁,允许 Bun 在运行时覆盖导出值并递归更新实时绑定。
从 Bun v1.0.19 开始,Bun 会自动将 specifier
参数解析为 mock.module()
,就好像你执行了 import
一样。如果解析成功,则解析后的规范字符串将用作模块缓存中的键。这意味着你可以使用相对路径、绝对路径,甚至模块名称。如果 specifier
无法解析,则原始 specifier
将用作模块缓存中的键。
解析后,模拟的模块将存储在 ES 模块注册表和 CommonJS require 缓存中。这意味着你可以对模拟的模块交替使用 import
和 require
。
仅当导入或需要模块时,才会延迟调用回调函数。这意味着你可以使用 mock.module()
来模拟尚未存在的模块,并且这意味着你可以使用 mock.module()
来模拟其他模块导入的模块。