Skip to content

モックは、依存関係を制御された実装に置き換えることで、テストに不可欠です。Bun は、関数モック、スパイ、モジュールモックを含む包括的なモック機能を提供します。

基本的な関数モック

mock 関数を使用してモックを作成します。

ts
import { test, expect, mock } from "bun:test";

const random = mock(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

Jest 互換性

または、Jest と同様に jest.fn() 関数を使用できます。これは同じように動作します。

ts
import { test, expect, jest } from "bun:test";

const random = jest.fn(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

モック関数のプロパティ

mock() の結果は、いくつかの追加プロパティで装飾された新しい関数です。

ts
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 }
//  ]

利用可能なプロパティとメソッド

次のプロパティとメソッドがモック関数に実装されています。

プロパティ/メソッド説明
mockFn.getMockName()モック名を返します
mockFn.mock.calls各呼び出しの引数配列
mockFn.mock.results各呼び出しの戻り値配列
mockFn.mock.instances各呼び出しの this コンテキスト配列
mockFn.mock.contexts各呼び出しの this コンテキスト配列
mockFn.mock.lastCall最新の呼び出しの引数
mockFn.mockClear()呼び出し履歴をクリアします
mockFn.mockReset()呼び出し履歴をクリアし、実装を削除します
mockFn.mockRestore()元の実装を復元します
mockFn.mockImplementation(fn)新しい実装を設定します
mockFn.mockImplementationOnce(fn)次の呼び出しのみ実装を設定します
mockFn.mockName(name)モック名を設定します
mockFn.mockReturnThis()戻り値を this に設定します
mockFn.mockReturnValue(value)戻り値を設定します
mockFn.mockReturnValueOnce(value)次の呼び出しのみ戻り値を設定します
mockFn.mockResolvedValue(value)解決された Promise 値を設定します
mockFn.mockResolvedValueOnce(value)次の呼び出しのみ解決された Promise を設定します
mockFn.mockRejectedValue(value)拒否された Promise 値を設定します
mockFn.mockRejectedValueOnce(value)次の呼び出しのみ拒否された Promise を設定します
mockFn.withImplementation(fn, callback)一時的に実装を変更します

実用的な例

基本的なモックの使用

ts
import { test, expect, mock } from "bun:test";

test("モック関数の動作", () => {
  const mockFn = mock((x: number) => x * 2);

  // モックを呼び出す
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // 呼び出しを検証
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // 結果を確認
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // 呼び出し履歴を検証
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

動的モック実装

ts
import { test, expect, mock } from "bun:test";

test("動的モック実装", () => {
  const mockFn = mock();

  // 異なる実装を設定
  mockFn.mockImplementationOnce(() => "first");
  mockFn.mockImplementationOnce(() => "second");
  mockFn.mockImplementation(() => "default");

  expect(mockFn()).toBe("first");
  expect(mockFn()).toBe("second");
  expect(mockFn()).toBe("default");
  expect(mockFn()).toBe("default"); // デフォルト実装を使用
});

非同期モック

ts
import { test, expect, mock } from "bun:test";

test("非同期モック関数", async () => {
  const asyncMock = mock();

  // 解決された値をモック
  asyncMock.mockResolvedValueOnce("first result");
  asyncMock.mockResolvedValue("default result");

  expect(await asyncMock()).toBe("first result");
  expect(await asyncMock()).toBe("default result");

  // 拒否された値をモック
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock error"));

  await expect(rejectMock()).rejects.toThrow("Mock error");
});

spyOn() を使用したスパイ

関数をモックに置き換えずに、関数の呼び出しを追跡できます。spyOn() を使用してスパイを作成します。これらのスパイは .toHaveBeenCalled() および .toHaveBeenCalledTimes() に渡すことができます。

ts
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);
});

高度なスパイの使用

ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // 元の実装
    return { id, name: `User ${id}` };
  }

  async saveUser(user: any) {
    // 元の実装
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // 各テスト後にすべてのスパイを復元
  jest.restoreAllMocks();
});

