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

如果我 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 缓存中。这意味着您可以互换使用 importrequire 来 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');
});