株式会社ジオデータ開発 ロゴ
株式会社ジオデータ開発GEO DATA DEVELOPMENT Inc.
TDDテスト駆動開発Red Green RefactorTypeScriptユニットテストリファクタリングXPCI

SOFTWARE DEVELOPMENT / TEST FIRST / RED GREEN REFACTOR

テスト駆動開発(TDD)とは? Red・Green・RefactorをTypeScriptで実践する

公開日:2026-10-13

「修正したら別の機能が壊れた」「要件を実装してからテストを考えると、期待値があいまい」。そんな場面で、テストを先に書き、設計を小さく確かめながら進める方法がTDDです。

この記事の学習ルート

手作業の確認 → TDDとXP → Red・Green・Refactor → 送料計算の実装 → 境界値と例外 → テストダブル → CI・セキュリティ → 操作ラボ

1. なぜ実装の前にテストを書くのか

小さなECサイトを想像してください。送料は通常500円で、5,000円以上の買い物、または有料会員には無料になる仕様です。実装を先に始めると「5,000円ちょうど」「会員の少額注文」「0円注文」などの具体例を見落としやすくなります。テストを先に書けば、「どんな入力で、何を返すべきか」をコードに落とし込んだうえで実装に取りかかれます。

TDDはバグを必ずなくす手法ではありません。むしろ、仕様への誤解を早期に見つけ、変更を安心して進めるための短いフィードバックループです。最初は簡単な振る舞いを1つ決め、失敗することを確認してから必要最小限を実装します。

2. TDDとエクストリームプログラミング(XP)

XP(Extreme Programming)は、短い反復で利用者からフィードバックを受け、継続的インテグレーション、ペアプログラミング、リファクタリングなどの実践を重視するアジャイル開発手法です。TDDはその実践の一つとして広まりました。TDD自体はXPを採用しないチームでも利用できます。

用語焦点実践例
TDD小さなテストから仕様と実装を進めるRED → GREEN → REFACTOR
XP開発チーム全体の働き方ペア作業、CI、リファクタリング
BDDビジネス上の振る舞いの共有Given / When / Then
通常の自動テスト実装の回帰確認や品質検証実装後に単体・結合テストを追加

3. Red・Green・Refactorの3段階

小さく繰り返す3段階の開発ループ

RED

失敗するテストを書く

GREEN

最小限の実装で通す

REFACTOR

振る舞いを保って整理

Refactor後に次の小さな仕様へ戻り、新しいRedを開始する

Red:テストを先に書いて、意味のある失敗を得る

最初に「小計3,000円・非会員なら送料500円」という期待結果をテストにします。未実装の関数を呼び出すのでテストは失敗します。重要なのは、期待する振る舞いがまだ存在しないという正しい理由で失敗していることを確認する点です。実行環境の設定ミスや構文エラーだけでは、振る舞いの検証になりません。

Green:テストを通す必要最小限の実装

まだ要求されていない複雑な汎用化は避け、テストを通すコードを実装します。途中で新しい条件が必要になった場合、次の小さなテストとして追加します。単一の例だけに合わせた定数返却で緑になる場合もありますが、その後に代表例と境界値を増やして振る舞いを絞り込みます。

Refactor:外から見た動作を変えずに内部構造を整える

重複した条件、意味の分からない変数名、長すぎる関数を整理します。変更前後で自動テストを通すことで、外から観測できる振る舞いを保てていることを確認します。リファクタリングの最中に仕様の変更を混ぜると、不具合の原因が分かりにくくなるので分離することが重要です。

4. TypeScriptで送料計算を実装する

まず規則を表にします。仕様を言葉だけで残すのではなく、テスト入力と期待結果を具体化することが出発点です。ここでは小計が0円以上の整数であることを前提にしています。負数や小数を受け付けるAPIでは別途入力検証が必要です。

注文小計会員期待する送料確認したいこと
3,000円いいえ500円基本例
4,999円いいえ500円境界値の直前
5,000円いいえ0円境界値ちょうど
1,000円はい0円会員条件
0円いいえ500円ゼロ円の仕様
shipping.test.ts|最初にテストを書く(Vitestの例)
import { describe, expect, it } from "vitest";
import { shippingFee } from "./shipping";

describe("shippingFee", () => {
  it("通常注文では500円", () => {
    expect(shippingFee(3000, false)).toBe(500);
  });
  it("5,000円ちょうどで送料無料", () => {
    expect(shippingFee(5000, false)).toBe(0);
  });
  it("4,999円では無料にならない", () => {
    expect(shippingFee(4999, false)).toBe(500);
  });
  it("会員は少額でも送料無料", () => {
    expect(shippingFee(1000, true)).toBe(0);
  });
});

