テスト
既存のテストコードから関数の仕様や期待される動作を理解して、ClaudeCodeはより正確なコードを生成できます。テストを増やすことで渡せる情報が増え、意図に沿った実装が自動生成されやすくなります。弊社では、高速で軽量なbun testを標準的なテストランナーとして使用しています。
弊社の推奨ルール
- Bunテストランナーを標準とする - 追加の依存関係なしで高速に実行できる
- テストファイルは同じ階層に配置 -
.test.tsサフィックスで統一 - モックは最小限に - 外部依存のみモック化し、可能な限り実際の実装を使用
- 日本語でテストケースを記述 - describeとitの説明は日本語で明確に書く
- 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"
}
}