TDD とは何か
テスト駆動開発(Test-Driven Development)は、実装コードより先にテストコードを書く開発手法です。Kent Beck が 1990 年代後半に Extreme Programming(XP)の中で体系化し、2002 年に書籍として出版しました。
手法の核心はシンプルです。「動作する最小限の実装を、失敗するテストが要求する形で積み上げる」。これにより、設計の過剰複雑化を防ぎ、変更に強いコードベースが自然に育ちます。
Red → Green → Refactor サイクル
各段階が何をするか
- Red: これから作る機能に対して、まだ通らないテストを1つ書きます。実装が無いので失敗します。この失敗(赤)が出発点です。
- Green: そのテストを通すためだけの最小限のコードを書きます。きれいさは後回しで、まず緑(成功)にすることだけを目指します。
- Refactor: テストが緑のまま、重複を消したり名前を整えたりコードを整理します。テストが緑であり続けることが安全網になります。
なぜこの順番なのか
- 先にテストを書くと、作るものの仕様が具体的なコードとして固定されます。何を実装すべきか迷いにくくなります。
- 「最小限」に絞ることで、使われない機能を先回りで作り込む過剰実装を防げます。
- 整理はテストが守ってくれる状態で行うため、リファクタリングで壊しても即座に気づけます。
主なテストフレームワーク
言語ごとに標準的なフレームワークが確立されています。以下は 2025 年時点で広く使われているものです。
| 言語 | フレームワーク | コマンド例 | 備考 |
|---|---|---|---|
| JavaScript / TypeScript | Jest, Vitest | npx jest / npx vitest |
Vitest は Vite との統合が速い |
| Python | pytest | pytest |
プラグインエコシステムが豊富 |
| Java | JUnit 5 | mvn test |
Spring Boot と同梱で導入しやすい |
| Ruby | RSpec | rspec |
BDD 記法で記述する |
| C# | xUnit | dotnet test |
.NET 標準推奨 |
| Go | testing(標準) | go test ./... |
外部依存なし・ビルトイン |
出典: vitest.dev
生成 AI 時代になぜ注目されるか
AI がコードを書く時代になったことで、TDD の価値がむしろ上がっています。3 つの理由があります。
AI 幻覚の検証手段になる
AI コーディングツール(例えば Claude Code、Cursor、GitHub Copilot など)が生成したコードは、一見正しそうに見えて境界値や例外処理を誤ることがあります。境界値とは、18歳以上を大人とするとき「17・18・19」のような、条件の境目にあたる入力のことです。事前に書いたテストがあれば、生成コードをその場で検証できます。「通った」という事実が、目視の印象より確かな根拠になります。
AI への指示精度が上がる
「FizzBuzz を実装して」より「このテストをパスする最小の実装を書いて」のほうが、AI の出力が安定します。テストが仕様書の役割を果たし、曖昧さを減らします。Given-When-Then(前提・操作・期待結果の3つに分けてテストを書く記法)でテストを渡すと、どの入力にどの結果を期待しているかが明確になり、AI の出力が期待値からずれにくくなります。この効果は特定の AI ツールに限りません。
テスト生成コストが下がった
以前はテストを書く時間が実装の倍かかることも珍しくありませんでした。AI を使えば、仕様を日本語で説明するだけでテストの雛形が得られます。人間はケースの妥当性確認に集中できます。
AI へのテストファーストの指示例
次のプロンプトは特定ツール専用ではありません。Claude Code / Cursor / GitHub Copilot / Windsurf / Gemini CLI など、お使いの AI コーディングアシスタントの入力欄にそのまま貼り付けて使えます。テストコードを一緒に渡すことで、AI が何を作ればよいかを取り違えにくくなります。
以下のテストをすべてパスする最小限の実装を src/fizzbuzz.ts に書いてください。
テスト以外の機能は先回りして実装しないでください。
--- test/fizzbuzz.test.ts ---
import { fizzbuzz } from "../src/fizzbuzz";
describe("fizzbuzz", () => {
it("3の倍数のとき Fizz を返す", () => {
expect(fizzbuzz(3)).toBe("Fizz");
});
it("5の倍数のとき Buzz を返す", () => {
expect(fizzbuzz(5)).toBe("Buzz");
});
it("15の倍数のとき FizzBuzz を返す", () => {
expect(fizzbuzz(15)).toBe("FizzBuzz");
});
});
準備と気をつけること
環境の前提を確認する
本ページのハンズオンは JavaScript / TypeScript と Vitest を使います。始める前に Node.js(サーバー側でも動く JavaScript 実行環境。npm というパッケージ管理ツールが同梱されます)が入っているか確認してください。ターミナルで次を実行し、バージョン番号が表示されれば準備完了です。
node -v # 例: v20.11.0 のように表示されれば OK npm -v # 例: 10.2.4 のように表示されれば OK
node -v が使えるようになります。エディタは VS Code / Cursor / JetBrains 系など、お好みのもので構いません。AI コーディングアシスタント(例えば Claude Code、Cursor、GitHub Copilot、Windsurf、Gemini CLI など)のいずれかが使える状態になっていれば、後半の AI 連携ステップも試せます。必須ではなく、手で実装する形でも一通り体験できます。
導入の準備
- 使用言語のテストフレームワークをインストールする(npm / pip / mvn)
- プロジェクトルートに
test/フォルダを先に作る習慣をつける - CI(GitHub Actions など)でテストを自動実行するパイプラインを設定する
- カバレッジ計測ツールを設定し、80% 以上を目標値にする
- チームで「テストがないコードはレビューしない」ルールを合意する
よくある落とし穴
- テストが複雑すぎる。1 テストに複数の assert を詰め込まない
- Green になった後でリファクタリングを怠り、コードが汚いまま進む
- モック(本物の代わりに用意した偽の部品。外部 API やデータベースの代役として、決まった値を返すよう仕込んだもの)を使いすぎて、実際の動作とテストが乖離する
- 「テストを書く時間がない」という理由でテストを後回しにする(これが TDD をやめる最大の誘因)
- 全てをユニットテスト(単体の部品を単独で確認するテスト)にしようとして、結合テスト(複数の部品を組み合わせた状態で確認するテスト)を無視する
ハンズオン: お手元の AI ツールで FizzBuzz を TDD
以下の手順に沿って、FizzBuzz を TDD で実装します。FizzBuzz は、1 から順に数を出力し、3 の倍数なら「Fizz」、5 の倍数なら「Buzz」、両方の倍数(15 の倍数)なら「FizzBuzz」に置き換える、テストの練習として定番の小さな課題です。エディタは VS Code / Cursor / JetBrains 系など好きなもので、AI アシスタントも Claude Code / Cursor / GitHub Copilot / Gemini CLI などお使いのもので構いません。まずは手を動かして Red と Green を体感してみてください。
プロジェクトの初期化
まず作業フォルダを作り、テストフレームワーク Vitest を導入します。Vitest はテストを実行するための道具で、これがないと自動テストを走らせられません。
mkdir tdd-fizzbuzz && cd tdd-fizzbuzz npm init -y
ここで mkdir はフォルダ作成、cd はそのフォルダへ移動、npm init -y はプロジェクト管理ファイル package.json を既定値で生成するコマンドです。続いて、生成された package.json を次の内容で上書きします。scripts にテスト実行コマンドを登録しておくと、以降は短いコマンドでテストを回せます。
{
"name": "tdd-fizzbuzz",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "vitest",
"test:run": "vitest run"
},
"devDependencies": {
"vitest": "^2.0.0",
"typescript": "^5.0.0"
}
}
npm install mkdir src test
npm install は package.json に書いた道具(ここでは Vitest と TypeScript)をダウンロードして揃えるコマンドです。mkdir src test で、実装を置く src フォルダとテストを置く test フォルダを用意します。
テストファイルを先に作成する
実装より先にテストを書きます。これが TDD の肝です。test/fizzbuzz.test.ts を新規作成し、以下を貼り付けてください。src/fizzbuzz.ts はまだ作りません。テストが「こう動いてほしい」という仕様を、実行できる形で表しています。
// describe: 関連するテストをひとまとめにする箱(テスト対象の名前を書く)
// it: 1つの振る舞いを確認するテスト1件(日本語で仕様を書ける)
// expect(...).toBe(...): 実際の値が期待値と一致するかを検証する
import { describe, it, expect } from "vitest";
// まだ存在しない実装を import しておく(この時点では読み込みに失敗する)
import { fizzbuzz } from "../src/fizzbuzz";
describe("fizzbuzz", () => {
it("3の倍数のとき Fizz を返す", () => {
// Given: 3 という入力に対して(前提)
// When: fizzbuzz(3) を呼び出したとき(操作)
// Then: "Fizz" が返ってくることを期待する(期待結果)
expect(fizzbuzz(3)).toBe("Fizz");
});
it("5の倍数のとき Buzz を返す", () => {
// Given 5 / When fizzbuzz(5) / Then "Buzz"
expect(fizzbuzz(5)).toBe("Buzz");
});
it("15の倍数のとき FizzBuzz を返す", () => {
// Given 15(3と5の両方の倍数)/ When fizzbuzz(15) / Then "FizzBuzz"
expect(fizzbuzz(15)).toBe("FizzBuzz");
});
it("それ以外のとき数値の文字列を返す", () => {
// Given 倍数でない数 / When fizzbuzz(n) / Then その数の文字列
expect(fizzbuzz(1)).toBe("1");
expect(fizzbuzz(7)).toBe("7");
});
});
テストを実行して失敗させる
この段階では src/fizzbuzz.ts が存在しないため、テストは必ず失敗します。失敗を確認することが TDD の出発点です。
npm run test:run
ERROR: Cannot find module '../src/fizzbuzz' FAIL test/fizzbuzz.test.ts
最小限の実装を書く
いよいよ実装します。ここでは飾らず、テストを通すのに必要なだけのコードを書きます。src/fizzbuzz.ts を新規作成し、以下を貼り付けてください。
// n を受け取り、FizzBuzz のルールに従った文字列を返す関数
// : number は引数の型、: string は戻り値の型(TypeScript の型注釈)
export function fizzbuzz(n: number): string {
// % は割った余り。余り0 = 割り切れる = 倍数。
// 15の判定を先に置くのが要点。先に3で拾うと15が "Fizz" になってしまう
if (n % 15 === 0) return "FizzBuzz";
if (n % 3 === 0) return "Fizz";
if (n % 5 === 0) return "Buzz";
// どの倍数でもなければ、数値を文字列に変換して返す
return String(n);
}
テストを再実行して成功させる
実装ができたので、もう一度テストを走らせます。今度は全部緑(passed)になるはずです。ここで失敗が残る場合は、実装かテストのどちらかにずれがあるので、エラーメッセージが指す行を見直してください。
npm run test:run
✓ test/fizzbuzz.test.ts (4) ✓ 3の倍数のとき Fizz を返す ✓ 5の倍数のとき Buzz を返す ✓ 15の倍数のとき FizzBuzz を返す ✓ それ以外のとき数値の文字列を返す Test Files 1 passed (1) Tests 4 passed (4)
テストを追加してリファクタリングする
仕様が増える場面を体験します。「0 や負の数を渡したら "Invalid"(無効)を返す」というルールを足すとします。TDD では、まずこの仕様を表すテストを追加します。追加した直後はまだ実装が対応していないので、そのテストは失敗(Red)します。失敗を確認してから実装を直し(Green)、余裕があればコードを整理(Refactor)します。
it("0 または負の数のとき Invalid を返す", () => {
// Given 0 や負の数 / When fizzbuzz(n) / Then "Invalid"
expect(fizzbuzz(0)).toBe("Invalid");
expect(fizzbuzz(-1)).toBe("Invalid");
});
このテストを足して npm run test:run を実行すると、新しい1件だけが失敗するはずです(Red)。実装の先頭に「n が 1 未満なら Invalid を返す」ガード節を1行足せば緑に戻ります。下の折りたたみに実装例を置いています。
実装の直し方(答え)
export function fizzbuzz(n: number): string {
// 追加した仕様。異常な入力を最初に弾く(ガード節)
if (n < 1) return "Invalid";
if (n % 15 === 0) return "FizzBuzz";
if (n % 3 === 0) return "Fizz";
if (n % 5 === 0) return "Buzz";
return String(n);
}
同じことを AI アシスタントにやらせる
ここまでを手で体験したら、今度は AI に実装させてみます。お使いの AI コーディングアシスタント(例えば Claude Code なら VS Code や Cursor の拡張として、GitHub Copilot ならチャット欄から、Gemini CLI ならターミナルから)に、次の指示を入力してください。テストファイル名を明示すると、AI の出力が仕様に沿ったものになります。この指示文はツールに依存せず、そのまま使い回せます。
test/fizzbuzz.test.ts のすべてのテストをパスする最小限の実装を src/fizzbuzz.ts に書いてください。 テストで要求されていない機能は追加しないでください。
npm run test:run で実際に通ることを確認してください。テストという客観的な合否基準があるので、どの AI ツールを使っても「通ったかどうか」で正しく判断できます。AI の出力を検証する仕組みとしてテストが働く、これが TDD と AI を組み合わせる価値です。
関連手法との比較
TDD は単体テストを起点にした手法ですが、テストには複数の層と目的があります。
| 手法 | 焦点 | 主なツール | 使い分け |
|---|---|---|---|
| TDD | ユニットテスト・設計 | Jest / pytest / JUnit | 実装の正確性を保証したい |
| BDD | 振る舞い・仕様共有 | Cucumber / SpecFlow | 非エンジニアと仕様を共有したい |
| ATDD | 受け入れ基準 | Selenium / Playwright | E2E・業務ルールを網羅したい |
| SDD | 仕様ファースト | Spec Kit / Amazon Kiro | AI 開発の上流から設計を管理したい |
BDD と TDD の違いをもう少し詳しく知りたい方へ
TDD は「開発者がコードの動作を保証するためのテスト」を先に書きます。テストコードは技術者向けです。
BDD は「ユーザーや PO が理解できる自然言語に近い記法」でシナリオを書き、それを自動テストに変換します。Cucumber の場合、Gherkin 記法(Given/When/Then)で書いた .feature ファイルがそのままテスト仕様書になります。
どちらを使うかは「テストの読み手が誰か」で決まります。開発チーム内だけで完結するなら TDD で十分です。
参考資料
出典: martinfowler.com/bliki/TestDrivenDevelopment.html
-
TDD の創始者による原典。マネーサンプルを通じて Red→Green→Refactor を体験できます。Amazon.co.jp で入手可能(英語版)。
-
TDD の概念と意義を簡潔にまとめた解説。Refactoring の著者でもある Fowler による整理です。
-
日本語で TDD を広めた第一人者。書籍翻訳者として Kent Beck の意図を正確に伝えてきた方です。ブログやスライドが参考になります。
-
生成 AI 時代における TDD の位置づけを論じた記事。AI がコードを書く時代にテストファーストがなぜ重要かを掘り下げています。
-
TDD のサイクル・メリット・デメリットを実例付きで解説した英語記事。現代的な開発環境での実践例が参考になります。
-
創始者本人が「本来の TDD とは何か」を改めて定義し直した記事。テストリストを作り、1件ずつ Red→Green→Refactor する正統な手順が読めます。誤解されがちな TDD 像を本人が正しています。
-
本ハンズオンで使用する TypeScript 向け高速テストフレームワーク。Jest 互換の API を持ちながら Vite のエコシステムと統合されています。導入方法・mock・非同期テストの書き方が読めます。
-
JavaScript / TypeScript で最も広く使われるテストフレームワークの日本語ドキュメント。matcher やモックの API が体系的に読めます。Vitest とほぼ同じ書き味です。
-
Kent Beck が Smalltalk 用に作った SUnit を起点に、JUnit・NUnit・pytest など各言語の単体テスト基盤へ広がった系譜が読めます。今日のテストフレームワークの共通構造(テストケース・アサーション・テストランナー)の由来が分かります。
-
和田卓人さんが TDD の考え方をライブコーディングで示す定番講演の資料。Red→Green→Refactor を実演で追え、初学者が最初に見る教材として広く薦められています。
よくあるテストパターン集
実務でよく書くテストのパターンをまとめています。FizzBuzz のような単純な関数から、非同期処理・例外処理まで、型ごとのひな形として使えます。
正常系と異常系を分ける
import { describe, it, expect } from "vitest";
import { divide } from "../src/calculator";
describe("divide", () => {
// 正常系: 期待する結果を確認する
it("10 ÷ 2 = 5 を返す", () => {
expect(divide(10, 2)).toBe(5);
});
it("負の数でも正しく割る", () => {
expect(divide(-10, 2)).toBe(-5);
});
// 異常系: 不正入力に対して適切に例外を投げるか
it("0 除算のとき Error を投げる", () => {
expect(() => divide(10, 0)).toThrow("Division by zero");
});
});
非同期関数のテスト
API 通信のように結果が返るまで待つ処理を非同期処理と呼びます。テストでは async / await で完了を待ちます。実際の API を呼ぶとテストが遅く不安定になるため、モック(本物の代役として決まった値を返す偽物)に差し替えます。vi.fn() はモック関数を作る道具、vi.stubGlobal はグローバルな fetch を一時的にモックへ置き換える指定です。似た用語のスタブは「決まった値を返すだけの代役」を指し、モックは「呼ばれ方まで検証できる代役」を指します。用語は現場によって曖昧に使われることもあります。
import { describe, it, expect, vi } from "vitest";
import { fetchUser } from "../src/api";
describe("fetchUser", () => {
it("ID を渡すとユーザーオブジェクトを返す", async () => {
// fetch をモック(外部 API を呼ばない)
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 1, name: "山田" }),
}));
const user = await fetchUser(1);
expect(user.name).toBe("山田");
});
it("API が 404 のとき null を返す", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: false,
status: 404,
}));
const user = await fetchUser(99);
expect(user).toBeNull();
});
});
境界値テスト
ビジネスロジックは境界値(上限・下限・境目)でバグが出やすい。この3点を必ずテストします。
import { describe, it, expect } from "vitest";
import { isAdult } from "../src/ageCheck";
describe("isAdult", () => {
it("17歳は false を返す(境界の手前)", () => {
expect(isAdult(17)).toBe(false);
});
it("18歳は true を返す(境界)", () => {
expect(isAdult(18)).toBe(true);
});
it("19歳は true を返す(境界の次)", () => {
expect(isAdult(19)).toBe(true);
});
it("負の年齢は false を返す(想定外入力)", () => {
expect(isAdult(-1)).toBe(false);
});
});
演習課題
FizzBuzz の次に取り組む題材です。仕様を自分で読み解き、テストを先に書いてから実装してください。
パスワード強度チェッカー
以下の仕様を満たす checkPassword(password: string): "weak" | "medium" | "strong" を TDD で実装してください。
- 8文字未満 → "weak"
- 8文字以上かつ英字と数字のみ → "medium"
- 8文字以上かつ記号(!@#$%^&* など)を含む → "strong"
- 空文字は "weak" を返す
ヒント: テストケースの例
it("8文字未満は weak", () => expect(checkPassword("abc123")).toBe("weak"))
it("英字+数字8文字以上は medium", () => expect(checkPassword("Abc12345")).toBe("medium"))
it("記号を含む8文字以上は strong", () => expect(checkPassword("Abc1234!")).toBe("strong"))
it("空文字は weak", () => expect(checkPassword("")).toBe("weak"))
ショッピングカート合計金額
以下の仕様を満たす calcTotal(items: CartItem[]): number を TDD で実装してください。
- 各アイテムは
{ price: number; qty: number; taxRate: number }の形式 - 合計 = Σ( price × qty × (1 + taxRate) )、小数点以下切り捨て
- items が空配列のとき 0 を返す
- price または qty が 0 以下のアイテムは集計から除外する
ヒント: 型定義
interface CartItem {
price: number;
qty: number;
taxRate: number; // 0.1 = 10%
}
テストを渡して AI に実装させる
演習 A または B のテストファイルを書いたら、お使いの AI アシスタント(例えば Claude Code、Cursor、GitHub Copilot、Gemini CLI など)に以下の指示で実装を生成させてください。その後、自分で出力を検証します。この指示テンプレートはツール共通です。
test/[ファイル名].test.ts のすべてのテストをパスする 最小限の実装を src/[ファイル名].ts に書いてください。 テストで要求されていない機能は追加しないでください。 実装後に npm run test:run を実行して結果を確認してください。
よくある質問
研修中や実務導入初期に多く出る質問をまとめています。
テストを書く時間がない場合はどうすればいいですか?
「テストを書く時間がない」という状況の多くは、実際には「後でテストを書く時間をとるつもりだったが取れなかった」です。TDD は後でテストを追加する手法ではなく、最初からテストを書くことで実装の迷いを減らす手法です。慣れれば実装とテストの合計時間は変わらないか短くなります。まず FizzBuzz だけ TDD で実装する習慣から始めてください。
全ての関数にテストを書く必要がありますか?
必要ありません。業務ロジック・バリデーション・計算処理など「仕様が固まっていてバグると困る部分」を優先してください。DOM 操作や UI の細かい表示、プロトタイプ段階のコードにテストを書くコストは高くなりがちです。「壊れたとき気づきにくい部分」にテストを集中させるのが実用的です。
AI が生成したテストはそのまま使えますか?
使えますが、必ず内容を確認してください。AI はテストの形式は正しく生成しますが、境界値や異常系を省略することがあります。特に「空文字」「0」「負の数」「null」のケースが漏れやすいです。AI が生成したテストを出発点にして、自分で境界値ケースを追加する使い方が現実的です。
Vitest と Jest の違いは何ですか?
Vitest は Jest と API 互換の TypeScript ファーストなテストフレームワークです。Vite プロジェクトなら設定なしで使え、実行速度が Jest より速いのが特徴です。既存の Jest プロジェクトに Vitest を導入する際は、import 文を from "vitest" に変更するだけで動くケースがほとんどです。新規プロジェクトなら Vitest を選ぶのが現時点での標準的な判断です。
テストカバレッジ 100% を目標にすべきですか?
100% は避けた方がよい目標です。カバレッジを上げるためだけにテストを書くと、意味のないテスト(実行されるだけで何も検証しないテスト)が増えます。80% 前後を目安にして、残りの 20% には「テストが難しいUI部分」や「外部サービス連携」が含まれることを許容してください。重要なのはカバレッジの数字ではなく、バグが出やすい部分に十分なテストがあることです。
Red の状態でコミットしても良いですか?
チームのルール次第ですが、一般的にはブランチを分けて Red のコミットを作り、Green になってからマージします。「Red コミット → Green コミット → Refactor コミット」の粒度でコミットすると、後から何を実現しようとしたかが追えて便利です。main ブランチに Red 状態をプッシュするのはチームへの影響があるため避けてください。
チームへの導入ガイド
個人での習得が済んだら、チームへ広げる段階です。一度に全員に強制しても定着しません。段階的に広げる方法を示します。
Phase 1: 試験導入(1〜2週)
- 新規の小さなユーティリティ関数に限定して TDD を試す
- テストファイルのひな形をチームで共有する
- 誰かが詰まったらすぐ相談できる雰囲気を作る
- 成功体験(Green になった瞬間)を言語化して共有する
Phase 2: ルール化(3〜4週)
- 「新規ファイルにはテストを必ず追加する」ルールを合意する
- PR テンプレートにテストの有無チェックを加える
- CI でテスト失敗時にマージをブロックする設定を入れる
- カバレッジをダッシュボードで可視化する
よくある抵抗への対応
| 抵抗の声 | 対応方針 |
|---|---|
| 「テストを書く時間がない」 | 最初の 1〜2 件だけペアで一緒に書いて、時間感覚を体験させる |
| 「仕様がまだ固まっていない」 | 確定している部分だけをテストする。仕様変更はテストの修正で追跡できる |
| 「既存コードにどう適用するか分からない」 | 既存コードはいじらず、新規追加分だけ TDD にする。リプレースは後から |
| 「AI に書かせればいい」 | AI が生成したテストを確認し、不足ケースを追加する役割は人間が担う |