本文へスキップ

テスト駆動開発

鉄の法則:失敗するテストなしにプロダクションコードを書いてはならない。


なぜTDDは交渉の余地がないのか

Test-Driven Development(TDD)はソフトウェアエンジニアリングで最も頻繁に議論され、最も頻繁にスキップされるプラクティスです。開発者はテストファーストが遅い、特定の種類のコードには不自然だ、締め切りが近すぎる、後でテストを追加すると主張します。

Superpowersはこれらすべての主張を合理化として扱い、テストファースト規律をスキップする許容可能な理由として扱いません。

コードファーストで書く場合の核心的な問題は:テストの前にコードを書くと、テストはコードに合わせて書かれ、意図を検証するものではありません。 最終的にコードが何をするかをテストすることになり、何をすべきかをテストしません。テストは正確性の保証ではなく、ドキュメントの成果物になります。元のコードに存在していたバグが期待される動作としてテストにエンコードされます。

テストを先に書くと:

  • 実装方法を決める前に、望む正確な動作を指定することが強制されます
  • 実装を構築する前にインターフェースの問題を発見できます(修正が安価)
  • 失敗するテストはテストが実際に何かをテストしていることの証明です
  • 実装後の通過するテストは意図したものを構築したことの証明です

TDDは遅くありません。遅い部分の排除です:適切に指定されなかったコードのデバッグに費やす時間。


RED-GREEN-REFACTORサイクル

TDDはすべての機能について単純な3フェーズサイクルに従います:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   RED ──────────────────→ GREEN ──────────────→ REFACTOR   │
│                                                             │
│   必要な動作の          最も単純なコードで      動作を変えず │
│   失敗するテストを      通過させる              コードを     │
│   書く                                         改善する     │
└─────────────────────────────────────────────────────────────┘

RED:失敗するテストを書く

正確に一つの必要な動作を記述するテストを書いてください。実行してください。失敗するのを確認してください。

このステップはオプションではありません。 テストを書いて実装なしにすぐに通過した場合、2つのことのどちらかが当てはまります:

  1. 機能がすでに存在する(なぜかを確認すべき)
  2. テストが間違っている—テストしていると思うものを実際にはテストしていない

実装前に通過するテストは価値がありません。REDフェーズはテストが本物であることを確認するために存在します。

// RED: calculateDiscountがまだ存在しないため、このテストは失敗する
test('$100を超える注文に10%の割引を適用する', () => {
  const order = { total: 150, items: [] };
  expect(calculateDiscount(order)).toBe(15);
});

GREEN:シンプルに通過させる

失敗するテストを通過させる最も単純なコードを書いてください。最もエレガントなコードではありません。最も拡張可能なコードではありません。最も単純なコードです。

このルールは実装フェーズでの過度な設計を防ぎます。まだテストのないケースのために構築することは許可されていません。「20%の割引は$500を超える注文についてはどうか?」と思ったら—まずそのテストを書き、それから通過させてください。

// GREEN: テストを通過させる最も単純な実装
function calculateDiscount(order: Order): number {
  if (order.total > 100) {
    return order.total * 0.10;
  }
  return 0;
}

REFACTOR:壊さずに改善する

テストが通過したら、セーフティネットができました。今、コードを改善できます:より良い命名、ヘルパー関数の抽出、重複の削除、読みやすさの向上—動作を変えずに品質を向上させるもの。

リファクタリングのたびにテストを実行してください。まだ通過していれば、何も壊していません。テストが失敗した場合、最後の変更を元に戻してください。

// REFACTOR: より明確な命名と定数
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.10;

function calculateDiscount(order: Order): number {
  const qualifiesForDiscount = order.total > DISCOUNT_THRESHOLD;
  return qualifiesForDiscount ? order.total * DISCOUNT_RATE : 0;
}

テストはまだ通過しています。コードはよりクリーンです。サイクルが完了しました。


テストの前にコードが書かれた場合の対処法

この状況は発生します。AIエージェントはテストの前に実装コードを書きます。締め切りのプレッシャーにある開発者もそうします。問題は発生したときに何をするかです。

Superpowersは明確です:

削除してください。最初からやり直してください。

「今テストを追加する」ではありません。「それでほぼ十分だ」でもありません。実装コードを削除し、テストファーストで書いてください。

これがなぜ厳しいかは理由を理解するまでそう見えます。失敗するテストなしに書かれたコードは検証された仕様を持ちません。実装は挑戦を受けていない仮定をエンコードしています。事後にテストを書くとき、通過することがわかっているテストを書いています—つまり、何も発見していません。コードがコードがすることを確認しているだけです。

テストファースト規律は単にテストを持つことではありません。実装する前に動作を指定するというプロセスです。後付けのテストはこのステップをスキップします。

IF: プロダクションコードが先行する失敗するテストなしに書かれた
THEN:
  1. プロダクションコードを削除する
  2. 削除をcommitする
  3. 失敗するテストを書く
  4. テストに対して実装する
  5. 続ける

