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()

可以追蹤函式的呼叫,而無需將其替換為 mock。使用 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

模組 mock 讓您可以覆寫模組的行為。使用 mock.module(path: string, callback: () => Object) 來 mock 模組。

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 的其他部分一樣,模組 mock 支援 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");
});

Hoisting & preloading

如果您需要確保模組在匯入之前被 mock,您應該使用 --preload 在測試執行之前載入您的 mock。

// 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 Modules,我們已將 patch 新增至 JavaScriptCore,讓 Bun 能夠在執行時覆寫匯出值並遞迴更新即時綁定。

從 Bun v1.0.19 開始,Bun 會自動解析 specifier 參數給 mock.module(),就像您執行了 import 一樣。如果它成功解析,則解析後的 specifier 字串將用作模組快取中的 key。這表示您可以使用相對路徑、絕對路徑,甚至模組名稱。如果 specifier 無法解析,則原始的 specifier 將用作模組快取中的 key。

解析後,mock 模組會儲存在 ES Module 註冊表**和** CommonJS require 快取中。這表示您可以針對 mock 模組交換使用 importrequire

回呼函式只有在模組被匯入或 require 時才會延遲呼叫。這表示您可以使用 mock.module() 來 mock 尚不存在的模組,並且表示您可以使用 mock.module() 來 mock 被其他模組匯入的模組。

使用 mock.restore() 將所有函式 mock 還原為其原始值

與其使用 mockFn.mockRestore() 手動個別還原每個 mock,不如透過呼叫 mock.restore() 以一個指令還原所有 mock。這樣做不會重設使用 mock.module() 覆寫的模組值。

在每個測試檔案的 afterEach 區塊中,甚至在您的 測試 preload 程式碼 中加入 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');
});