Interactive

テスト

既存のテストコードから関数の仕様や期待される動作を理解して、ClaudeCodeはより正確なコードを生成できます。テストを増やすことで渡せる情報が増え、意図に沿った実装が自動生成されやすくなります。弊社では、高速で軽量なbun testを標準的なテストランナーとして使用しています。

弊社の推奨ルール

  1. Bunテストランナーを標準とする - 追加の依存関係なしで高速に実行できる
  2. テストファイルは同じ階層に配置 - .test.tsサフィックスで統一
  3. モックは最小限に - 外部依存のみモック化し、可能な限り実際の実装を使用
  4. 日本語でテストケースを記述 - describeとitの説明は日本語で明確に書く
  5. AAAパターンに従う - Arrange(準備)→Act(実行)→Assert(検証)の構造を守る

ClaudeCodeでの利用

「この関数の単体テストをBunで書いて。正常系・境界値・異常系をカバーして」
「外部APIをモックしてサービスクラスのテストを書いて」
「このテストをAAAパターンに整理して、日本語でdescribeを書いて」
「時刻に依存する処理のテストを、DateProviderパターンで書いて」

曖昧なテストではなくAAAパターンを使う

テストの意図を明確にするため、準備・実行・検証の3段階で構造化します。

// ❌ 避ける:曖昧なテスト
import { describe, it, expect } from "bun:test"

describe("計算", () => {
  it("works", () => {
    expect(calculateTotal(1000, 0.1)).toBe(1100) // 何をテストしているか不明
  })
})

AAAパターンでは、Arrange(準備)、Act(実行)、Assert(検証)の3段階でテストを構造化します。テストの意図が明確になり、可読性が向上します。

// ✅ 推奨:AAAパターン
describe("合計金額計算", () => {
  it("税込み価格を正しく計算する", () => {
    // Arrange - テストデータの準備
    const price = 1000
    const taxRate = 0.1
    
    // Act - 処理の実行
    const result = calculateTotal(price, taxRate)
    
    // Assert - 結果の検証
    expect(result).toBe(1100)
  })

小数点以下の処理など、境界値のテストも同様のAAA構造で記述します。

  it("小数点以下を適切に処理する", () => {
    // Arrange
    const price = 333
    const taxRate = 0.1
    
    // Act
    const result = calculateTotal(price, taxRate)
    
    // Assert - 期待値を明確に
    expect(result).toBe(366) // 333 * 1.1 = 366.3 → 366
  })
})

throwエラーではなくRejectsを使う

非同期処理のエラーテストは、rejectsで型安全に検証します。

// ❌ 避ける:throwでのエラーテスト
describe("APIクライアント", () => {
  it("エラーを投げる", async () => {
    try {
      await fetchData("invalid")
      throw new Error("Should have thrown") // テストが失敗すべき
    } catch (error) {
      expect(error.message).toBe("Not Found")
    }
  })
})

// ✅ 推奨:rejectsでエラー検証
describe("APIクライアント", () => {
  it("データを正常に取得する", async () => {
    // Arrange
    const userId = "user-123"
    
    // Act & Assert
    const data = await fetchData(userId)
    expect(data.id).toBe(userId)
  })
  
  it("無効なIDでエラーを投げる", async () => {
    // Arrange
    const invalidId = "invalid"
    
    // Act & Assert
    await expect(fetchData(invalidId)).rejects.toThrow("Not Found")
  })
})

本物の依存ではなくモックを使う

外部依存(DB、API等)はテスト用の実装に置き換えてテストを高速化します。

// ❌ 避ける:本物のDBに依存
describe("ユーザーサービス", () => {
  it("ユーザー情報を取得する", async () => {
    // 本物のDBに接続(遅い、不安定)
    const service = new UserService(new PostgresRepository())
    const user = await service.getUser("1") // DB接続が必要
    expect(user.name).toBe("太郎")
  })
})

外部依存(DB、API等)はモック実装に置き換えてテストを高速化します。モッククラスはインターフェースを実装し、テスト用のヘルパーメソッドも提供します。

// ✅ 推奨:モック実装を使用
type UserRepository = {
  findById(id: string): Promise<User | null>
}

class MockUserRepository implements UserRepository {
  private users = new Map<string, User>()
  
