テスト駆動開発(TDD)は、次の小さな振る舞いをテストで表し、期待した理由で失敗させ、最小実装で通し、動作を保って整える反復です。 完成したコードに後からテストを足すことだけを指すのではありません。
なぜRedから始めるか
最初に失敗を確認すると、テストが対象コードを実際に検査していること、失敗メッセージが意図した要求を表すことを確かめられます。最初からGreenなら、既に実装済みなのか、テストが実行されていないのか、誤った条件なのか区別できません。
Redは「何でもよいから失敗」ではなく、次に実装したい振る舞いがないために失敗する状態です。
登場人物と対象
実装者、ペアやレビュー担当、要求を決める担当が関わります。対象は入力と出力が明確な計算、変換、バリデーション、業務ルールから始めると理解しやすいです。外部APIやDBは境界を分け、テストダブルの必要性と実環境での結合テストを別に考えます。
一周の流れ

- 次の振る舞いを一文で決める。
- 最小のテストを一つ書く。
- 実行し、期待した理由で失敗するか読む。
- そのテストを通す最小の実装を書く。
- 全テストがGreenであることを確認する。
- 重複、名前、責任を整え、再度全テストを実行する。
練習する関数
税込価格を返す関数を作ります。ここではNode.js 22の組み込みテスト機能を使います。
Red:まず失敗させる
// price.test.js
import test from "node:test";
import assert from "node:assert/strict";
import { calculateTaxIncluded } from "./price.js";
test("1000円に10%の税を加える", () => {
assert.equal(calculateTaxIncluded(1000, 0.1), 1100);
});
まだ price.js がないため、テストは失敗します。失敗理由が期待どおりであることを確認します。
Green:最小の実装で通す
// price.js
export function calculateTaxIncluded(price, taxRate) {
return price * (1 + taxRate);
}
node --test price.test.js
ここでは、将来必要そうな通貨変換や割引機能を先に作りません。
Refactor:動きを変えずに整える
小数の扱いを明確にする要件を追加します。
厳密には、この時点の新しい丸め要件は次のRedを作る振る舞い追加です。テストを通した後に、変数名や重複を整理する段階がRefactorです。機能追加と内部整理を同時にしない点が重要です。
test("小数になった金額は切り捨てる", () => {
assert.equal(calculateTaxIncluded(999, 0.1), 1098);
});
export function calculateTaxIncluded(price, taxRate) {
const total = price * (1 + taxRate);
return Math.floor(total);
}
テストがあるため、既存の動きを壊していないかすぐ確認できます。
小さく進める理由
一度に多くのテストを書くと、どの前提が間違ったか分かりにくくなります。
- 一つの期待を書く
- 期待した理由で失敗する
- 最小のコードで通す
- 名前や重複を整える
- 次の期待へ進む
この短い周期がTDDの中心です。
テスト候補の選び方
| 候補 | 先に書く理由 |
|---|---|
| 代表的な正常値 | 最小の契約を決める |
| 境界値 | 条件の切り替わりを決める |
| 無効入力 | 失敗方法を決める |
| 過去の不具合 | 再発を検知する |
すべてを先に列挙して一括実装せず、一件ずつ回します。今回の例では負数や不正な税率をどう扱うかは仕様が示されていません。勝手に例外を追加せず、要求を確認して次のテストにします。
テスト名は実装方法ではなく、利用側から見た振る舞いを表します。
TDDが向かない時
見た目を探索している初期UIや、使い捨ての検証コードでは、先に動かして学ぶ方が早い場合があります。すべてをTDDにするのではなく、入力と出力が明確なロジックから試します。
探索後に残すコードでは、判明した要求をテストへ戻します。外部サービスの仕様が不安定な箇所では、単体テストだけで安心せず契約・結合テストも必要です。
よくある誤解
最小実装は雑なコードを永続化する意味ではありません。Greenの後に整えます。テストを先に大量に書くこともTDDの短い反復とは異なります。また、モックが多いほど優れた単体テストとは限りません。内部実装へ密結合すると、安全なリファクタリングを妨げます。
注意とベストプラクティス
- 一回のRedで一つの理由だけを扱う。
- 失敗メッセージを読み、想定外の構文・設定エラーを先に直す。
- Green中に将来機能を作り込まない。
- Refactor中は新しい振る舞いを追加しない。
- 時刻や乱数など不安定な入力は制御可能にする。
デバッグ・確認方法
テストが通らない時は、最初の失敗、実際値と期待値、テスト対象のimportを確認します。対象テストだけを実行した後、全テストを実行します。常に通るテストを疑う場合は、実装を一時的に意図と異なる値へ変え、Redになることを確認して元へ戻します。
ケーススタディ:受講月数に応じた料金計算
月額料金から割引後の請求額を返す関数を題材にします。要件は、一か月なら通常料金、三か月以上なら五パーセント割引、十二か月なら十パーセント割引、零以下は受け付けない、というものです。最初から全条件を実装せず、まず一か月の入力で通常料金を返す一例だけを赤くします。この赤は構文エラーではなく、期待金額と未実装の結果が違うために失敗する必要があります。
良いRedは、失敗名から要求が読め、実装前には確実に失敗します。悪いRedは、データの読み込み失敗や関数名の誤字で止まる状態です。そのままGreenへ進むと、要求を検証したのか配線を直しただけか分かりません。失敗メッセージ、期待値、実際値を読み、狙った理由で赤いことを観測します。
一つ目のGreenでは通常料金を返す最小実装で構いません。次に三か月の例を追加すると固定値では通らなくなります。境界条件は二か月、三か月、十一か月、十二か月の順に置くと、どこから割引率が変わるか説明できます。異常値を先に大量追加するより、代表例と境界を交互に進めると、失敗した要求を一つずつ特定できます。
リファクタリングでは、割引率の条件が重複した段階で名前を付けます。ただし、テストが緑でも将来の通貨対応など未要求の一般化は行いません。良い変更は条件分岐を読みやすい関数へ抽出し、前後で全テストが同じ結果になることを確認するものです。悪い変更は、機能追加と名称変更とデータ構造変更を同時に行い、失敗原因を分からなくします。
判断手順と失敗条件
各周回では、次に学びたい仕様を一文で書き、その仕様を最小入力で表すテストを選びます。最初から緑なら既に実装済みか、アサーションが弱すぎる可能性を調べます。狙いと違う例外で赤いなら、テスト準備を直してから進みます。Greenでは対象テストだけを実行し、その後に関連テスト全体を実行します。
仕様の根拠が曖昧、外部APIの結果を安定して制御できない、テストのためだけに公開APIを不自然に変えたくなった場合は一周を止めます。要件確認、依存境界の整理、より小さい単位への分割が必要な合図です。速度を守るために誤った前提を固定してはいけません。
観測するのは成功件数だけではありません。一周ごとの変更量、赤が狙った理由だったか、Greenまでのやり直し、リファクタリング後に振る舞いが不変かを記録します。周回が大きくなり続けるならテスト候補が大きすぎます。内部の一行ごとに壊れるなら、利用者から見える振る舞いではなく内部構造へ結び付きすぎています。
完成条件は緑だけではありません。境界の根拠を説明でき、未対応入力の扱いが明示され、同じ入力から同じ結果が得られ、リファクタリング前後が一致することです。最後に要件表とテスト名を突き合わせ、重要な規則と境界が観測可能か確認します。
まとめ
TDDは、期待した理由のRed、最小実装のGreen、動作を変えないRefactorを一振る舞いずつ回す設計フィードバックです。 すべてに適用せず、契約が明確な小さなロジックから練習します。