Use Cases (更新: 2026/6/2)

Claude CodeでTDDを実践する方法: Vitestとnode:testで学ぶテスト駆動開発

Claude CodeでTDDを実践する手順を、Vitestとnode:testの動く例、CI、hooks、依頼テンプレ付きで解説。

Claude CodeでTDDを実践する方法: Vitestとnode:testで学ぶテスト駆動開発

Claude Codeに「この機能を作って」と頼むと、動くコードは早く出ます。しかし初心者ほど、その速さで落とし穴に入ります。仕様の読み違い、境界値の抜け、古いテストの破壊、CIでだけ落ちる差分が、後からまとめて見つかるからです。

そこで有効なのがTDDです。TDDはTest-Driven Developmentの略で、日本語ではテスト駆動開発です。難しく聞こえますが、やることは「先に失敗するテストを書く、最小実装で通す、読みやすく直す」の繰り返しです。この3段階はRed-Green-Refactorとも呼ばれます。Redは失敗、Greenは成功、Refactorは振る舞いを変えずに整理することです。

Claude CodeとTDDは相性が良いです。人間が退屈に感じやすいテストケースの列挙、失敗ログの読み取り、境界値の追加、CI設定の整備を、会話の中でまとめて進められるからです。ただし、Claude Codeに丸投げすると「実装に合わせたテスト」を後から作るだけになりがちです。この記事では、テストを先に固定し、Claude Codeを安全に動かすための依頼文、Vitestとnode:testのコピペで動くコード、CI、hooks、失敗例までまとめます。

公式仕様は変わるため、この記事では2026年6月2日時点でClaude Code hooks referenceClaude Code memoryClaude Code settingsVitest Getting StartedVitest CLINode.js test runnerを確認しています。特にhooksは、古い記事にあるcommand直書きやCLAUDE_FILE_PATH前提の例をそのまま使わず、現在のJSON stdin形式に合わせます。

TDDでClaude Codeに任せる範囲

Claude Codeに任せやすいのは、テスト観点の洗い出し、テストファイル作成、最小実装、失敗ログの解釈、CI設定、レビュー観点の整理です。逆に、人間が先に決めるべきなのは、仕様の優先順位、収益やセキュリティに関わる許容範囲、外部APIの契約、公開判断です。

段階Claude Codeへの指示人間が見ること
Red仕様から失敗テストを書く仕様を勝手に足していないか
Greenテストを通す最小実装にする余計な抽象化や副作用がないか
Refactor重複を減らし名前を整える振る舞いが変わっていないか
CInpm testを自動実行する本番に近いNodeバージョンか
運用hooksやCLAUDE.mdで習慣化する遅すぎる自動処理になっていないか

概念図にすると、Claude Codeは真ん中の作業を速く回す道具です。仕様と合格ラインは人間が握ります。

flowchart LR
  A["仕様を小さく切る"] --> B["Red: 失敗テスト"]
  B --> C["Green: 最小実装"]
  C --> D["Refactor: 整理"]
  D --> E["CIとhooksで再実行"]
  E --> B

実例1: Vitestで価格計算をRed-Green-Refactorする

最初のユースケースは、クーポン付きの価格計算です。EC、教材販売、SaaSの割引など、収益に近いロジックはTDD向きです。1円のズレや期限切れクーポンの見落としが、そのまま売上や信頼に影響するからです。

まず、プロジェクトにVitestを入れます。Vitest公式はnpm install -D vitestを案内しており、現在のVitestはNode 20以上を要求します。

npm install -D vitest

package.jsonには次のように入れます。

{
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  }
}

Redでは、まだ実装がない状態でテストを書きます。src/cart.test.tsを作ります。

import { describe, expect, it } from "vitest";
import { priceCart, ValidationError } from "./cart";