  async findById(id: string) {
    return this.users.get(id) || null
  }
  
  // テスト用のヘルパーメソッド
  setUser(user: User) {
    this.users.set(user.id, user)
  }
}

モックを使用したテストは、依存性注入でモックを渡して高速に実行できます。

describe("ユーザーサービス", () => {
  it("ユーザー情報を取得する", async () => {
    // Arrange - モックの準備
    const repository = new MockUserRepository()
    repository.setUser({ id: "1", name: "太郎" })
    
    // Act - モックを注入してテスト
    const service = new UserService(repository)
    const user = await service.getUser("1")
    
    // Assert
    expect(user.name).toBe("太郎")
  })
})

よくある問題

テストが不安定(flaky)になる

時刻、ランダム性、非同期処理の競合でテストが不安定になる問題です。時刻は固定値を使用し、非同期処理は必ずawaitすることで解決できます。

// ❌ 不安定:実際の時刻に依存
describe("期限チェック", () => {
  it("期限切れを判定する", () => {
    const checker = new ExpirationChecker()
    const yesterday = new Date(Date.now() - 86400000) // 昨日
    expect(checker.isExpired(yesterday)).toBe(true) // 時刻によって失敗する
  })
})

// ✅ 安定:固定時刻を使用
class FixedDateProvider {
  constructor(private readonly date: Date) {}
  now() { return this.date }
}

describe("期限チェック", () => {
  it("期限切れを正しく判定する", () => {
    // Arrange
    const provider = new FixedDateProvider(new Date("2024-01-01"))
    const checker = new ExpirationChecker(provider)
    
    // Act & Assert
    const isExpired = checker.check(new Date("2023-12-31"))
    expect(isExpired).toBe(true)
  })
})

モックが複雑になりすぎる

テスト対象が密結合でモックが複雑になる問題です。インターフェースを導入して依存性注入を可能にすることで解決できます。

// ❌ テスト困難:密結合
class OrderService {
  async createOrder(data: OrderData) {
    const db = new Database() // 内部で生成
    const email = new EmailService() // テストできない
    
    const order = await db.save(data)
    await email.send(order.customerEmail, 'Order created')
    return order
  }
}

密結合なコードはモックが複雑になります。インターフェースを導入して依存性注入を可能にし、テストしやすい設計に変更します。

// ✅ テスト可能:依存性注入
interface Database {
  save(data: OrderData): Promise<Order>
}

interface EmailService {
  send(to: string, message: string): Promise<void>
}

サービスクラスはコンストラクタで依存を受け取り、テスト時にモックを注入できるようにします。

class OrderService {
  constructor(
    private readonly db: Database,
    private readonly email: EmailService
  ) {}
  
  async createOrder(data: OrderData) {
    const order = await this.db.save(data)
    await this.email.send(order.customerEmail, 'Order created')
    return order
  }
}

// テストでモックを注入
const mockDB = { save: async (data) => ({ ...data, id: '123' }) }
const mockEmail = { send: async () => {} }
const service = new OrderService(mockDB, mockEmail)

テストの実行が遅い

実際のI/O処理でテストが遅くなる問題です。外部APIやデータベースアクセスはモックに置き換えることで解決できます。

// ❌ 遅い:実際のAPIを呼び出し
describe("ユーザー取得", () => {
  it("APIからユーザーを取得する", async () => {
    const service = new UserService(new HttpClient()) // 実際のHTTP通信
    const user = await service.getUser("123") // ネットワーク通信で遅い
    expect(user.id).toBe("123")
  })
})

実際のI/O処理はテストを遅くし、不安定にします。モックで固定レスポンスを返すことで、高速で信頼性の高いテストが実現できます。

// ✅ 速い:モックでレスポンスを固定
const mockClient = {
  get: async (url: string) => ({ id: "123", name: "テスト太郎" })
}

describe("ユーザー取得", () => {
  it("APIからユーザーを取得する", async () => {
    const service = new UserService(mockClient)
    const user = await service.getUser("123") // 即座に完了
    expect(user.id).toBe("123")
  })
})

Bunテストが認識されない

bun testコマンドでテストが実行されない問題です。package.jsonにテストスクリプトを追加することで解決できます。

{
  "scripts": {
    "test": "bun test",
    "test:watch": "bun test --watch"
  }
}