Givery 研修教材 / TDD

テスト駆動開発(TDD)

テストを先に書く。それだけで開発が変わる。

"Write a failing automated test before you write any code." — Kent Beck, 『Test-Driven Development: By Example』2002

TDD とは何か

テスト駆動開発(Test-Driven Development)は、実装コードより先にテストコードを書く開発手法です。Kent Beck が 1990 年代後半に Extreme Programming(XP)の中で体系化し、2002 年に書籍として出版しました。

手法の核心はシンプルです。「動作する最小限の実装を、失敗するテストが要求する形で積み上げる」。これにより、設計の過剰複雑化を防ぎ、変更に強いコードベースが自然に育ちます。

初めての方へ ここで言うテストとは、人が手で画面を操作して確かめる作業ではなく、プログラムがプログラムを自動で検証するコード(自動テスト)を指します。中心になるのはユニットテスト(関数やクラスなど、単体の部品が期待どおり動くかを確認する最小単位のテスト)です。テストコードを1度書いておけば、コマンド1つで何度でも同じ検証を繰り返せます。TDD はこの自動テストを「実装より先に」書くところに特徴があります。

Red → Green → Refactor サイクル

Red テストを書く (失敗する) Green 最小の実装 (通す) Refactor 整理する (テストは保つ) 実装する テスト通過を維持 次のテストへ

各段階が何をするか

  • Red: これから作る機能に対して、まだ通らないテストを1つ書きます。実装が無いので失敗します。この失敗(赤)が出発点です。
  • Green: そのテストを通すためだけの最小限のコードを書きます。きれいさは後回しで、まず緑(成功)にすることだけを目指します。
  • Refactor: テストが緑のまま、重複を消したり名前を整えたりコードを整理します。テストが緑であり続けることが安全網になります。

なぜこの順番なのか

  • 先にテストを書くと、作るものの仕様が具体的なコードとして固定されます。何を実装すべきか迷いにくくなります。
  • 「最小限」に絞ることで、使われない機能を先回りで作り込む過剰実装を防げます。
  • 整理はテストが守ってくれる状態で行うため、リファクタリングで壊しても即座に気づけます。
初学者がいちばん戸惑うところ Red の段階でテストが失敗するのは、間違いではなく正しい進み方です。まだ実装していないのだから失敗して当然、という状態をあえて作ります。ここで「エラーが出た、失敗だ」と手を止めないでください。赤を見てから緑にする、この往復が TDD そのものです。
書籍について Kent Beck 『Test-Driven Development: By Example』(2002)は、TDD の考え方と実践例を示した書籍です。マネーサンプル(Python/Java)とフレームワーク実装(xUnit)の2部構成で、実際に手を動かしながら読む形式になっています。翻訳版は和田卓人さんによる『テスト駆動開発』(オーム社、2017)が入手できます。

主なテストフレームワーク

言語ごとに標準的なフレームワークが確立されています。以下は 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 公式サイトのトップページ
本ページのハンズオンで使う Vitest の公式サイト。導入方法や API リファレンスが揃っています。
出典: vitest.dev

生成 AI 時代になぜ注目されるか

AI がコードを書く時代になったことで、TDD の価値がむしろ上がっています。3 つの理由があります。

01

AI 幻覚の検証手段になる

AI コーディングツール(例えば Claude Code、Cursor、GitHub Copilot など)が生成したコードは、一見正しそうに見えて境界値や例外処理を誤ることがあります。境界値とは、18歳以上を大人とするとき「17・18・19」のような、条件の境目にあたる入力のことです。事前に書いたテストがあれば、生成コードをその場で検証できます。「通った」という事実が、目視の印象より確かな根拠になります。

02

AI への指示精度が上がる

「FizzBuzz を実装して」より「このテストをパスする最小の実装を書いて」のほうが、AI の出力が安定します。テストが仕様書の役割を果たし、曖昧さを減らします。Given-When-Then(前提・操作・期待結果の3つに分けてテストを書く記法)でテストを渡すと、どの入力にどの結果を期待しているかが明確になり、AI の出力が期待値からずれにくくなります。この効果は特定の AI ツールに限りません。

03

テスト生成コストが下がった

以前はテストを書く時間が実装の倍かかることも珍しくありませんでした。AI を使えば、仕様を日本語で説明するだけでテストの雛形が得られます。人間はケースの妥当性確認に集中できます。

AI へのテストファーストの指示例

次のプロンプトは特定ツール専用ではありません。Claude Code / Cursor / GitHub Copilot / Windsurf / Gemini CLI など、お使いの AI コーディングアシスタントの入力欄にそのまま貼り付けて使えます。テストコードを一緒に渡すことで、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");
  });
});
Tips 「最小限の実装」と明示することが大切です。AI は既定でより多くの機能を先回りして実装しようとする傾向があります。TDD の原則である「テストが要求する以上は書かない」を指示に含めると、過剰実装を防げます。この指示はどの AI コーディングツールでも同じように効きます。

