Bun の組み込みテストランナーは Jest と互換性があり、TypeScript と JSX をサポートしています。このガイドでは、Bun で効果的なテストを記述する方法について説明します。
基本的なテストの構造
テストファイルの配置
Bun は、次のパターンに一致するテストファイルを自動的に検出します。
*.test.{js|jsx|ts|tsx}*_test.{js|jsx|ts|tsx}*.spec.{js|jsx|ts|tsx}*_spec.{js|jsx|ts|tsx}
基本的なテストの構文
ts
import { test, expect } from "bun:test";
test("2 + 2 は 4 に等しい", () => {
expect(2 + 2).toBe(4);
});テストの実行
bash
# すべてのテストを実行
bun test
# 特定のテストファイルを実行
bun test math.test.ts
# パターンに一致するテストを実行
bun test math
# ウォッチモードで実行
bun test --watchテストの記述
test 関数
test 関数は 2 つの形式を受け入れます。
ts
import { test, expect } from "bun:test";
// 基本的な形式
test("テスト名", () => {
// テスト実装
expect(true).toBe(true);
});
// タイムアウト付き
test("非同期テスト", async () => {
await new Promise(resolve => setTimeout(resolve, 100));
}, 5000); // 5 秒タイムアウトdescribe ブロック
関連するテストをグループ化します。
ts
import { describe, test, expect } from "bun:test";
describe("UserService", () => {
describe("createUser", () => {
test("有効なユーザーを作成する", () => {
// 実装
});
test("無効なデータでエラーをスローする", () => {
// 実装
});
});
describe("deleteUser", () => {
test("既存のユーザーを削除する", () => {
// 実装
});
});
});it エイリアス
it は test のエイリアスです。
ts
import { it, expect } from "bun:test";
it("動作するはず", () => {
expect(true).toBe(true);
});期待値(Expect)
基本的なマッチャー
ts
import { test, expect } from "bun:test";
test("基本的なマッチャー", () => {
// 等値
expect(2 + 2).toBe(4);
expect("hello").toBe("hello");
// 等価性
expect({ a: 1 }).toEqual({ a: 1 });
// 真偽値
expect(true).toBeTruthy();
expect(false).toBeFalsy();
// 未定義
expect(undefined).toBeUndefined();
expect(null).toBeDefined();
// null
expect(null).toBeNull();
expect({}).not.toBeNull();
});数値マッチャー
ts
import { test, expect } from "bun:test";
test("数値マッチャー", () => {
expect(5).toBeGreaterThan(3);
expect(5).toBeGreaterThanOrEqual(5);
expect(3).toBeLessThan(5);
expect(3).toBeLessThanOrEqual(5);
expect(0.1 + 0.2).toBeCloseTo(0.3);
});文字列マッチャー
ts
import { test, expect } from "bun:test";
test("文字列マッチャー", () => {
expect("Hello World").toContain("World");
expect("Hello World").toMatch(/world/i);
expect("Hello World").toMatchObject("Hello World");
});配列マッチャー
ts
import { test, expect } from "bun:test";
test("配列マッチャー", () => {
const arr = [1, 2, 3, 4, 5];
expect(arr).toContain(3);
expect(arr).toContainEqual(3);
expect(arr).toHaveLength(5);
expect(arr).toEqual(expect.arrayContaining([1, 2, 3]));
});オブジェクトマッチャー
ts
import { test, expect } from "bun:test";
test("オブジェクトマッチャー", () => {
const user = { name: "John", age: 30, email: "john@example.com" };
expect(user).toHaveProperty("name");
expect(user).toHaveProperty("age", 30);
expect(user).toHaveProperty("name", "John");
expect(user).toMatchObject({ name: "John", age: 30 });
});エラーマッチャー
ts
import { test, expect } from "bun:test";
test("エラーマッチャー", () => {
function throwError() {
throw new Error("Something went wrong");
}
expect(throwError).toThrow();
expect(throwError).toThrow(Error);
expect(throwError).toThrow("Something went wrong");
expect(throwError).toThrow(/went wrong/);
});非同期マッチャー
ts
import { test, expect } from "bun:test";
test("解決される値", async () => {
const promise = Promise.resolve("resolved");
await expect(promise).resolves.toBe("resolved");
await expect(promise).resolves.toMatch("resolved");
});
test("拒否される値", async () => {
const promise = Promise.reject(new Error("error"));
await expect(promise).rejects.toThrow("error");
await expect(promise).rejects.toThrow(Error);
});非同期テスト
async/await
ts
import { test, expect } from "bun:test";
test("非同期関数", async () => {
const result = await fetch("https://api.example.com/data");
const data = await result.json();
expect(data).toHaveProperty("items");
});Promise の返却
ts
import { test, expect } from "bun:test";
test("Promise を返す", () => {
return Promise.resolve("value").then(value => {
expect(value).toBe("value");
});
});コールバック
ts
import { test, expect } from "bun:test";
test("コールバック", done => {
setTimeout(() => {
expect(true).toBe(true);
done();
}, 100);
});モックとスパイ
関数モック
ts
import { test, expect, mock } from "bun:test";
test("関数モック", () => {
const mockFn = mock((x: number) => x * 2);
const result = mockFn(5);
expect(result).toBe(10);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(5);
});モジュールモック
ts
import { test, expect, mock } from "bun:test";
mock.module("./api", () => ({
fetchData: mock(async () => ({ data: "mocked" })),
}));
test("モック API", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
expect(result.data).toBe("mocked");
});spyOn
ts
import { test, expect, spyOn } from "bun:test";
const consoleSpy = spyOn(console, "log");
test("スパイ", () => {
console.log("Hello");
expect(consoleSpy).toHaveBeenCalledWith("Hello");
});ライフサイクルフック
ts
import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test";
describe("データベーステスト", () => {
let db;
beforeAll(async () => {
// テストデータベースに接続
db = await connectToTestDatabase();
});
beforeEach(() => {
// 各テスト前にデータをクリア
clearTestData(db);
});
afterEach(() => {
// 各テスト後にクリーンアップ
cleanup(db);
});
afterAll(async () => {
// データベース接続をクローズ
await db.close();
});
test("ユーザーを作成", () => {
// テスト実装
});
});テストの修正
.each を使用したパラメータ化テスト
ts
import { test, expect } from "bun:test";
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 2, 4],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});.failing を使用した既知の失敗
ts
import { test, expect } from "bun:test";
test.failing("未実装の機能", () => {
// このテストは失敗すると予想されます
throw new Error("未実装");
});.skip を使用したテストのスキップ
ts
import { test, expect } from "bun:test";
test.skip("後で実装するテスト", () => {
// このテストはスキップされます
});
// または
test("スキップされたテスト", () => {
// 実装
}, { skip: true });.only を使用した単一テストの実行
ts
import { test, expect } from "bun:test";
test.only("このテストのみを実行", () => {
// このテストのみが実行されます
expect(true).toBe(true);
});
test("このテストはスキップされます", () => {
// このテストはスキップされます
});スナップショットテスト
ts
import { test, expect } from "bun:test";
test("スナップショット", () => {
const data = { name: "John", age: 30 };
expect(data).toMatchSnapshot();
});
test("インラインスナップショット", () => {
expect({ hello: "world" }).toMatchInlineSnapshot();
});DOM テスト
ts
import { test, expect } from "bun:test";
test("DOM 操作", () => {
const div = document.createElement("div");
div.innerHTML = "<h1>Hello World</h1>";
expect(div.querySelector("h1")?.textContent).toBe("Hello World");
});ベストプラクティス
説明的なテスト名
ts
// 良い
test("ユーザーが有効なメールアドレスで登録できる", () => {
// 実装
});
// 避ける
test("登録テスト", () => {
// 実装
});単一の責任
ts
// 良い:各テストは 1 つのことをテスト
test("ユーザー名が設定される", () => {
const user = createUser({ name: "John" });
expect(user.name).toBe("John");
});
test("メールアドレスが設定される", () => {
const user = createUser({ email: "john@example.com" });
expect(user.email).toBe("john@example.com");
});
// 避ける:複数のことを 1 つのテストで
test("ユーザー作成", () => {
const user = createUser({ name: "John", email: "john@example.com" });
expect(user.name).toBe("John");
expect(user.email).toBe("john@example.com");
expect(user.id).toBeDefined();
expect(user.createdAt).toBeDefined();
// ... 多くの期待値
});テストの独立性
ts
// 良い:各テストは独立
test("テスト 1", () => {
const user = createUser();
expect(user).toBeDefined();
});
test("テスト 2", () => {
const user = createUser(); // 新しいインスタンス
expect(user).toBeDefined();
});
// 避ける:テスト間の共有状態
let sharedUser;
test("テスト 1", () => {
sharedUser = createUser();
});
test("テスト 2", () => {
// sharedUser に依存 - 避ける
expect(sharedUser).toBeDefined();
});AAA パターン
ts
test("AAA パターン", () => {
// Arrange(準備)
const userService = new UserService();
const userData = { name: "John", email: "john@example.com" };
// Act(実行)
const user = userService.createUser(userData);
// Assert(検証)
expect(user.name).toBe("John");
expect(user.email).toBe("john@example.com");
});実用的な例
API テスト
ts
import { test, expect, mock } from "bun:test";
mock.module("./api-client", () => ({
get: mock(async (url: string) => ({
json: async () => ({ data: "mocked" }),
})),
}));
test("API からデータを取得", async () => {
const { get } = await import("./api-client");
const response = await get("/users");
const data = await response.json();
expect(data).toHaveProperty("data", "mocked");
});コンポーネントテスト
tsx
import { test, expect } from "bun:test";
import { render, screen } from "@testing-library/react";
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
test("ボタンをクリック", () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
screen.getByText("Click").click();
expect(handleClick).toHaveBeenCalledTimes(1);
});エラーハンドリングテスト
ts
import { test, expect } from "bun:test";
class UserService {
async getUser(id: string) {
if (!id) {
throw new Error("ID is required");
}
// 実装
}
}
test("無効な ID でエラーをスロー", async () => {
const service = new UserService();
await expect(service.getUser("")).rejects.toThrow("ID is required");
});トラブルシューティング
よくあるエラー
タイムアウト
ts
// タイムアウトエラーを避けるために適切なタイムアウトを設定
test("長時間実行されるテスト", async () => {
await longRunningOperation();
}, 10000); // 10 秒タイムアウト未処理の Promise
ts
// 良い
test("Promise を適切に処理", async () => {
await expect(someAsyncOperation()).resolves.toBe("value");
});
// 避ける
test("未処理の Promise", () => {
someAsyncOperation().then(result => {
expect(result).toBe("value");
});
// テストは Promise が解決する前に完了する可能性があります
});共有状態
ts
// 良い:各テストで状態をリセット
let counter;
beforeEach(() => {
counter = 0;
});
test("テスト 1", () => {
counter++;
expect(counter).toBe(1);
});
test("テスト 2", () => {
counter++;
expect(counter).toBe(1); // 各テストは 0 から開始
});