Bun

模拟

使用 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 的其他部分一样,模块模拟同时支持 importrequire

覆盖已导入的模块

如果你需要覆盖一个已经导入的模块,你不需要做任何特殊的事情。只需调用 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 缓存中。这意味着你可以对模拟的模块交替使用 importrequire

仅当导入或需要模块时,才会延迟调用回调函数。这意味着你可以使用 mock.module() 来模拟尚未存在的模块,并且这意味着你可以使用 mock.module() 来模拟其他模块导入的模块。