test("サービスメソッドをスパイ", async () => {
  // 実装を変更せずにスパイ
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // サービスを通常どおり使用
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // 呼び出しを検証
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("モック実装でスパイ", async () => {
  // スパイして実装を上書き
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "Mocked User",
  });

  const result = await userService.getUser("123");

  expect(result.name).toBe("Mocked User");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});

mock.module() を使用したモジュールモック

モジュールモックを使用すると、モジュールの動作を上書きできます。mock.module(path: string, callback: () => Object) を使用してモジュールをモックします。

ts
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() を呼び出すだけで、モジュールは上書きされます。

ts
import { test, expect, mock } from "bun:test";

// モックするモジュールはここにあります。
import { foo } from "./module";

test("mock.module", async () => {
  const cjs = require("./module");
  expect(foo).toBe("bar");
  expect(cjs.foo).toBe("bar");

  // ここで更新します。
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // ライブバインディングが更新されます。
  expect(foo).toBe("baz");

  // モジュールは CJS 用にも更新されます。
  expect(cjs.foo).toBe("baz");
});

ホイスティングとプリロード

インポート前にモジュールがモックされることを確認する必要がある場合は、--preload を使用してテスト実行前にモックを読み込む必要があります。

ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
bash
bun test --preload ./my-preload

生活を楽にするために、bunfig.toml にプリロードを配置できます。

toml
[test]
# テスト前にこれらのモジュールを読み込みます。
preload = ["./my-preload"]

モジュールモックのベストプラクティス

プリロードを使用するタイミング

すでにインポートされたモジュールをモックするとどうなりますか?

すでにインポートされたモジュールをモックすると、モジュールはモジュールキャッシュで更新されます。これは、モジュールをインポートするすべてのモジュールがモックバージョンを取得することを意味しますが、元のモジュールは評価されたままです。つまり、元のモジュールからの副作用はすべて発生したままです。

元のモジュールが評価されるのを防ぎたい場合は、--preload を使用してテスト実行前にモックを読み込む必要があります。

実用的なモジュールモックの例

ts
import { test, expect, mock, beforeEach } from "bun:test";

// API クライアントモジュールをモック
mock.module("./api-client", () => ({
  fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
  createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
  updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));

test("モック API を使用したユーザーサービス", async () => {
  const { fetchUser } = await import("./api-client");
  const { UserService } = await import("./user-service");

  const userService = new UserService();
  const user = await userService.getUser("123");

  expect(fetchUser).toHaveBeenCalledWith("123");
  expect(user.name).toBe("User 123");
});

外部依存のモック

ts
import { test, expect, mock } from "bun:test";

// 外部データベースライブラリをモック
mock.module("pg", () => ({
  Client: mock(function () {
    return {
      connect: mock(async () => {}),
      query: mock(async (sql: string) => ({
        rows: [{ id: 1, name: "Test User" }],
      })),
      end: mock(async () => {}),
    };
  }),
}));

test("データベース操作", async () => {
  const { Database } = await import("./database");
  const db = new Database();

  const users = await db.getUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("Test User");
});

グローバルモック関数

すべてのモックをクリア

すべてのモック関数の状態(呼び出し、結果など)を、元の実装を復元せずにリセットします。

ts
import { expect, mock, test } from "bun:test";

const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());

test("すべてのモックをクリア", () => {
  random1();
  random2();

  expect(random1).toHaveBeenCalledTimes(1);
  expect(random2).toHaveBeenCalledTimes(1);

  mock.clearAllMocks();

  expect(random1).toHaveBeenCalledTimes(0);
  expect(random2).toHaveBeenCalledTimes(0);

  // 注意:実装は保持されます
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});

これにより、すべてのモックの .mock.calls.mock.instances.mock.contexts.mock.results プロパティがリセットされますが、mock.restore() とは異なり、元の実装は復元されません。

すべてのモックを復元