合理化テーブル

TDDをスキップするプレッシャーが高まるとき、これらは一般的な議論とそれが成立しない理由です:

言い訳現実
「後でテストを追加する」後から追加されたテストは、コードがすべきことではなく、コードがすることをテストします。「後で」がくることは稀です。
「このコードはシンプルすぎてテストは必要ない」シンプルなコードにもバグがあります。テストは2分かかります。
「明日が締め切りだ」速く提供された未テストのコードは、テストを書く時間よりも修正に時間がかかるバグを生み出します。
「この種のコードにはテストを書けない」書けます。テストが本当に難しい場合、それはコード設計が間違っているシグナルです。
「最初に探索的コーディングをして、後で形式化する」探索的コードはプロダクションコードになります。削除するかテストするか—第3の選択肢はありません。
「AIが生成したので、おそらく大丈夫だ」AI生成コードにはバグがあります。テストなしのAI生成コードには未検出のバグがあります。
「テストの追加は今は遅くなる」プロダクションでバグが見つかった後にテストを追加することははるかに遅いです。
「これをテストするには多くのものをmockする必要がある」多くのmockは設計に依存関係が多すぎることを意味します。設計を修正してください。

完了前の確認

機能が完了したと主張する前に、Superpowers確認プロトコルは5ステップのゲートを必要とします:

┌────────────────────────────────────────────────────────────────┐
│                  VERIFICATION GATE                             │
│                                                                │
│  ステップ1:IDENTIFY — この機能に関連するすべてのテストをリスト │
│  ステップ2:RUN      — テストスイートを実行する(仮定しない)  │
│  ステップ3:READ     — 実際の出力を1行ずつ読む                │
│  ステップ4:VERIFY   — 各テストが出力で通過していることを確認  │
│  ステップ5:CLAIM    — 今初めて機能が完了したと述べる         │
└────────────────────────────────────────────────────────────────┘

ステップ1:IDENTIFY

完了する機能をカバーするすべてのテストファイルとテストケースをリストしてください。今日書いたテストだけでなく—影響を受けるコードに触れる既存のテストも。

ステップ2:RUN

実際にテストスイートを実行してください。頭の中でではありません。仮定ではありません。実行してください。

npm test -- --coverage --testPathPattern=order

ステップ3:READ

実際の出力を読んでください。すべての行を。「全体的に通過したか失敗したか」だけではありません。以下を確認してください:

  • 実行されたテスト
  • 通過したテスト
  • スキップされたテスト
  • カバレッジの数値

ステップ4:VERIFY

IDENTIFYリストの各テストについて、それが出力に表示されて通過として表示されていることを確認してください。テストが出力にない場合、実行されていません。テストが失敗している場合、機能は完了していません。

ステップ5:CLAIM

ステップ1〜4を完了した後のみ、AI(または開発者)は「この機能は完了です」と述べることができます。

「動くはずだ」と言うことはステップ5を完了することではありません。 実際のテスト出力のみがステップ5を完了します。


停止を必要とするレッドフラグ

TDD中にこれらの状況が発生した場合、即座に作業を停止してエスカレーションしてください:

レッドフラグ何を意味するか
テストが実装前に通過するテストがおそらく間違っている
テストは通過するが機能が正しく動作しないテストが間違ったものをテストしている
大きなリファクタリング後に初回実行でスイートのすべてのテストが通過する疑わしい—テストが意図せずスキップされていないか確認する
新しい機能を追加するときにテストカバレッジが下がるギャップが導入されている
テストが通過するために15の他のテストを変更する必要がある設計変更が大きすぎた;より小さなステップに分解する
AIがテスト出力を表示せずにテストが通過すると主張する許容不可能—続行前に実際の出力を要求する

Superpowers計画のコンテキストにおけるTDD

計画の作成に記載されているように、計画のすべての実装タスクの前にテストタスクが来なければなりません。計画構造は計画レベルでTDDを強制します:

タスクN:   [動作]の失敗するテストを書く     ← RED
タスクN+1: [動作]を実装する                 ← GREEN
[REFACTORはタスクN+1内またはスコープが必要な場合はタスクN+2として行われる]

計画を実行するとき、subagentがタスクN(テスト)が完了する前にタスクN+1(実装)にディスパッチされた場合、subagentはタスクを拒否してBLOCKEDとして報告しなければなりません。先行する失敗するテストのない実装タスクは計画の欠陥です。


SuperpowersでTDDを実行する

TDDスキルを明示的に呼び出すには:

/test-driven-development ユーザー認証を実装する必要があります

AIはRED-GREEN-REFACTORサイクルを通じてガイドし、失敗するテストファーストの要件を強制し、何かを完了とマークする前に5ステップの確認ゲートを適用します。


TDDはコードが動くと信じることと知ることの違いです。 プロフェッショナルなソフトウェア開発において、信念は許容可能な基準ではありません。証拠です。