describe("priceCart", () => {
  it("calculates subtotal and total without a coupon", () => {
    const result = priceCart({
      items: [
        { sku: "book", unitPriceCents: 1200, quantity: 2 },
        { sku: "video", unitPriceCents: 3000, quantity: 1 },
      ],
    });

    expect(result).toEqual({
      subtotalCents: 5400,
      discountCents: 0,
      totalCents: 5400,
    });
  });

  it("applies a valid percent coupon", () => {
    const result = priceCart(
      {
        items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
        coupon: {
          code: "SPRING20",
          percentOff: 20,
          expiresAt: "2026-06-30T00:00:00.000Z",
        },
      },
      { now: new Date("2026-06-02T00:00:00.000Z") },
    );

    expect(result.totalCents).toBe(8000);
    expect(result.discountCents).toBe(2000);
  });

  it("rejects expired coupons", () => {
    expect(() =>
      priceCart(
        {
          items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
          coupon: {
            code: "OLD20",
            percentOff: 20,
            expiresAt: "2026-05-01T00:00:00.000Z",
          },
        },
        { now: new Date("2026-06-02T00:00:00.000Z") },
      ),
    ).toThrow(ValidationError);
  });

  it("rejects zero or negative quantity", () => {
    expect(() =>
      priceCart({
        items: [{ sku: "book", unitPriceCents: 1200, quantity: 0 }],
      }),
    ).toThrow("quantity must be positive");
  });
});

この時点でnpm testを実行すると、./cartが存在しないため失敗します。これがRedです。Claude Codeにはこの失敗を見せてから実装させます。

今はRedの段階です。src/cart.test.tsを先に書きました。
まだsrc/cart.tsはありません。

お願い:
1. npm testを実行して失敗を確認してください。
2. テストを通す最小のsrc/cart.tsだけを実装してください。
3. 余計な機能、DB、UI、外部APIは追加しないでください。
4. Greenになった後に、重複や命名だけをリファクタリングしてください。

Greenの最小実装は次の通りです。src/cart.tsとして保存すれば、上のテストと合わせて動きます。

export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

type CartItem = {
  sku: string;
  unitPriceCents: number;
  quantity: number;
};

type Coupon = {
  code: string;
  percentOff: number;
  expiresAt: string;
};

type CartInput = {
  items: CartItem[];
  coupon?: Coupon;
};

type PriceOptions = {
  now?: Date;
};

export function priceCart(input: CartInput, options: PriceOptions = {}) {
  if (input.items.length === 0) {
    throw new ValidationError("cart must contain at least one item");
  }

  const subtotalCents = input.items.reduce((sum, item) => {
    if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
      throw new ValidationError("quantity must be positive");
    }
    if (!Number.isInteger(item.unitPriceCents) || item.unitPriceCents < 0) {
      throw new ValidationError("unitPriceCents must be a non-negative integer");
    }
    return sum + item.unitPriceCents * item.quantity;
  }, 0);

  const discountCents = calculateDiscount(subtotalCents, input.coupon, options.now ?? new Date());

  return {
    subtotalCents,
    discountCents,
    totalCents: subtotalCents - discountCents,
  };
}

function calculateDiscount(subtotalCents: number, coupon: Coupon | undefined, now: Date) {
  if (!coupon) return 0;

  if (coupon.percentOff <= 0 || coupon.percentOff > 100) {
    throw new ValidationError("percentOff must be between 1 and 100");
  }

  if (new Date(coupon.expiresAt).getTime() < now.getTime()) {
    throw new ValidationError("coupon expired");
  }

  return Math.round(subtotalCents * (coupon.percentOff / 100));
}

ここで重要なのは、AIに「もっと汎用的に」と頼まないことです。TDDのGreenは最小で十分です。先に大きな設計へ広げると、テストが守っていない抽象化、未使用の設定、存在しない将来要件が混ざります。

実例2: node:testでCLI入力の境界値を確認する

