テスト駆動開発

テストを先に書き、テストが通るコードを実装し、リファクタリングするサイクルで開発を進める手法

テスト開発手法
「テスト駆動開発」の技術書を見る →

テスト駆動開発とは

テスト駆動開発 (Test-Driven Development, TDD) は、Kent Beck が著書『Test-Driven Development: By Example』(Addison-Wesley / 2002 年 11 月刊) で体系化した開発手法で、Red → Green → Refactor の 3 ステップを小刻みに繰り返しながらコードを書く。エクストリームプログラミング (XP) のプラクティス群の一部として実践されていたものを、Beck がこの本で独立した規律として書き下した。

混同されやすいが、「テストを先に書けば TDD」ではない。TDD の本体は 3 ステップを 1 サイクルとして回すところにあり、Refactor を省いてテストファーストだけを取り入れると、通すために書いた場当たりの実装がそのまま残り、テストは実装の形をなぞって固定される。仕様が変わったときにテストが最初に壊れて修正の足かせになるのは、この形の運用が原因になっていることがある。

Red → Green → Refactor

3 ステップは順序そのものに意味がある。先に Red を目で確認するので、条件を書き間違えて何をしても成功するテスト (常に通る空振りのテスト) を最初にふるい落とせる。逆にテストを後から書くと、既に動く実装に対して書くため、そのテストが本当に失敗を検出できるのかを確かめる機会が無くなる。

1. Red:      失敗するテストを書く
2. Green:    テストが通る最小限のコードを書く
3. Refactor: コードを整理する (テストは通ったまま)
→ 1 に戻る

1 サイクルの目安は数分。Green にたどり着くまでが長いと感じたら、テストが一度に要求している範囲が広すぎる合図なので、テストを分割して要求を小さくする。

実践例

割引計算を題材に 1 サイクル半を追う。Step 1 のテストは関数がまだ存在しないため参照エラーで落ちる。これが Red の正しい姿で、期待値の不一致で落ちる前に「そもそも無い」で落ちることを確認しておく。

// Step 1: Red - 失敗するテストを書く
test('10,000円以上で10%割引', () => {
  expect(calculateDiscount(15000)).toBe(1500);
});
// → ReferenceError: calculateDiscount is not defined

// Step 2: Green - テストが通る最小限のコードを書く
function calculateDiscount(total: number): number {
  if (total >= 10000) return total * 0.1;
  return 0;
}
// → ✅ テスト通過

// Step 3: Refactor - コードを整理
const DISCOUNT_THRESHOLD = 10000;
const DISCOUNT_RATE = 0.1;

function calculateDiscount(total: number): number {
  if (total >= DISCOUNT_THRESHOLD) return total * DISCOUNT_RATE;
  return 0;
}
// → ✅ テスト通過 (リファクタリング後も)

// Step 1 (次のサイクル): 新しいテストを追加
test('会員は15%割引', () => {
  expect(calculateDiscount(15000, true)).toBe(2250);
});
// → Red → Green → Refactor ...

TDD で確実に得られるもの

  • テストが後回しにならない。カバレッジは目標として追うものではなく、サイクルの副産物として積み上がる
  • テストしにくさが着手時点で痛みとして出る。グローバル状態への依存や生成と処理の混在は、テストを書こうとした瞬間に手が止まるので設計の警告として先に届く
  • リファクタリングの安全網が常に手元にある。直す前に網が張られている状態を保てる
  • 実装が要求の範囲に収まる。通すべきテストが無い機能は書かないという歯止めになる

一方で「TDD にすれば設計が良くなる」「欠陥が減る」を無条件の効果として期待しない。設計が良くなるのは Refactor を実際に行った分だけで、手順を踏むこと自体の効果ではない。上の 4 点、つまり工程上の性質までを利点として数え、それ以上は自分たちの結果で確かめる姿勢が安全側になる。

TDD が難しいケース

TDD が効くのは、入力と期待する出力を先に言葉で決められる範囲。次の 4 つは先にテストを書く費用を回収しにくいので、無理に当てはめず代替へ寄せる。

ケース理由代替アプローチ
UI のスタイリング見た目のテストが困難ビジュアルリグレッションテスト
外部 API との連携モックの設定が複雑結合テストで補完
探索的な開発仕様が不明確スパイクで仕様を固めてから TDD
レガシーコードテストが書きにくい構造まずリファクタリングしてテスタブルに

TDD と BDD の違い

TDD を回していると「次に何をテストすべきか」で手が止まる。BDD はその問いへの答えをテスト名の書き方から与える立場で、同じサイクルを別の語彙で運用する。

手法テストの記述焦点
TDDexpect(calculateDiscount(15000)).toBe(1500)実装の正しさ
BDDGiven 15000円の注文 When 割引計算 Then 1500円ビジネス要件の充足

BDD (Behaviour-Driven Development) は Dan North が記事「Introducing BDD」(2006 年) で整理した進め方で、テスト名を振る舞いを述べた文にすること、要求そのものを実行できる受け入れ条件として書くことを軸にする。TDD を置き換えるものではなく、テストの粒度と言葉遣いを要求側から決める派生と捉えるとよい。

Lambda での TDD

Lambda ハンドラーはイベントを受けて応答を返す形なので、外部呼び出しを引数か環境変数で差し替えられるように書けば、そのまま関数として呼んでテストできる。逆に、ハンドラー内部で直接クライアントを生成すると差し替え口が無くなり、TDD のサイクルが最初のテストで止まる。

// テストを先に書く
test('正常な注文リクエストで 201 を返す', async () => {
  const event = createApiEvent({ body: JSON.stringify({ item: 'Book', qty: 1 }) });
  const result = await handler(event);
  expect(result.statusCode).toBe(201);
});

実務での活用方法は関連書籍にも詳しい。

共有:

この記事は役に立ちましたか?

関連用語

関連する記事