テスト駆動開発(TDD: Test-Driven Development)は、テストリストの作成・テストの記述・実装・リファクタリングを小さなサイクルで繰り返す開発ワークフローです。実装コードより先にテストを書く「テストファースト」を含む、より広い手法です。TDDを調べると「Red-Green-Refactoringの3ステップ」の解説が多いですが、そのサイクルに入る前の準備などが見えにくいです。この記事では、テストリストの作成からサイクルを回すまでの全体の流れを整理します。単体テストへの適用を前提に説明します。
TDDは以下の流れで進めます。
1. テストリストを作る
機能のテスト観点を一覧にする(コードはまだ書かない)
2. テストリストから1件選ぶ
3. Red-Green-Refactoringサイクルを回す
Red : テストコードを書く(この時点では失敗する)
Green : テストをパスする最小の実装を書く
Refactoring : コードを整える(ふるまいは変えない)
4. 2〜3をテストリストの全件完了まで繰り返す
テストリストを作る
テストリストとは何か
テストリストとは、実装する機能に対してテストすべき内容を書き出した一覧です。コードではなく、メモ書きで構いません。
実装しながらテストケースを考えると、判断と実装が同時に発生します。観点の抜け漏れが生じやすくなります。先にテストリストを作ることで、実装時にサイクルだけに集中できます。
テストリストは最初から完全に揃える必要はありません。わかる範囲で書き始め、実装を進める中で気づいたケースを追加していきます。
テストリスト(カート追加機能):
[ ] 在庫がある商品を追加できる ← 機能要件から
[ ] 同じ商品を追加すると数量が増える ← 機能要件から
[ ] 在庫がない商品は追加できない ← 機能要件から
[ ] 商品IDが存在しない場合はエラーになる ← DB定義(FOREIGN KEY)から
[ ] 数量に0を指定したとき追加できない ← 実装中に気づいたケース
[ ] カートの上限20件を超えると追加できない ← 業務ルール定義から
このテストリストはServiceクラスのメソッドを対象にしています。バックエンドは一般的にController・Service・Repositoryの3層で構成されますが、TDDの単体テストで主に扱うのはServiceです。
Serviceにはビジネスロジックが集中しており、入力・出力・条件分岐が明確に定義できます。ControllerはHTTPルーティングのみを担うためビジネスロジックを持たず、Repositoryは実際のDBへのアクセスを伴うため単体テストの対象にしにくいです。これらは実装が揃った後の結合テストで検証します。
*
テストリストはテキストファイルや紙など、書きやすい場所に記録して構いません。実装が完了したら削除してよいメモです。その後はテストコードのメソッド名が「何をテストしているか」を示すため、保守時の網羅確認はテスト名と機能要件を照らし合わせることで行います。
テストリストのすべてをテストコードに起こす必要はありません。実装を進める中で「別のテストで既にカバーされている」と気づいた項目は取り除いて構いません。
取り組む順番は、シンプルなケースから始めるのが推奨です。正常系から入ることで関数の基本構造が先に確立し、後続のテストが積み上がりやすくなります。推奨順は「正常系 → ふるまいのバリエーション → 異常系 → データ制約」です。
*
テストケースには2つの種類があります。ふるまいのケースは機能要件から導き、データ制約のケースは設計ドキュメントから導きます。
ふるまいレベル(機能要件から):
- 在庫がある商品をカートに追加できる
- 在庫がない商品は追加できない
データ制約レベル(設計ドキュメントから):
- カートに追加できる上限は20件まで ← DB定義から判明
- 数量は1〜99の整数のみ指定できる ← 画面項目定義から判明
テストリストのインプット情報
2種類のテストケースは、それぞれ異なるドキュメントから導きます。
ふるまいのケースは機能要件から作ります。機能要件の1項目が、テストケースの1つに対応します。
機能要件:
- 在庫がある商品をカートに追加できる → ふるまいのテストケース
- 同じ商品を追加すると数量が増える → ふるまいのテストケース
データ制約のケースは設計ドキュメントから導きます。ドキュメントの種類によって、得られる観点が異なります。
| ドキュメント | 主なテスト観点 |
|---|---|
| ユーザーストーリー + 機能要件 | ふるまい・正常系 |
| 画面項目定義 | 入力値の境界値・異常系 |
| DB定義 | データ制約の境界値・異常系 |
| 業務ルール定義 | 条件分岐のエッジケース |
| API仕様書 | リクエスト/レスポンスの有効・無効パターン |
データパターンの絞り込み方
ドキュメントから観点を洗い出したら、次にデータの値を絞り込みます。制約が多いと組み合わせが膨大になるため、同値分割と境界値分析を活用します。
同値分割は、同じふるまいをするデータをグループにまとめ、各グループから1件だけテストする方法です。全パターンを試す必要がなくなります。
年齢入力(1〜120が有効)の同値分割:
グループA: 有効な値 → 代表値: 30
グループB: 小さすぎる値 → 代表値: 0
グループC: 大きすぎる値 → 代表値: 121
境界値分析は、グループの境界の値をテストする方法です。境界の前後にバグが潜みやすいためです。同値分割と組み合わせて使います。
業務ルール例: 購入金額による割引率
1万円未満 → 割引なし
1万円以上 → 5%割引
10万円以上 → 10%割引
境界値のテストケース:
9,999円 → 割引なし(境界の手前)
10,000円 → 5%割引(境界)
99,999円 → 5%割引(境界の手前)
100,000円 → 10%割引(境界)
絞り込んだ各パターンが1件のテストケースになります。1件のテストケースは1つの検証観点に対応させます。複数の観点を1件にまとめると、テストが失敗したときの原因を特定しにくくなります。
Red-Green-Refactoringサイクル
TDDはRed・Green・Refactoringの3ステップを繰り返します。このサイクルを「Red-Green-Refactoring」と呼びます。
各ステップの役割は以下のとおりです。
| ステップ | 書くもの | 目的 |
|---|---|---|
| Red | テストコード | 仕様を先に定義する |
| Green | 実装コード | テストをパスさせる |
| Refactoring | 実装コード | ふるまいを変えずに整える |
1サイクルの単位
1サイクルは、テストリストの1件に対応します。テストケースを1つ選び、Red→Green→Refactoringを完結させてから、次のテストケースに進みます。
テストリスト:
[x] 正の数同士を足せる ← 1サイクル完了
[ ] 0を含む加算ができる ← 次のサイクルへ
[ ] 負の数を含む加算ができる
Red:失敗するテストを書く
実装コードがない状態でテストコードだけを書きます。テストは必ず失敗します(テストされる側のコードがないので、失敗するという話です。)。
失敗するテストを書く目的は2つあります。1つは、実装前に失敗を確認することで、後でGreenになったときに「偽陽性(常にパスする壊れたテスト)ではない」と判断できます。
もう1つは、仕様を先に明確にしてから実装に入れることです。テストを書く行為は、関数の入力・出力・ふるまいを先に決める行為です。「何を渡したら何が返るか」「エラー時はどう振る舞うか」をこの段階で整理します。
この整理をRedで完結させることで、Greenでは「どう実装するか」だけに集中できます。仕様を考えながら実装するよりも、各フェーズの思考の負荷が下がります。
設計(何を作るか)と実装(どう作るか)の責務を分けることで、それぞれの判断の質が上がります。仕様を先に整理してから実装に入るため、実装中の手戻りが減り、結果として堅牢なコードになりやすくなります。
// テストだけ書く(実装はまだない)
test("正の数同士を足せる", () => {
expect(add(1, 2)).toBe(3); // add() は存在しないので失敗する
});
Green:テストをパスする最小の実装を書く
テストがパスするための最小限のコードを書きます。この段階の目的は「動くこと」だけです。コードの読みやすさや構造は問いません。
極端な例として、最初のテスト1件だけをパスさせるためにハードコードしても構いません。
// ハードコードでも、この時点では許容される
function add(a: number, b: number): number {
return 3; // add(1, 2) === 3 はパスする
}
次のテストケースを追加したとき、ハードコードでは対応できなくなります。return 3 は入力に関わらず常に 3 を返すため、add(0, 0) のような別の入力に対して正しい値を返せないからです。そのタイミングで正しい実装に直します(極端な例で説明してます)。このハードコードをしてから一般化するアプローチは、TDDにおいて『仮実装』や『三角測量』と呼ばれる伝統的なテクニックです。
// 次のテスト add(0, 0) === 0 が失敗するため、実装を修正する
function add(a: number, b: number): number {
return a + b;
}
Refactoring:テストをパスしたまま整理する
Green フェーズは「動くこと」を優先するため、コードの設計は意図的に後回しにします。Refactoring フェーズが、実装設計を行う段階です。
テストがグリーンの状態を維持しながら、コードを整理します。テストが保護網になるため、リファクタリングで意図せずふるまいを壊した場合はすぐに検知できます。
リファクタリングの中心は重複の除去です。Greenフェーズは「動くこと」を優先するため、似た処理のコピーやハードコードした値が複数箇所に散らばりやすいです。重複があると、仕様変更のとき散らばった箇所をすべて修正する必要が生じます。Refactoringで重複を取り除くことで、変更に強いコードになります。
リファクタリングの主な観点は以下のとおりです。
| 観点 | 具体例 |
|---|---|
| 重複の除去 | 同じ処理が複数箇所に散らばっている |
| 命名の改善 | 変数名・関数名が意図を表していない |
| 関数の分割 | 1つの関数が複数の責務を持っている |
| マジックナンバーの排除 |
if age > 20 を if age > LEGAL_AGE にする |
| 条件分岐の整理 | ネストが深い・条件が複雑すぎる |
このサイクルをテストリストの件数だけ繰り返しながら開発を進めます。
TDDのスコープと完了条件
単体テストと結合テストの違い
TDDのRed-Green-Refactoringサイクルは、単体テストで回します。単体テストと結合テストでは、検証の対象と目的が異なります。
| 項目 | 単体テスト | 結合テスト |
|---|---|---|
| 検証の対象 | 関数・クラス単体 | 複数コンポーネントの連携 |
| 外部依存 | モックで切り離す | 実際のDBやAPIを使う |
| 実行速度 | 速い | 遅い |
| TDDとの関係 | メインのサイクルで使う | 実装が揃ってから使う |
結合テストはサイクルに含みません。実行に時間がかかるため、素早いフィードバックというTDDの前提が成り立たないからです。
単体テストで「ロジックが正しい」ことを確認し、結合テストで「つないだときも正しい」ことを確認するという役割分担になります。単体テストはモックを使って外部への依存を切り離すため、実際のDBクエリやコンポーネント間の連携は検証対象外になります。結合テストはこの部分を補います。
単体テストが検証できないもの(結合テストで補う):
- 実際のDBへのクエリが正しく動くか
- トランザクションが正しく機能するか
- 複数のサービスが連携したときの動作
- 認証フローの全体の流れ
TDDは単体テストで回すのが基本ですが、モックを多用しすぎると実装の詳細に依存した壊れやすいテストになることがあります。環境構築の進化により、現在ではインメモリDBやフロントエンドのコンポーネントテストなど、小規模な結合テストの範囲でもTDDのサイクルを回すケースが増えています
完了条件とカバレッジの考え方
TDDの完了基準は、機能要件に対応するすべてのテストがグリーンになっていることです。全パターンを網羅する必要はなく、正常系・境界値・異常系の観点で機能要件の各項目を確認できていれば十分です。
カバレッジは、テストがコードのどの割合を実行しているかを示す指標です。テストリストを機能要件から作成している以上、TDDを実践すれば結果としてカバレッジは高くなります。100%を目標にする必要はありませんが、極端に低い場合はテストの抜け漏れを調べるきっかけになります。カバレッジを高めることを目標にすると、意味のないテストが増えるため注意が必要です。
テストは書けば書くほど良いわけではありません。テストにもメンテナンスコストがかかります。複雑なロジックや業務ルールを厚くカバーし、単純な処理は薄くするバランスが現実的です。
TDDが向いている場面・向いていない場面
向いている場面
入力と出力が明確に定義できる処理はTDDと相性が良いです。条件分岐が多くエッジケースの洗い出しが重要な処理や、長期にわたって改修が発生するコアなロジックも同様です。
バックエンドのビジネスロジックや計算処理は特に適しています。フロントエンドでも、ユーティリティ関数や状態管理ロジックなどふるまいが明確な部分にはTDDを適用できます。
- 計算ロジック・割引ロジックなどの業務ルール
- バリデーション処理
- 状態管理のロジック(Redux等)
- 繰り返し改修が発生するコアなビジネスロジック
向いていない場面
仕様が未確定の段階では、テストを先に書くコストが無駄になることがあります。また、UIの見た目の検証はTDDのサイクルに向きません。CSSレイアウトやアニメーションはテストで表現しにくく、ビジュアルリグレッションツールなど別のアプローチが適しています。
- 仕様が固まっていない探索的な実装・試作段階
- CSSレイアウトやアニメーションなど視覚的な要素
- E2Eテスト(実行速度が遅く毎サイクルには向かない)
おわりに
TDDはテストリストの作成からRed-Green-Refactoringのサイクルを繰り返すことで、仕様を明確にしながら実装を進める開発手法です。単体テストを中心に適用することで、設計の整理とコードの保守性向上を両立できます。
TDDが品質に貢献するのは、Red-Greenのサイクルを回す以上、テストのない実装コードが原理的に生まれないためです。ビジネスロジックは常にテストで保護された状態に保たれます。ただし、TDD単体でシステム全体の品質を保証することはできません。一般的なテスト戦略は「テストピラミッド」と呼ばれる3層で構成されます。
| 層 | 特徴 | TDDとの関係 |
|---|---|---|
| 単体テスト | 多い・速い | TDDが担う |
| 結合テスト | 中程度 | 実装後に整備する |
| E2Eテスト | 少ない・遅い | 全体フローの最終確認 |
TDDは単体テスト層を確実に積み上げる手法です。上位層は別途整備することで、初めてシステム全体の品質が保たれます。
導入にあたっては、いくつかの点を事前に把握しておくと良いです。
- 慣れるまでに時間がかかります。「テストを先に書く」思考の切り替えには練習期間が必要です
- テストコードのメンテナンスコストが発生します。実装の変更に合わせてテストも修正する必要があります
- 機能要件が定義されていないと、テストケースの設計が難しくなります。TDDは仕様がある程度固まった段階で効果を発揮します
最初から全コードに適用しようとするより、業務ロジックやバリデーションなど範囲を絞って試すのが取り組みやすいです。また、作成したテストコードはGitHub ActionsなどのCI/CDパイプラインに組み込み、プッシュのたびに自動実行させることで、将来にわたってコードの安全性を守る強力な保護網になります
最後まで読んでいただいた方、ありがとうございました。
参考文献
- 和田卓人. 「テスト駆動開発のはじめの一歩|t_wadaさんに聞く1人で始める自動テストのコツと考え方」. Agile Journey. 2023年11月30日.
- 和田卓人. 「【翻訳】テスト駆動開発の定義」. t-wada のブログ. 2024年3月8日.