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

为了方便起见,您可以将 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 缓存中。这意味着您可以对模拟模块互换使用 importrequire

回调函数是惰性调用的,仅当模块被导入或要求时才会调用。这意味着您可以使用 mock.module() 模拟尚不存在的模块,并且这意味着您可以使用 mock.module() 模拟被其他模块导入的模块。

模块模拟实现细节

了解 mock.module() 的工作原理有助于您更有效地使用它

  1. 缓存交互:模块模拟与 ESM 和 CommonJS 模块缓存进行交互。

  2. 惰性求值:模拟工厂回调仅在模块实际导入或要求时才会被求值。

  3. 路径解析:Bun 会自动解析模块说明符,就像您正在执行导入一样,支持

    • 相对路径 ('./module')
    • 绝对路径 ('/path/to/module')
    • 包名 ('lodash')
  4. 导入时机效果:

    • 在首次导入之前模拟时:不会发生原始模块的副作用
    • 在导入之后模拟时:原始模块的副作用已经发生
    • 因此,对于需要阻止副作用的模拟,建议使用 --preload
  5. 实时绑定:模拟的 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 变得更容易,而无需重写所有模拟。