2つ目のユースケースはCLIや小さなユーティリティです。外部ライブラリを増やしたくないNode.jsプロジェクトでは、標準のnode:testが便利です。Node.js公式ドキュメントでは、node --testでテストランナーを起動でき、*.test.jsなどのファイル名パターンが自動検出されます。

次のファイルはlimit.test.mjsとしてそのまま実行できます。

import test from "node:test";
import assert from "node:assert/strict";

export function parseLimit(value, fallback = 20) {
  if (value === undefined || value === "") return fallback;

  const parsed = Number(value);
  if (!Number.isInteger(parsed)) {
    throw new TypeError("limit must be an integer");
  }
  if (parsed < 1 || parsed > 100) {
    throw new RangeError("limit must be between 1 and 100");
  }

  return parsed;
}

test("parseLimit uses fallback when the value is empty", () => {
  assert.equal(parseLimit(undefined), 20);
  assert.equal(parseLimit("", 50), 50);
});

test("parseLimit accepts values from 1 to 100", () => {
  assert.equal(parseLimit("1"), 1);
  assert.equal(parseLimit("100"), 100);
});

test("parseLimit rejects decimals and out-of-range values", () => {
  assert.throws(() => parseLimit("1.5"), /integer/);
  assert.throws(() => parseLimit("0"), /between 1 and 100/);
  assert.throws(() => parseLimit("101"), /between 1 and 100/);
});
node --test limit.test.mjs

Claude Codeへの依頼では、「境界値を増やして」ではなく、「1、100、0、101、小数、空文字、未指定を含める」と具体化します。境界値とは、仕様の端にある値です。多くのバグは真ん中の普通の値ではなく、この端で起きます。

実例3: API変更を回帰テストとして固定する

3つ目のユースケースは、既存APIのバグ修正です。たとえば「期限切れクーポンがAPIで通ってしまった」という障害があったなら、最初に失敗テストを固定します。

既存APIの回帰テストをTDDで追加してください。

背景:
- 期限切れクーポンがPOST /checkoutで通ってしまった。
- 修正後も、正常なクーポンとクーポンなし購入は壊したくない。

Red:
- まず期限切れクーポンで400を期待するテストを追加。
- 現在の実装でそのテストが失敗することを確認。

Green:
- API実装を最小変更してテストを通す。

Refactor:
- 日付比較の重複だけを関数に切り出す。

完了条件:
- 追加したテスト名、失敗ログ、修正ファイル、実行したコマンドを報告。

この依頼は、Claude Codeに「障害の再発防止」という目的を渡しています。単なる実装依頼より、レビューしやすい差分になります。API全体のテスト設計はClaude CodeでAPIテストを自動化する実践ガイド、より広いテスト設計はClaude Codeテスト戦略完全ガイドも合わせて読むとつながります。

CIでTDDを止めない

ローカルでGreenになっても、CIで落ちるなら完成ではありません。GitHub Actionsでは、Nodeのバージョンを明示し、npm cinpm testを分けます。

name: test
on:
  pull_request:
  push:
    branches: [main]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm test

Claude Codeには、CI設定もTDDの一部として依頼します。

テストがローカルでGreenになった後、CI設定も確認してください。
- package.jsonのtest scriptがCIで動くこと
- GitHub ActionsでNode 22を使うこと
- npm ciの後にnpm testを実行すること
- 変更内容をPR本文向けに短くまとめること

hooksとCLAUDE.mdでTDDを習慣にする

Claude Codeのhooksは、ツール実行後にコマンドを動かす仕組みです。公式ドキュメントでは、PostToolUseのhookはツール実行後に発火し、command hookにはイベントJSONがstdinで渡されます。古いサンプルにある環境変数だけに依存すると動かない場合があるため、JSONからtool_input.file_pathを読みます。

.claude/settings.jsonの例です。

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/run-related-vitest.mjs",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

.claude/hooks/run-related-vitest.mjsは次のようにします。

import { spawnSync } from "node:child_process";
import path from "node:path";