テスト用コードをまとめて示していますが、実際のTDDでは最初から4件書く必要はありません。まず1件のRedを確認してGreenにし、続けて境界値や会員条件を追加していく方法が基本です。

shipping.ts|最小実装からリファクタリングへ
/** subtotal は0以上の整数とする */
export function shippingFee(
  subtotal: number,
  isMember: boolean
): number {
  const freeShipping = subtotal >= 5000 || isMember;
  return freeShipping ? 0 : 500;
}
package.json|テスト実行コマンド例
// 利用するテストランナーを導入したプロジェクトで実行
// npm install -D vitest
// npm exec vitest -- run
// npm exec vitest -- watch

上の例はTypeScriptの純粋関数に焦点を当てたものです。実プロジェクトでは既存のテスト環境、TypeScript設定、Node.jsバージョン、CIのワークフローに合わせて実行方法を調整します。テストライブラリはこの記事を表示するための追加依存ではありません。

5. 境界値・同値分割・例外をどうテストするか

送料が無料になる条件が「5,000円以上」であれば、4,999円と5,000円を確認すると条件の境界を直接検証できます。1円、2,000円、3,000円と同じ種類の値を大量に加えるより、境界や条件の組合せを狙う方が欠陥を検出しやすいことがあります。

同値分割

同じ振る舞いになる領域を代表値で確認。例:5,000円未満の非会員注文。

境界値

条件が切り替わる直前・直後を確認。例:4,999円と5,000円。

異常系

負数、NaN、巨大な値、不正な会員情報をAPI層でどう扱うか決める。

0円という値を許容するのか、負の金額をどの層で拒否するのか、通貨の丸め処理をどうするのか。これらは自動テストを増やす以前に決める必要がある業務仕様です。テストが仕様そのものを正しく定義しているかも、レビューの対象にします。

6. テストダブルを使うのはどんな時か

外部の決済API、メール配信、データベースに毎回アクセスすると、テストが遅くなり、ネットワーク障害やデータ変更に影響されます。単体テストでは外部との境界を差し替えて、呼び出した結果や渡した引数を検証することがあります。

通知サービスを差し替えるテストダブル
interface Notifier {
  send(to: string, message: string): Promise<void>;
}
async function notifyOrder(
  notifier: Notifier,
  address: string
) {
  await notifier.send(address, "注文を受け付けました");
}
// テストでは外部メールサービスを呼び出さない
const sent: Array<{ to: string; message: string }> = [];
const fake: Notifier = {
  async send(to, message) { sent.push({ to, message }); }
};
await notifyOrder(fake, "user@example.test");
// sent に通知内容が記録されることを検証する

Fakeは簡易実装、Stubは決まった値を返すもの、Spyは呼び出し履歴を観測するもの、Mockは期待する呼び出しを検証するもの、と目的に応じて用語が使い分けられます。ただしチームやライブラリによって呼称は異なります。実通信の互換性や権限は、別途結合テストで確認する必要があります。

7. 単体・結合・E2EテストとTDDの関係

テストの層と観測する境界

E2E / ユーザー視点

ブラウザ → API → DBを通して重要シナリオを確認

結合 / 境界

APIとDB、外部サービスの契約、認可の連携

単体 / 純粋関数

送料計算や入力検証など小さなロジック

これは用途の概念図であり、必ずしも特定の件数比率を推奨するものではありません。

TDDは単体テストと相性がよい一方、ビジネス全体の動作確認には結合テストやE2Eテストも欠かせません。たとえば送料計算が正しくても、注文APIが別の会員フラグを渡していたら、購入画面では期待どおりになりません。適切な粒度を選ぶために「何をどこで観測するか」を考えます。

ユニットテストが速くても、外部サービスの契約変更、DBマイグレーション、ログインの実動作までは保証されません。ユーザーにとって致命的なシナリオは、ブラウザやAPIの境界をまたいで別途確認します。

8. CIとコードレビューへ組み込む

小さなRed・Green・Refactorのサイクルをローカルで実践した後、変更を共有する前にテストを通します。CIではソースコードの取得、依存の準備、静的解析、ユニットテスト、ビルド、必要に応じた結合テストなどを自動化し、変更による回帰を継続的に検知します。

開発者の手元から共有リポジトリまで

01

テストで仕様化

02

実装して通す

03

安全に改善

04

変更をレビュー

05

CIで再検証