準備と気をつけること

環境の前提を確認する

本ページのハンズオンは JavaScript / TypeScript と Vitest を使います。始める前に Node.js(サーバー側でも動く JavaScript 実行環境。npm というパッケージ管理ツールが同梱されます)が入っているか確認してください。ターミナルで次を実行し、バージョン番号が表示されれば準備完了です。

ターミナル(バージョン確認)
node -v   # 例: v20.11.0 のように表示されれば OK
npm -v    # 例: 10.2.4 のように表示されれば OK
Node.js が入っていない場合 Node.js 公式サイトから LTS 版をインストールしてください。LTS は長期サポート版で、研修や実務ではこちらを選ぶのが無難です。インストール後にターミナルを開き直すと 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 をやめる最大の誘因)
  • 全てをユニットテスト(単体の部品を単独で確認するテスト)にしようとして、結合テスト(複数の部品を組み合わせた状態で確認するテスト)を無視する
注意 TDD はすべての場面に適しているわけではありません。プロトタイプ検証や UI の試行錯誤では、先にテストを書くコストが高くなりがちです。安定した仕様がある業務ロジック・ライブラリ・API 実装で特に効果を発揮します。

ハンズオン: お手元の 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 を体感してみてください。

このハンズオンの進み方 Step 2 でテストを先に書き、Step 3 でわざと失敗(Red)させ、Step 4〜5 で最小実装を書いて成功(Green)させ、Step 6 でテストを足してリファクタリングし、Step 7 で同じことを AI に任せて検証します。1 サイクル通すと TDD の流れが体でつかめます。
1
準備

プロジェクトの初期化

まず作業フォルダを作り、テストフレームワーク Vitest を導入します。Vitest はテストを実行するための道具で、これがないと自動テストを走らせられません。

ターミナル
mkdir tdd-fizzbuzz && cd tdd-fizzbuzz
npm init -y

ここで mkdir はフォルダ作成、cd はそのフォルダへ移動、npm init -y はプロジェクト管理ファイル package.json を既定値で生成するコマンドです。続いて、生成された package.json を次の内容で上書きします。scripts にテスト実行コマンドを登録しておくと、以降は短いコマンドでテストを回せます。

package.json の内容(上書きしてください)
{
  "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 フォルダを用意します。

2
Red

テストファイルを先に作成する

実装より先にテストを書きます。これが TDD の肝です。test/fizzbuzz.test.ts を新規作成し、以下を貼り付けてください。src/fizzbuzz.ts はまだ作りません。テストが「こう動いてほしい」という仕様を、実行できる形で表しています。

test/fizzbuzz.test.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");
  });
});
Given-When-Then とは テストを「前提(Given)」「操作(When)」「期待結果(Then)」の3つに分けて考える整理術です。上のコメントのように分けて書くと、何を検証しているかが一目で分かり、後から読む人にも AI にも意図が伝わりやすくなります。
3
Red 確認

テストを実行して失敗させる

この段階では src/fizzbuzz.ts が存在しないため、テストは必ず失敗します。失敗を確認することが TDD の出発点です。

ターミナル
npm run test:run
期待される出力(エラー)
ERROR: Cannot find module '../src/fizzbuzz'
FAIL  test/fizzbuzz.test.ts
この失敗は成功です ここで赤い FAIL が出るのは想定どおりで、間違いではありません。まだ実装ファイルが無いのだから失敗して当然です。エラーメッセージ(Cannot find module)は「読み込もうとした src/fizzbuzz が見つからない」という意味で、次のステップで実装を作れば解消します。TDD ではこの Red をあえて先に見て、これから何を通すのかをはっきりさせます。
Tips 逆に、実装前なのにテストが最初から通ってしまう場合は要注意です。テスト自体が何も検証していない(expect の書き忘れなど)可能性があります。まず失敗を確認できてこそ、後で緑になったことに意味が生まれます。
4
Green

最小限の実装を書く

いよいよ実装します。ここでは飾らず、テストを通すのに必要なだけのコードを書きます。src/fizzbuzz.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);
}
なぜ判定の順番が大事か 15 は 3 でも 5 でも割り切れます。もし 3 の判定を先に書くと、15 を渡したときに "Fizz" で確定してしまい "FizzBuzz" になりません。条件が重なるときは、より厳しい条件(ここでは 15)を先に判定します。この気づきは「15 のテスト」を先に書いていたからこそ得られます。
5
Green 確認

テストを再実行して成功させる

実装ができたので、もう一度テストを走らせます。今度は全部緑(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)
ここまでで1サイクル Red(Step 3)→ Green(Step 5)を1周しました。この時点で実装は正しく動いています。次の Step 6 で、動作を保ったままコードを整える Refactor と、新しい仕様を足す流れを体験します。
6
Refactor