let raw = "";
for await (const chunk of process.stdin) {
  raw += chunk;
}

const event = raw ? JSON.parse(raw) : {};
const filePath = event.tool_input?.file_path;

if (typeof filePath !== "string" || !/\.[cm]?[jt]sx?$/.test(filePath)) {
  process.exit(0);
}

const target = path.isAbsolute(filePath)
  ? path.relative(process.cwd(), filePath)
  : filePath;

const result = spawnSync("npx", ["vitest", "related", target, "--run"], {
  stdio: "inherit",
  shell: process.platform === "win32",
});

process.exit(result.status ?? 1);

このhookは便利ですが、巨大なテストを毎回走らせると開発が遅くなります。最初は関連テストだけ、失敗時だけ詳細ログ、重いE2EはCIに任せる、という分担が現実的です。hooks全体の考え方はClaude Code hooksガイドにも内部リンクとして整理しておくと、チームで説明しやすくなります。

CLAUDE.mdには、行動ルールを短く書きます。Claude Code公式のmemoryドキュメントでは、CLAUDE.mdがプロジェクト文脈として読み込まれること、AGENTS.mdを使うリポジトリではCLAUDE.mdからimportできることが説明されています。

## TDD workflow
- Behavior changes start with a failing test.
- Show the Red result before implementation.
- Implement the smallest change that makes the test pass.
- Refactor only after the targeted test is Green.
- Report the command, result, changed files, and remaining risk.

Claude CodeへのTDD依頼テンプレート

テンプレートは毎回ゼロから考えない方が安定します。次の4つを状況に合わせて使い分けます。

新機能TDDテンプレート:
目的:
  [機能名]を追加する。
仕様:
  - [正常系]
  - [境界値]
  - [失敗時の扱い]
進め方:
  1. テストを先に書く。
  2. npm testでRedを確認する。
  3. 最小実装でGreenにする。
  4. Refactorは振る舞いを変えない範囲に限定する。
完了時:
  失敗ログ、実行コマンド、変更ファイル、残リスクを報告。
バグ修正TDDテンプレート:
再現条件:
  [ユーザー操作/API入力/ログ]
期待:
  [本来の結果]
現在:
  [実際の結果]
依頼:
  まず再現テストを追加し、現在の実装で失敗することを確認してください。
  その後、最小修正でGreenにしてください。
  既存テストを弱めたり削除したりしないでください。
リファクタリングTDDテンプレート:
対象:
  [ファイル/関数]
制約:
  公開APIと既存UIの振る舞いは変えない。
手順:
  1. 現在の振る舞いをcharacterization testとして固定する。
     characterization testは、今の動きを仕様として写し取るテストです。
  2. テストがGreenであることを確認する。
  3. 内部構造だけを整理する。
  4. 同じテストがGreenのままか確認する。
CI追加テンプレート:
目的:
  TDDで追加したテストをPRごとに必ず実行する。
確認:
  - package.jsonのtest script
  - Nodeバージョン
  - npm ci
  - npm test
  - 失敗時に原因が読めるログ
依頼:
  既存CIの形式に合わせて最小差分で追加してください。

よくある落とし穴

1つ目は、Redを確認しないことです。失敗するはずのテストが最初から通るなら、そのテストはバグを検出できていません。Claude Codeには必ず「現在の実装で失敗するログを見せて」と依頼します。

2つ目は、実装詳細に寄りすぎたテストです。内部関数名、配列の並び、モックの呼ばれた回数だけを見ていると、利用者にとって重要な振る舞いを守れません。価格計算なら、見るべきは合計、割引、エラーです。

3つ目は、日付と時刻を固定しないことです。new Date()をテスト内で直接使うと、翌月に落ちるテストになります。上の例のようにnowを注入します。注入とは、関数の外から値を渡して、テストで制御できるようにすることです。

