Skip to content

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 エイリアス

ittest のエイリアスです。

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 から開始
});

Bun by www.bunjs.com.cn 編集