💧

【Golang】AI駆動×テスト駆動×テーブル駆動

に公開

はじめに

LLMによるコード生成が当たり前になった今、エンジニアの課題にどう書くかに加えてどう正しさを担保するかが重要視されるようになってきているのかなと思います。正しさの担保をするためには、AIの特徴を掴み厳密なルールの制定や評価指標の設定が重要になってきます。
今回は、今参加しているチームで行っているテーブル駆動(TDT)、テスト駆動(TDD)、AI駆動(AI-Driven)を組み合わせた、開発フローについての記事です。チームは昨年の12月に結成されたプロダクトの立ち上げフェーズなこともあり、最初からAI駆動を行うための技術選定を行ってきました。
その中で、なぜこの三つの組み合わせの相性がいいのかを述べられればと思います。

前提

テーブル駆動(Table-Driven Tests)とは

Go言語などで広く採用されているテスト手法です。テストケースを入力値と期待値のペアとして構造体のスライスに定義し、それらをループで回して一括検証します。テスト対象のロジックと具体的なデータを切り離せるため、ケースの追加や修正が容易になるのが特徴です。

参考になりそうな記事
https://qiita.com/ryo_manba/items/242f629e0b3593879c6d

テスト駆動(Test-Driven Development)とは

実際のプログラムを記述する前にテストコードを書き、そのテストをパスさせるための最小限の実装を行い、最後にコードを整理するというサイクルを繰り返す開発手法です。常にテストが通る状態を維持することで、意図しないデグレを防ぎながら開発を進められます。

参考になりそうな記事
https://qiita.com/MatsudaSaku/items/61ed504b8fd91a191f48

AI駆動(AI-Driven)とは

開発のプロセスにAIを深く組み込み、コード生成やテスト作成、リファクタリングをAIに主導させる考え方です。
たとえばClaude Codeのようなツールでは、skillはファイルの読み書きやテスト実行といった具体的な操作能力を指します。commandはエンジニアがAIに対して出す指示の最小単位です。そしてagentは、複数のスキルを自律的に組み合わせて、複雑な目標を達成するために思考し行動する実行主体を意味します。

参考になりそうな記事
https://zenn.dev/yahsan2/articles/claude-code-game-analogy

3つの駆動が噛み合う理由

なぜこの3つを組み合わせるのかというと、AIの弱点をテストが補い、テストの面倒さをAIが解決する相互補完の関係にあるからです。

  1. テーブル駆動(Table-Driven Tests): AIへの仕様書
    Goで一般的なテーブル駆動テストは、入力と期待値を構造体のスライスで定義します。
    この構造化されたパターンは、AIにとって理解しやすい仕様書となります。

  2. テスト駆動(Test-Driven Development): AIへのガードレール
    AIに実装を丸投げする前に、まずテストを書く(あるいはAIに書かせる)。
    先に正解の判定基準を作ることで、AIが生成したコードが正しいかどうかを、人間が一行ずつレビューする代わりにマシンがマシンを検品する状態を作ります。

  3. AI駆動(AI-Driven): 反復からの解放
    テストケースの列挙やボイラープレートの記述は、従来はとても手間でした。
    AIはここを得意とします。この関数に必要そうな異常系ケースを5つ追加してと指示するだけで、テストコードがみるみる充実していきます。

実践ワークフロー:Goによる実装例

例えば、シンプルなユーザーの年齢に応じた割引率計算(CalculateDiscount)を実装するとします。

STEP 1: AIにテーブル駆動テストの骨組みを作らせる (Red)

まずは実装(ロジック)を空にして、テストだけをAIに生成させます。

// 期待するテストコードのイメージ
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name string
        age  int
        want float64
    }{
        {name: "子供", age: 10, want: 0.5},
        {name: "成人", age: 25, want: 0.0},
        {name: "シニア", age: 65, want: 0.3},
        // ここにエッジケース(負の数、150歳など)をAIに追加させる
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := CalculateDiscount(tt.age); got != tt.want {
                t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
            }
        })
    }
}

STEP 2: テストをパスする最小限の実装をAIに書かせる (Green)

次に、このテストスイートをAIに渡しこのテストをすべてパスするコードを書いてと指示します。
実際はスキルを組み合わせてコマンド一発で動くようにすることが多いと思います。

STEP 3: テストを維持したままリファクタリング (Refactor)

最後に、計算ロジックをストラテジーパターンに変更してといった指示をAIに出します。
すでにテーブル駆動テストというセーフティネットがあるため、AIがロジックを壊してもすぐに検知でき、安心して大胆な修正を依頼できます。

メリット

  • レビューコストの激減:コードの動きを一行ずつ追う必要はありません。テストが通っていることを確認し、テーブルの中身(仕様)が妥当かを見るだけで済みます。
  • ドキュメントとしての価値:テーブル駆動テスト自体が、そのままこの関数は何をすべきかのドキュメントになります。

まとめ

テーブル駆動で型を作り、テスト駆動で品質を担保し、AI駆動で速度を出す。testに関して厳密にルールを決めておけば、効率的な実装と認知不可を低く人間のレビューができそうです。

Discussion