モックは、依存関係を制御された実装に置き換えることで、テストに不可欠です。Bun は、関数モック、スパイ、モジュールモックを含む包括的なモック機能を提供します。
基本的な関数モック
mock 関数を使用してモックを作成します。
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() 関数を使用できます。これは同じように動作します。
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() の結果は、いくつかの追加プロパティで装飾された新しい関数です。
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) | 一時的に実装を変更します |
実用的な例
基本的なモックの使用
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 },
]);
});動的モック実装
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"); // デフォルト実装を使用
});非同期モック
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() に渡すことができます。
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);
});高度なスパイの使用
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) を使用してモジュールをモックします。
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 の他の部分と同様に、モジュールモックは import と require の両方をサポートします。
すでにインポートされたモジュールの上書き
すでにインポートされたモジュールを上書きする必要がある場合、特別なことは必要ありません。mock.module() を呼び出すだけで、モジュールは上書きされます。
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 を使用してテスト実行前にモックを読み込む必要があります。
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preload生活を楽にするために、bunfig.toml にプリロードを配置できます。
[test]
# テスト前にこれらのモジュールを読み込みます。
preload = ["./my-preload"]モジュールモックのベストプラクティス
プリロードを使用するタイミング
すでにインポートされたモジュールをモックするとどうなりますか?
すでにインポートされたモジュールをモックすると、モジュールはモジュールキャッシュで更新されます。これは、モジュールをインポートするすべてのモジュールがモックバージョンを取得することを意味しますが、元のモジュールは評価されたままです。つまり、元のモジュールからの副作用はすべて発生したままです。
元のモジュールが評価されるのを防ぎたい場合は、--preload を使用してテスト実行前にモックを読み込む必要があります。
実用的なモジュールモックの例
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");
});外部依存のモック
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");
});グローバルモック関数
すべてのモックをクリア
すべてのモック関数の状態(呼び出し、結果など)を、元の実装を復元せずにリセットします。
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() で上書きされたモジュールの値はリセットされません。
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 グローバルオブジェクトを提供します。
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 モジュールはライブバインディングを維持するため、モックを変更するとすべての既存のインポートが更新されます。
高度なパターン
ファクトリ関数
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 })),
};条件付きモック
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");
}
});モッククリーンアップパターン
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// 一般的なモックを設定
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// すべてのモックをクリーンアップ
mock.restore();
mock.clearAllMocks();
});ベストプラクティス
モックをシンプルに保つ
// 良い:シンプルで焦点を絞ったモック
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);
}
// ... 多くの複雑なロジック
});型安全なモックを使用する
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 })),
};モックの動作をテストする
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 が実行時にエクスポート値を上書きし、ライブバインディングを再帰的に更新できるようにしています。