4つ目は、モックだけで安心することです。モックは外部サービスの代役ですが、代役だけを見ていると、本物のAPI契約との差分を見逃します。決済、メール、CRMのような収益導線では、単体テストに加えてステージングや契約テストも必要です。

5つ目は、Claude Codeにテスト削除を許すことです。「通すために古いテストを調整しました」と出たら危険です。削除や期待値変更が必要な場合は、理由を先に説明させます。

6つ目は、hooksを重くしすぎることです。編集のたびに全E2Eを走らせると、Claude Codeの作業が遅くなり、結局hookを無効化したくなります。関連テスト、lint、型チェックのように短い処理から始めます。

初心者が最初に決めるレビュー観点

初心者がTDDで迷いやすいのは、どこまでテストを書けばよいかです。すべての行をテストしようとすると疲れますし、正常系だけにするとTDDの価値が出ません。最初は「お金」「権限」「外部連携」「データ削除」「日付」の5つを優先してください。価格計算、ログイン、Webhook、削除API、期限切れ判定は、失敗したときの影響が大きいからです。

Claude Codeに渡すときも、この優先順位を明記します。「正常系を1つ、失敗系を2つ、境界値を2つ入れて」と頼むだけで、出力はかなり安定します。たとえば価格計算なら、通常購入、期限切れクーポン、0個注文、100%割引、端数丸めを入れます。CLIなら、未指定、最小値、最大値、小数、範囲外を入れます。APIなら、認証なし、不正JSON、存在しないID、重複リクエストを入れます。

レビューでは、テスト名が仕様として読めるかを見ます。workstest case 1のような名前では、半年後に何を守っているのか分かりません。rejects expired couponsreturns 401 without a bearer tokenのように、期待する振る舞いをそのまま名前にします。Claude Codeが曖昧な名前を出したら、実装より先にテスト名を直させます。

もう1つ大切なのは、失敗ログを短く残すことです。TDDではRedの証拠があるから、あとで「本当にバグを捕まえるテストだったのか」を確認できます。Masaの運用では、PR本文に「Redで落ちたテスト名」「Greenにしたコマンド」「まだ見ていない範囲」を3行だけ残すようにしたところ、レビュー担当者が差分を追いやすくなりました。これは大げさなドキュメントではなく、AIが速く動いた証拠を人間が確認できる形にするためのメモです。

小さなチームでは、この3行メモが引き継ぎにも効きます。翌日に別の人が続きを見る場合でも、どの失敗を直したのか、どの確認をまだしていないのかが分かります。Claude Codeの作業速度が上がるほど、人間側の判断材料を残す価値は大きくなります。

CTA: TDDの型をチームや記事運用に固定する

Claude CodeでTDDを始めるなら、最初から大規模なテスト基盤を作る必要はありません。1つの価格計算、1つのCLI入力、1つのAPIバグから始めて、Red-Green-Refactorの証拠を残す方が定着します。

日常コマンドと確認手順を手元に置きたい場合は無料チートシートから始めてください。プロンプト、hooks、CLAUDE.md、レビュー観点をまとめて整えたい場合は商品一覧が近道です。チームで既存リポジトリにTDD、CI、権限設計、レビュー運用まで入れるならClaude Code研修・導入相談で実コード前提に整理できます。

この記事で紹介した内容を実際に試した結果

Masaの運用では、Claude Codeに最初から「実装して」と頼むより、Redの失敗ログを先に作らせる方がレビュー時間を短くできました。特に価格計算とAPI回帰テストでは、期限切れクーポン、0個注文、未認証リクエストのような端のケースが早い段階で見つかりました。一方で、hookで全テストを毎回走らせる設定は遅すぎたため、関連VitestだけをPostToolUseで走らせ、E2EはCIに任せる形に落ち着きました。TDDは儀式ではなく、Claude Codeの速さを壊れにくい差分へ変えるための実務的な足場です。

#Claude Code #TDD #テスト駆動開発 #テスト #品質保証
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。