各モックを mockFn.mockRestore() で個別に復元する代わりに、mock.restore() を呼び出してすべてのモックを 1 つのコマンドで復元します。これにより、mock.module() で上書きされたモジュールの値はリセットされません。

ts
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");
});

mock.restore() を使用すると、各テストファイルの afterEach ブロックまたはテストプリロードコードに追加することで、テストのコード量を削減できます。

Vitest 互換性

Vitest 用に記述されたテストとの互換性を高めるために、Bun は Jest モック API の一部に対するエイリアスとして vi グローバルオブジェクトを提供します。

ts
import { test, expect } from "bun:test";

// Vitest と同様に 'vi' エイリアスを使用
test("vitest 互換性", () => {
  const mockFn = vi.fn(() => 42);

  mockFn();
  expect(mockFn).toHaveBeenCalled();

  // 次の関数が vi オブジェクトで利用可能です。
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.clearAllMocks
});

これにより、すべてのモックを書き換えることなく、Vitest から Bun にテストを移植しやすくなります。

実装の詳細

mock.module() の仕組みを理解すると、より効果的に使用できます。

キャッシュとの相互作用

モジュールモックは、ESM と CommonJS の両方のモジュールキャッシュと相互作用します。

遅延評価

モックファクトリコールバックは、モジュールが実際にインポートまたは要求された場合にのみ評価されます。

パス解決

Bun は、インポートを行うかのようにモジュール指定子を自動的に解決し、次をサポートします。

  • 相対パス('./module'
  • 絶対パス('/path/to/module'
  • パッケージ名('lodash'

インポートタイミングの影響

  • 最初のインポート前にモックする場合: 元のモジュールからの副作用は発生しません
  • インポート後にモックする場合: 元のモジュールの副作用はすでに発生しています

このため、副作用を防ぐ必要があるモックには --preload の使用が推奨されます。

ライブバインディング

モックされた ESM モジュールはライブバインディングを維持するため、モックを変更するとすべての既存のインポートが更新されます。

高度なパターン

ファクトリ関数

ts
import { mock } from "bun:test";

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "Mock User",
    email: "mock@example.com",
    ...overrides,
  };
}

const mockUserService = {
  getUser: mock(async (id: string) => createMockUser({ id })),
  createUser: mock(async (data: any) => createMockUser(data)),
  updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};

条件付きモック

ts
import { test, expect, mock } from "bun:test";

const shouldUseMockApi = process.env.NODE_ENV === "test";

if (shouldUseMockApi) {
  mock.module("./api", () => ({
    fetchData: mock(async () => ({ data: "mocked" })),
  }));
}

test("条件付き API 使用", async () => {
  const { fetchData } = await import("./api");
  const result = await fetchData();

  if (shouldUseMockApi) {
    expect(result.data).toBe("mocked");
  }
});

モッククリーンアップパターン

ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // 一般的なモックを設定
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // すべてのモックをクリーンアップ
  mock.restore();
  mock.clearAllMocks();
});

ベストプラクティス

モックをシンプルに保つ

ts
// 良い:シンプルで焦点を絞ったモック
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test User" })),
};

// 避ける:過度に複雑なモック動作
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ... 多くの複雑なロジック
});

型安全なモックを使用する

ts
interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "Test User" })),
  createUser: mock(async data => ({ id: "new-id", ...data })),
};

モックの動作をテストする

ts
test("サービスが API を正しく呼び出す", async () => {
  const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };

  const service = new UserService(mockApi);
  await service.getUser("123");

  // モックが正しく呼び出されたことを検証
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

注意事項

自動モック

__mocks__ ディレクトリと自動モックはまだサポートされていません。これが Bun への移行を妨げている場合は、イシューを登録してください

ESM 対 CommonJS

モジュールモックは、ESM と CommonJS モジュールに対して異なる実装を持っています。ES モジュールの場合、Bun は JavaScriptCore にパッチを追加して、Bun が実行時にエクスポート値を上書きし、ライブバインディングを再帰的に更新できるようにしています。

Bun by www.bunjs.com.cn 編集