コンテンツにスキップ

仕様書をそのままテストケースで書く:品質の出口はテスト

概要

「品質の出口はテスト。なら仕様書からテストケースで書く」という考え方。仕様書のIF文・条件・期待値をそのままテストコードに落とし込むことで、仕様とテストのズレをなくす。

詳細

従来のアプローチの問題

仕様書 → 実装 → 後からテストを書く

問題点:
- テストは実装を検証するものになり、仕様を検証するものにならない
- 仕様の変更がテストに伝播しにくい
- 「動いた」≠「仕様通り」になりがち

仕様書ベースのテスト設計

仕様書の条件文をそのままテストに変換する。

仕様書の例:

注文API:
- 在庫が足りる場合: 注文を確定し、在庫を減らす
- 在庫が足りない場合: エラーを返す("insufficient stock")
- ユーザーが未認証の場合: 401を返す
- 金額が0以下の場合: バリデーションエラーを返す

テストコードへの変換(Go):

func TestOrderAPI(t *testing.T) {
    tests := []struct {
        name        string
        stock       int
        quantity    int
        authenticated bool
        amount      float64
        wantStatus  int
        wantError   string
    }{
        {
            name:          "在庫が足りる場合: 注文を確定する",
            stock:         10,
            quantity:      5,
            authenticated: true,
            amount:        1000,
            wantStatus:    200,
        },
        {
            name:          "在庫が足りない場合: エラーを返す",
            stock:         3,
            quantity:      5,
            authenticated: true,
            amount:        1000,
            wantStatus:    400,
            wantError:     "insufficient stock",
        },
        {
            name:          "未認証の場合: 401を返す",
            authenticated: false,
            wantStatus:    401,
        },
        {
            name:          "金額が0以下: バリデーションエラー",
            authenticated: true,
            amount:        0,
            wantStatus:    422,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // テスト実行
        })
    }
}

ポイント: テスト名が仕様書の条件文と1対1で対応している。

仕様変更時の追跡

仕様変更: 「在庫が3個以下の場合は警告を出す」が追加

→ テストに新しいケースを追加するだけ:
{
    name:       "在庫が3個以下: 警告フラグをセットする",
    stock:      3,
    wantStatus: 200,
    wantWarning: true,
},

Given-When-Then パターン

// Given: 前提条件
// When: 操作
// Then: 期待結果
func TestOrder_GivenSufficientStock_WhenOrderPlaced_ThenStockDecreased(t *testing.T) {
    // Given
    stock := NewStock(10)
    order := NewOrder(quantity: 3)

    // When
    err := order.Place(stock)

    // Then
    assert.NoError(t, err)
    assert.Equal(t, 7, stock.Remaining())
}

なぜ重要か / いつ使うか

  • 「テストが通っているのに本番でバグる」を防ぐ(仕様とテストが一致していなかった)
  • 仕様書の品質も向上する(テストに書けない仕様は曖昧な仕様)
  • チームで仕様書とテストを一緒にレビューできる
  • リグレッションテストの網羅性を高めたいとき