Claude Codeに「このコードのテストを書いて」と頼むと、数秒でユニットテストが生成されます。しかしそのテストが本当に品質を担保できているかどうかは別の話です。何も考えずに使うと「テストが通るだけのコード」や「実装を追認するだけのテスト」が生まれやすく、テストを書いた意味がなくなります。
この記事では、Claude Codeをテストに活用するための正しい使い方を解説します。テスト後付けバイアスの問題・TDDをClaude Codeで実現するマルチエージェント構成・Jest/Vitest/pytestとの実践的な連携設定・Hooksでテストを強制する方法まで、実務で使えるパターンを体系的に紹介します。Claude Codeの基本はClaude Code完全ガイド、Hooksの詳細はHooks完全ガイドも参照してください。
「テストを書いて」だけでは品質が上がらない理由
Claude Codeがテストを生成するとき、実装コードを見ながらテストを書く構造になっています。これは「テスト後付けバイアス」と呼ばれる問題を引き起こします。
| 問題 | 具体例 |
|---|---|
| 追認テスト | 実装がreturn x * 2のとき、expect(fn(3)).toBe(6)というテストを書く。仕様バグが混入していてもそのままテストが通る |
| ハッピーパス偏重 | 正常系しかテストしない。エラー処理・境界値・nullチェックなどの異常系が手薄になる |
| コード構造依存 | 実装の内部構造に依存したテストを書くため、リファクタリングのたびにテストが壊れる |
| カバレッジ水増し | 意味のないテストを大量生成してカバレッジ数値だけを高くする |
これらの問題を避けるには、「何を検証したいか」を先に決めてからテストを書くというテスト駆動開発(TDD)の原則をClaude Codeに守らせる設計が必要です。
基本的なユニットテスト生成の使い方
効果的なテスト生成プロンプト
テストを依頼するときは、コードを渡すだけでなく仕様・期待動作・テストすべきケースを一緒に伝えることで品質が上がります。
| プロンプト例 | 生成されるテストの品質 |
|---|---|
| 「このコードのテストを書いて」 | 追認テストになりやすい(低品質) |
| 「この関数の仕様は〜。境界値・エラーケースも含めてテストを書いて」 | 仕様に基づくテストになる(中品質) |
| 「先にテストケース一覧を列挙して、承認後にコードを書いて」 | 何をテストするかを合意してから書く(高品質) |
テストケース一覧を先に確認するフロー
- まずClaudeに「〜という仕様があります。実装コードを見ずに、どんなテストケースが必要か箇条書きで列挙してください」と指示
- Claudeが提示したテストケース一覧を確認・修正
- 「承認したので、このリストに従ってテストコードを書いて」と指示
実装を見てからテストを書かせると追認テストになりやすいため、テスト設計フェーズでは意図的に実装から切り離すことが重要です。サブエージェントを使う方法は後述します。
テストフレームワーク別の連携設定
Claude Codeはプロジェクト内のテストフレームワークを自動検出します。ただしCLAUDE.mdにテストコマンドを明記しておくことで、Claudeが自律的にテスト→失敗確認→修正→再テストのサイクルを回せるようになります。
Jest / Vitest(TypeScript/JavaScript)
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
})
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
pytest(Python)
[pytest] testpaths = tests addopts = -v --tb=short --cov=src --cov-report=term-missing --cov-fail-under=80
pip install pytest pytest-cov pytest-asyncio # テスト実行 pytest # カバレッジレポートをHTMLで出力 pytest --cov=src --cov-report=html
CLAUDE.mdへのテスト設定記載
CLAUDE.mdにテストコマンドと基準を記載しておくと、Claudeがコードを変更するたびに自動でテストを実行し、失敗を認識して修正するようになります。CLAUDE.mdの書き方の詳細はCLAUDE.md完全設計ガイドを参照してください。
## テスト ### テストコマンド - 単体テスト: `npm test` または `pytest` - カバレッジ確認: `npm run test:coverage` または `pytest --cov=src` - 特定ファイル: `npx vitest run src/utils/` または `pytest tests/test_utils.py` ### テストルール - コードを変更したら必ずテストを実行すること - 新機能追加時は対応するテストを必ず書くこと - カバレッジは80%以上を維持すること - テストは正常系だけでなく境界値・エラーケースも含めること - テストが通るためだけのハードコーディングや条件分岐を実装コードに入れない
TDDをClaude Codeで実現する:3段階マルチエージェント構成
本格的なTDDをClaude Codeで実現したい場合、テスト作成・実装・リファクタリングを別々のサブエージェントに分担させるアプローチが有効です。同一コンテキスト内で全工程を実行すると追認テストになりやすいため、コンテキストを分離することでTDDの原則を守らせます。サブエージェントの基本はSubagents完全ガイドを参照してください。
RED:失敗するテストを書く(テスト専用エージェント)
最初のサブエージェントには仕様のみを渡し、実装コードを見せないことが重要です。
claude "以下の仕様を満たすコードのテストを書いてください。 実装ファイルは見ないでください(まだ存在しません)。 失敗するテストコードだけを書くことが目的です。 【仕様】 - formatPrice(price: number, currency: string): string 関数を実装する - 価格を3桁カンマ区切りでフォーマットする - 通貨コード(USD/JPY/EUR)に応じて記号($/¥/€)を先頭に付ける - 負の数は受け付けない(エラーをthrowする) - 小数点以下はUSD/EURは2桁、JPYは0桁"
GREEN:テストを通す最小実装(実装専用エージェント)
2番目のサブエージェントには失敗するテストのみを渡し、それを通す最小限のコードを書かせます。
claude "次のテストコードがすべて通るように実装してください。 テストを変更することは禁止です。 最小限のコードで通してください。 $(cat tests/formatPrice.test.ts)"
REFACTOR:品質改善(リファクタリング専用エージェント)
claude "以下のコードをリファクタリングしてください。 テストはすべて通ったままにすること。 チェックポイント: - 重複コードの除去 - マジックナンバー・文字列をconstに切り出す - エラーメッセージを具体的にする - 型定義を明示的にする"
実際のプロジェクトでこの構成を採用したケースでは、テストカバレッジが60%から90%以上に向上し、バグ修正コストが60〜75%削減されたという報告があります。コンテキスト分離がそれだけ追認テスト防止に効いています。
Hooksでテスト自動実行を強制する
CLAUDE.mdにテスト指示を書いても、Claudeがそれを無視することがあります。Hooksはそれを「確定実行」に変える仕組みです。ファイル編集のたびに自動でテストを走らせ、失敗したらその結果をClaudeに即座にフィードバックできます。
PostToolUseフック:ファイル編集後に自動テスト実行
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_DIR && npm test -- --reporter=verbose 2>&1 | tail -30"
}
]
}
]
}
}
このフックはファイル編集のたびにテストを実行し、結果(最後30行)をClaudeのコンテキストに追加します。テストが失敗した場合、Claudeはエラーメッセージをもとにすぐ修正します。
PreToolUseフック:テストなしのコミットを防ぐ
#!/bin/bash
# stdin からツール情報を受け取る
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# git commit コマンドの場合にテストを強制
if echo "$CMD" | grep -q "git commit"; then
echo "テスト実行中..." >&2
cd "$CLAUDE_PROJECT_DIR"
if ! npm test -- --run 2>&1; then
echo "テストが失敗しています。コミット前にテストを通してください。" >&2
exit 2 # exit 2 でコマンドをブロック
fi
fi
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/require-tests.sh"
}
]
}
]
}
}
全ファイル編集のたびにテストフルスイートを実行すると非常に遅くなります。
matcherを特定ディレクトリのファイル変更のみに絞るか、高速なユニットテストのみを実行するコマンドを指定してください。E2Eテストはコミット前のフックに限定するのがおすすめです。カバレッジを上げるアプローチ
未テスト箇所を特定して集中的に補う
まずカバレッジレポートを生成し、Claudeにそれを読ませて優先すべき未テスト箇所を判断させます。
# カバレッジレポートをJSON形式で生成(Vitest) npx vitest run --coverage --reporter=json # Claudeに分析させる claude "coverage/coverage-summary.json を読んで、 カバレッジが低い箇所を特定し、優先度が高い順にテストを追加してください。 追加後に npm run test:coverage を実行してカバレッジを確認してください"
Writer/Reviewerパターンで客観的なテストレビュー
同じコンテキストでテストを書いたものをレビューしても主観が入ります。別セッションでレビューさせることで客観性が生まれます。
| セッション | 役割 | 指示例 |
|---|---|---|
| Session A | テスト作成者 | 「この仕様のテストを書いて」 |
| Session B(新規) | テストレビュアー | 「このテストコードのレビューをしてください。追認テストになっていないか・異常系が漏れていないかを確認してください」 |
| Session A | テスト改善者 | Session Bのレビュー結果を受けて改善 |
CLAUDE.mdにカバレッジ基準を強制する
## テスト品質基準 - カバレッジは常に80%以上を維持すること - カバレッジが80%を下回った場合は、コード変更を完了させる前に 追加テストを書いてカバレッジを回復すること - `npm run test:coverage` の出力でthresholdエラーが出た場合は それを解消することを優先すること
フレームワーク別テスト生成のコツ
Vitest / Jest(TypeScript)
// Claude への指示
// 「以下の関数に対するテストを書いてください。
// - 正常系: 典型的な入力と期待出力
// - 境界値: 最小値・最大値・空文字・0
// - 異常系: null/undefined・型違い・範囲外の値
// モック化が必要な外部依存があれば vi.mock() を使ってください」
// 生成されるテスト例
import { describe, it, expect, vi } from 'vitest'
import { formatPrice } from '../src/formatPrice'
describe('formatPrice', () => {
// 正常系
it('USD: 3桁カンマ区切りと$記号', () => {
expect(formatPrice(1000, 'USD')).toBe('$1,000.00')
})
it('JPY: 小数なし、¥記号', () => {
expect(formatPrice(1000, 'JPY')).toBe('¥1,000')
})
// 境界値
it('0円は正常に処理される', () => {
expect(formatPrice(0, 'USD')).toBe('$0.00')
})
// 異常系
it('負の値はエラーをthrowする', () => {
expect(() => formatPrice(-1, 'USD')).toThrow()
})
it('不正な通貨コードはエラーをthrowする', () => {
expect(() => formatPrice(100, 'XXX')).toThrow()
})
})
pytest(Python)
# Claude への指示
# 「以下の Python 関数のテストを pytest で書いてください。
# 正常系・境界値・例外系を網羅してください。
# 外部依存は pytest-mock の mocker を使ってください」
import pytest
from src.format_price import format_price
class TestFormatPrice:
def test_usd_format(self):
assert format_price(1000, "USD") == "$1,000.00"
def test_jpy_no_decimal(self):
assert format_price(1000, "JPY") == "¥1,000"
def test_zero_price(self):
assert format_price(0, "USD") == "$0.00"
def test_negative_raises(self):
with pytest.raises(ValueError, match="負の値"):
format_price(-1, "USD")
def test_invalid_currency_raises(self):
with pytest.raises(ValueError, match="通貨コード"):
format_price(100, "XXX")
@pytest.mark.parametrize("amount,expected", [
(1, "$1.00"),
(999, "$999.00"),
(1000, "$1,000.00"),
(1000000, "$1,000,000.00"),
])
def test_comma_formatting(self, amount, expected):
assert format_price(amount, "USD") == expected
テスト失敗時のデバッグサイクル
失敗したテストをClaudeに渡す方法
テストが失敗したときは、エラーメッセージ全体を渡すことで原因調査の精度が上がります。「テストが失敗した」だけでなく、スタックトレース・期待値・実際の値を含めるのが重要です。
# テスト結果をファイルに保存してClaudeに渡す npm test 2>&1 | tee test-output.txt claude "test-output.txt の内容を確認して、テスト失敗の原因を調査・修正してください" # または直接パイプ npm test 2>&1 | claude "このテスト失敗を修正してください"
同じ失敗が続くときはコンテキストをリセットする
同じ問題を2回以上修正しようとして失敗している場合は、コンテキストが汚染されている可能性があります。/clearでセッションをリセットしてから、エラーメッセージと関連ファイルだけを渡してやり直すと解決することが多いです。
| 状況 | 対処法 |
|---|---|
| 同じ修正を繰り返して失敗 | /clearでリセット後、エラーと該当ファイルだけを再提示 |
| テスト修正の影響が広範囲に及ぶ | /rewind(Esc×2)で変更を巻き戻して別アプローチ |
| 原因調査に時間がかかる | 「このテスト失敗の原因を調査するサブエージェントを使ってください」と指示してコンテキスト分離 |
| スタックトレースが長い | まず「このエラーで最も可能性が高い原因TOP3を上げて」と絞り込んでから対処 |
ultrathinkで複雑なバグを深く分析する
原因が特定できない難しいバグには、ultrathinkキーワードが有効です。Claude Codeはこのキーワードを検出すると、より多くのトークンを思考に費やして根本原因を探ります。
# ultrathink をプロンプトに含めるだけで有効 claude "ultrathink このテストが特定の環境でのみ失敗する原因を調査してください。 テストコード: tests/integration/auth.test.ts 関連ファイル: src/auth/*, src/middleware/*"
ultrathinkはプロンプトに含めた1ターンだけ深い思考を有効にします。継続的な深い思考が必要な場合は/effort highコマンドで設定を変更してください。ただしトークン消費が大幅に増えるため、複雑な問題解決時のみ使用するのがおすすめです。よくある質問
QClaudeが「テストを書いた」と言うのに実際にはファイルが存在しないことがあります
AClaudeがツールを実行せずに回答だけを返す「ドライランバイアス」が起きています。CLAUDE.mdに「テストファイルは必ず実際に作成すること。Bashでファイルの存在を確認してから完了報告すること」と明記することで防げます。またはHooksのPostToolUseでファイル作成後にテスト実行を強制すると、実際にファイルが存在しないとHooksが失敗するため自然と防止されます。
QClaudeが生成したテストがすべて正常系で異常系がありません
A依頼時に「境界値・エラーケース・null/undefined・型違いも含めること」と明示してください。CLAUDE.mdに「テストは正常系・境界値・異常系を必ず含めること」と記載しておくと毎回指示しなくて済みます。また「まずテストケース一覧を作ってから書く」フローにするとより確実です。
Qテストカバレッジが高いのにバグが出ました
Aカバレッジはコードが実行されたかどうかの指標であり、「正しく動いているか」の指標ではありません。アサーション(expect)が甘いテストはカバレッジを高くしても意味がありません。Claudeに「アサーションが具体的かどうかレビューして」と頼むか、mutation testingツール(Stryker等)を使ってテストの実効性を測定することをおすすめします。
QTypeScriptのジェネリクスを使った関数のテスト生成でエラーになります
AClaude Codeの内部処理でHTMLエスケープの問題が起きている可能性があります。関数を直接コードブロックに貼るのではなく、ファイルパスを指定して「src/utils/genericFn.ts のテストを書いて」と指示する方が安定します。
QpytestとVitestが混在するフルスタックプロジェクトでのテスト設定は?
ACLAUDE.mdにバックエンド用・フロントエンド用それぞれのテストコマンドを記載してください。例:「バックエンドテスト: cd backend && pytest、フロントエンドテスト: cd frontend && npm test」のように分けて記載します。Claudeは編集したファイルの場所に応じて適切なテストコマンドを選択します。