テストを追加してリファクタリングする

仕様が増える場面を体験します。「0 や負の数を渡したら "Invalid"(無効)を返す」というルールを足すとします。TDD では、まずこの仕様を表すテストを追加します。追加した直後はまだ実装が対応していないので、そのテストは失敗(Red)します。失敗を確認してから実装を直し(Green)、余裕があればコードを整理(Refactor)します。

test/fizzbuzz.test.ts に追加(describe の中に足す)
  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);
}
7
AI 連携

同じことを AI アシスタントにやらせる

ここまでを手で体験したら、今度は AI に実装させてみます。お使いの AI コーディングアシスタント(例えば Claude Code なら VS Code や Cursor の拡張として、GitHub Copilot ならチャット欄から、Gemini CLI ならターミナルから)に、次の指示を入力してください。テストファイル名を明示すると、AI の出力が仕様に沿ったものになります。この指示文はツールに依存せず、そのまま使い回せます。

AI アシスタントへの指示(ツール共通)
test/fizzbuzz.test.ts のすべてのテストをパスする最小限の実装を
src/fizzbuzz.ts に書いてください。
テストで要求されていない機能は追加しないでください。
Tips AI が実装を生成したら、鵜呑みにせず必ず 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 開発の上流から設計を管理したい
現場での組み合わせ 実際の開発では TDD でユニットレベルを固め、Playwright で E2E を補完するパターンが多いです。BDD は QA やビジネスサイドとの連携が必要な場面で追加します。SDD は AI 駆動開発でコンテキストを仕様書で管理する新しいアプローチです。
BDD と TDD の違いをもう少し詳しく知りたい方へ

TDD は「開発者がコードの動作を保証するためのテスト」を先に書きます。テストコードは技術者向けです。

BDD は「ユーザーや PO が理解できる自然言語に近い記法」でシナリオを書き、それを自動テストに変換します。Cucumber の場合、Gherkin 記法(Given/When/Then)で書いた .feature ファイルがそのままテスト仕様書になります。

どちらを使うかは「テストの読み手が誰か」で決まります。開発チーム内だけで完結するなら TDD で十分です。

参考資料

Martin Fowler の TDD 解説ページ
Martin Fowler による 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 のような単純な関数から、非同期処理・例外処理まで、型ごとのひな形として使えます。

正常系と異常系を分ける

test/calculator.test.ts
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 を一時的にモックへ置き換える指定です。似た用語のスタブは「決まった値を返すだけの代役」を指し、モックは「呼ばれ方まで検証できる代役」を指します。用語は現場によって曖昧に使われることもあります。

test/api.test.ts
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点を必ずテストします。

test/ageCheck.test.ts
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);
  });
});
パターンの原則 1テストにつき1つの振る舞いだけを検証してください。複数の assert を詰め込むとどこで失敗したかが分からなくなります。「テスト名を読めば何を確認しているかが分かる」状態が理想です。

演習課題

FizzBuzz の次に取り組む題材です。仕様を自分で読み解き、テストを先に書いてから実装してください。

A
演習 A

パスワード強度チェッカー

以下の仕様を満たす 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"))
B
演習 B

ショッピングカート合計金額

以下の仕様を満たす 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%
}
C
AI 連携

テストを渡して AI に実装させる

演習 A または B のテストファイルを書いたら、お使いの AI アシスタント(例えば Claude Code、Cursor、GitHub Copilot、Gemini CLI など)に以下の指示で実装を生成させてください。その後、自分で出力を検証します。この指示テンプレートはツール共通です。

AI アシスタントへの指示テンプレート(ツール共通)
test/[ファイル名].test.ts のすべてのテストをパスする
最小限の実装を src/[ファイル名].ts に書いてください。
テストで要求されていない機能は追加しないでください。
実装後に npm run test:run を実行して結果を確認してください。
検証チェックリスト AI が実装を提案したら、以下を確認してください。テストが全部グリーンか、追加テストで仕様外の挙動がないか、不必要に複雑なコードになっていないか。テストという合否基準があるので、どの AI ツールでも同じ基準で判断できます。

よくある質問

研修中や実務導入初期に多く出る質問をまとめています。

テストを書く時間がない場合はどうすればいいですか?

「テストを書く時間がない」という状況の多くは、実際には「後でテストを書く時間をとるつもりだったが取れなかった」です。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 が生成したテストを確認し、不足ケースを追加する役割は人間が担う
最初の一歩として 一番効果が大きいのは「CI でテストが落ちたらマージ不可」の設定です。これがあると、テストを書かないままコードを出せなくなるため、自然と書く習慣が定着します。GitHub Actions なら 10 行の YAML 設定で実現できます。

次に読む

もう片方の開発原則も確認しましょう

GUIDE TOP
ガイドトップに戻る
TDD と SDD の入口ページ
SDD
仕様駆動開発を見る →
Spec → Plan → Tasks → Implement