CIが緑であることは、安全性を全面的に保証しません。フレークテスト(時々失敗する不安定なテスト)、秘密情報の漏えい、依存パッケージの脆弱性などは別途対策が必要です。失敗したテストを安易に無効化しない運用も重要です。

9. セキュリティ実装とTDD

認証・認可・入力検証などにも、仕様をテストで先に固定する考え方は使えます。特に「許可される場合」だけでなく、「拒否すべき場合」の期待動作をテストすると、開発者が安全側の条件を見落としにくくなります。

対象先に決めるテスト見落としやすい点
認可別ユーザーの注文IDは参照できないUI非表示だけではAPIの保護にならない
入力検証負数の金額や不正な値を拒否するフロント側だけの検証は回避できる
パスワード誤った資格情報を拒否するエラー応答から利用者の存在が漏れる
監査ログ重要操作で監査記録を生成するトークンや個人情報の過剰記録
エラー処理障害時に内部情報を外部へ返さない例外メッセージをそのまま露出する

10. よくある失敗と改善策

テストが実装の内部構造に結び付いている

プライベート関数の細部ではなく、公開された振る舞いを中心に検証する。

一度に巨大なテストを作って失敗する

1つの入力と期待結果から始め、境界値と組合せを段階的に追加する。

Redの理由を確認しない

失敗の内容が意図した仕様との差分なのか、環境・構文の問題なのかを区別する。

テストが緑になった瞬間に終わる

重複や命名を見直し、Refactor後に全テストを再実行する。

カバレッジの数字だけを目標にする

仕様上の重要な分岐、境界、失敗時の振る舞い、利用者への影響を考える。

11. FAQ

Q. TDDならバグが出なくなりますか?

いいえ。テストケースが不足していたり期待結果自体が間違っていたりすれば欠陥は残ります。短いフィードバックと設計改善が主な価値です。

Q. テストコードを先に全部完成させる必要はありますか?

ありません。1件の失敗を確認し、最小実装で通し、改善するという小さな反復を繰り返します。

Q. テストで100%のカバレッジを達成することが目的ですか?

違います。網羅率は未検証箇所を探す参考になりますが、仕様を適切に検証していることや品質を保証する数値ではありません。

Q. 既存の大きなコードにも導入できますか?

できます。ただし、まず振る舞いを固定する特徴づけテストを追加してから小さく分割するなど、変更の安全性を確保する工夫が必要です。

Q. TDDとBDDは何が違いますか?

TDDは開発者の小さな実装サイクル、BDDは業務上の振る舞いを関係者間で共有することに重点があります。両者は併用できます。

Q. データベースや外部APIもすべてモック化しますか?

いいえ。単体テストでは境界を差し替えることがありますが、実際の連携や制約を検証する結合テストも必要です。

12. 実践ラボ:テストを先に書く体験

最後に、送料計算のサンプルで各段階を切り替えましょう。Redの段階ではテストが失敗し、Greenで成功、Refactorで成功を維持します。境界値だけに絞っても確認でき、自由入力で追加のテスト条件も考えられます。

INTERACTIVE LAB / TDD

Red → Green → Refactorを操作する

架空の送料計算を題材にした教材用シミュレーションです。実際のテストランナーやJavaScript実行環境を動かしているわけではありません。

例: 5,000円以上または会員は送料無料。その他は500円。

まだテストも実装もありません。最初に入力・出力・境界値・例外を具体化します。

shipping.ts / 仕様メモ
// 仕様
// subtotal >= 5000 または member === true → 0円
// それ以外 → 500円
// まずテスト可能な純粋関数として設計する

テスト結果

「テストを実行」を押すと結果が表示されます。

テスト期待実際結果
500円—未実行
0円—未実行
500円—未実行
0円—未実行
500円—未実行

追加のテストケースを考える

入力値を変えると仕様上の期待結果を計算します。テストの自動追加・実行は行いません。

期待される送料:500円

4,999円と5,000円の境界、会員割引との組合せ、負数や巨大数などの入力検証も考えてみましょう。

教材上のREDは「未実装による失敗」です。実務では構文エラーや環境障害による失敗と、期待した仕様を満たさない失敗を区別します。リファクタリング後は全テストを再実行します。

13. まとめ

TDDは、期待される振る舞いを小さなテストに落とし込んでから実装する方法です。Redで要求を可視化し、Greenで最小限のコードを作り、Refactorで読みやすさを高めます。日々の開発では、境界値・失敗系・連携箇所の検証を組み合わせ、CIやコードレビューを通して安全に変更を積み重ねることが大切です。

一次資料・参考資料