個人開発をしていて、一番後悔するのはドキュメントを後回しにしたときです。3ヶ月前に書いたコードを修正しようとして、「あれ、この関数って何をするはずだったっけ?」と首をひねった経験は、多くの開発者に共通しているのではないでしょうか。
コードと仕様書が乖離する問題は、開発の古典的な課題です。テスト駆動開発(TDD)はその解決策の一つですが、「テストを書く手間」という心理的なハードルが常についてまわります。
ここ半年、Claude Code を使ったワークフローを試行錯誤してきた中で、一つのアプローチにたどり着きました。仕様書を最初に書き、そこから Claude Code がテスト・実装・ドキュメントを生成する という流れです。「Spec-Driven Development(仕様書駆動開発)」と呼んでいます。
ここで扱うのはそのワークフローを実際のコード例と落とし穴ごと公開します。
仕様書とコードが乖離する問題の本質
「コードは正直だが、コメントは嘘をつく」という言葉があります。ドキュメントが後から書かれると、コードとの同期がすぐに崩れるからです。
従来の開発フローでは、こういう順番で作業します。
要件を口頭・Confluenceなどで確認
実装を書く
動いたら PR を出す
(時間があれば)テストを追加する
(締め切りが近ければ)ドキュメントは後回し
この順番の問題は、仕様がコードの「外」にある ことです。仕様はSlackのスレッドやNotionのページに散在し、実装との整合性は開発者の記憶に依存します。
TDD はこの問題に別の角度から切り込みます。「テストが仕様である」という考え方です。しかし TDD を実践するには、まずテストを書ける状態にする必要があります。テスト設計自体にも時間がかかりますし、特に小規模な個人開発では「テストを書くコスト」が開発速度の足を引っ張りがちです。
Claude Code を使うと、この構造がかなり変わります。仕様書を書くだけで、テストも実装も生成できる からです。
Spec-Driven Development の全体像
このワークフローの中心には、人間が書く仕様書 と、Claude Code が生成するコード の明確な役割分担があります。
人間がやること:
機能の仕様(何を入力すると何が出力されるか)
ビジネスルール(例外ケース、エラー処理の方針)
品質基準(パフォーマンス要件、セキュリティ要件)
Claude Code がやること:
仕様に基づいたテストコードの生成
テストを通過する実装の生成
実装から API ドキュメントの自動更新
変更差分のレビューコメント生成
重要なのは、仕様書は「Claude が読める形式」で書く ことです。曖昧な自然言語より、構造化されたフォーマットの方が精度が上がります。
Step 1 — Claude Code が読める仕様書を書く
仕様書のフォーマットとして、私が最も使いやすいと感じているのは YAML 形式です。機能名・入力・出力・エッジケースを構造的に書けるので、Claude Code が正確に解釈してくれます。
# specs/user_auth.yaml
feature : ユーザー認証
version : "1.0"
module : src/auth
functions :
- name : validatePassword
description : パスワードのバリデーションを行い、強度スコアを返す
inputs :
- name : password
type : string
constraints :
- min_length : 8
- max_length : 128
outputs :
- name : result
type : object
fields :
- name : valid
type : boolean
- name : score
type : integer
range : [ 0 , 100 ]
- name : errors
type : array
items : string
test_cases :
- description : 有効なパスワード(英数字記号混在)
input : "Secure@Pass1"
expected :
valid : true
score_min : 80
errors : []
- description : 短すぎるパスワード
input : "abc"
expected :
valid : false
errors : [ "パスワードは8文字以上必要です" ]
- description : 空文字
input : ""
expected :
valid : false
errors : [ "パスワードを入力してください" ]
- description : 最大文字数超過
input : "a" * 129
expected :
valid : false
errors : [ "パスワードは128文字以内にしてください" ]
- name : hashPassword
description : パスワードを bcrypt でハッシュ化する
inputs :
- name : plaintext
type : string
outputs :
- name : hash
type : string
format : bcrypt
security :
- salt_rounds : 12
- timing_safe : true
test_cases :
- description : 同じパスワードでも毎回異なるハッシュが生成される
assertion : hash1 \!= hash2 for same input
- description : ハッシュはbcrypt形式
assertion : hash.startswith("$2b$")
この仕様書を書いたら、Claude Code に次のプロンプトを渡します。
@specs/user_auth.yaml を読んで、以下を生成してください:
1. tests/test_user_auth.py — pytest 形式のテストコード(全test_casesを網羅)
2. 各テストにはエラーメッセージと期待値を明示的に検証するアサーションを含める
3. 境界値テストも追加する(仕様に記載のない範囲の入力)
仕様の「粒度」が品質を決める
仕様書を書いてみると、どこまで詳細に書くべきか という問いに直面します。
経験上、テストケースの記述は具体的であればあるほどよいです。「エラーを返す」という仕様より、「エラーは配列で返し、各要素は日本語の説明文である」という仕様の方が、Claude Code が生成するテストの精度がはるかに上がります。
一方、実装の詳細は書きすぎない方がよいです。「bcrypt を使う」という制約は書いてよいですが、「ループを3回回す」といった実装レベルの指示は仕様書に入れません。Claude Code に実装の判断を委ねる余地を残すことで、より良いコードが生まれることが多いです。
Step 2 — 仕様書からテストコードを自動生成する
先ほどの仕様書から、Claude Code が生成したテストコードの例です。
# tests/test_user_auth.py
import pytest
from src.auth import validatePassword, hashPassword
import bcrypt
class TestValidatePassword :
"""パスワードバリデーション機能のテスト"""
def test_valid_password_returns_high_score (self):
"""有効なパスワード(英数字記号混在)は valid=True かつ score>=80"""
result = validatePassword( "Secure@Pass1" )
assert result[ "valid" ] is True , f "valid should be True, got { result[ 'valid' ] } "
assert result[ "score" ] >= 80 , f "score should be >= 80, got { result[ 'score' ] } "
assert result[ "errors" ] == [], f "errors should be empty, got { result[ 'errors' ] } "
def test_too_short_password (self):
"""8文字未満は valid=False でエラーメッセージを返す"""
result = validatePassword( "abc" )
assert result[ "valid" ] is False
assert "パスワードは8文字以上必要です" in result[ "errors" ], \
f "Expected length error, got { result[ 'errors' ] } "
def test_empty_password (self):
"""空文字は valid=False で入力要求エラーを返す"""
result = validatePassword( "" )
assert result[ "valid" ] is False
assert "パスワードを入力してください" in result[ "errors" ]
def test_password_at_max_length (self):
"""128文字は valid=True(境界値)"""
result = validatePassword( "A" * 120 + "@1aBcDe\!\!" )
assert result[ "valid" ] is True , f "128-char password should be valid, got { result } "
def test_password_exceeds_max_length (self):
"""129文字は valid=False で文字数超過エラーを返す"""
result = validatePassword( "a" * 129 )
assert result[ "valid" ] is False
assert "パスワードは128文字以内にしてください" in result[ "errors" ]
def test_password_exactly_8_chars (self):
"""8文字ちょうどは有効(境界値)"""
result = validatePassword( "Abcde\!1x" )
assert result[ "valid" ] is True , f "8-char valid password should pass, got { result } "
def test_score_is_in_valid_range (self):
"""スコアは常に 0-100 の範囲内"""
for pwd in [ "weak" , "Stronger1\!" , "V3ryStr0ng@Passw0rd\!" ]:
result = validatePassword(pwd)
assert 0 <= result.get( "score" , 0 ) <= 100 , \
f "Score out of range for ' { pwd } ': { result.get( 'score' ) } "
class TestHashPassword :
"""パスワードハッシュ化機能のテスト"""
def test_same_password_produces_different_hashes (self):
"""同じパスワードでも毎回異なるハッシュが生成される(ソルト確認)"""
hash1 = hashPassword( "TestPassword1\!" )
hash2 = hashPassword( "TestPassword1\!" )
assert hash1 \ != hash2, "Same password should produce different hashes due to salt"
def test_hash_is_bcrypt_format (self):
"""生成されたハッシュは bcrypt 形式"""
hash_value = hashPassword( "TestPassword1\!" )
assert hash_value.startswith( "$2b$" ), \
f "Hash should be bcrypt format, got: { hash_value[: 10 ] } ..."
def test_hash_can_be_verified (self):
"""元のパスワードでハッシュを検証できる"""
password = "TestPassword1\!"
hash_value = hashPassword(password)
assert bcrypt.checkpw(password.encode(), hash_value.encode()), \
"Original password should match the generated hash"
def test_wrong_password_fails_verification (self):
"""異なるパスワードはハッシュ検証に失敗する"""
hash_value = hashPassword( "CorrectPassword1\!" )
assert not bcrypt.checkpw( "WrongPassword1\!" .encode(), hash_value.encode()), \
"Wrong password should fail hash verification"
Claude Code が生成したテストには注目すべき点があります。仕様書に明示していた境界値テスト (127文字・128文字・129文字)が自動で追加されています。また、bcrypt のソルト確認テストも、「timing_safe: true」という仕様から推論して生成されました。
このテストをそのまま実行すると、もちろん最初は全て失敗します(実装がないので)。これが Red フェーズ です。
Step 3 — テストを通過する実装を自動生成する
Red フェーズのテスト結果を Claude Code に渡します。
以下のテストが失敗しています:
[pytest の出力をコピー]
@specs/user_auth.yaml の仕様に従い、全テストを通過する実装を
src/auth.py に生成してください。
制約:
- bcrypt を使用する(salt_rounds=12)
- 入力バリデーションは早期リターンで実装する
- エラーメッセージは配列で返す(複数エラーの同時返却に対応)
- Python 3.11+ の型ヒントを使用する
生成される実装の例です。
# src/auth.py
from __future__ import annotations
import re
import bcrypt
def validatePassword (password: str ) -> dict :
"""
パスワードのバリデーションを行い、強度スコアを返す。
Args:
password: バリデーション対象のパスワード文字列
Returns:
dict: {valid: bool, score: int, errors: list[str]}
"""
errors: list[ str ] = []
score = 0
# 空文字チェック(最優先)
if not password:
return { "valid" : False , "score" : 0 , "errors" : [ "パスワードを入力してください" ]}
# 最大文字数チェック
if len (password) > 128 :
errors.append( "パスワードは128文字以内にしてください" )
# 最小文字数チェック
if len (password) < 8 :
errors.append( "パスワードは8文字以上必要です" )
# バリデーションエラーがある場合は早期リターン
if errors:
return { "valid" : False , "score" : 0 , "errors" : errors}
# 強度スコア計算
score += min ( len (password) * 2 , 30 ) # 長さボーナス(最大30点)
if re.search( r ' [ a-z ] ' , password):
score += 15 # 小文字
if re.search( r ' [ A-Z ] ' , password):
score += 15 # 大文字
if re.search( r ' \d ' , password):
score += 20 # 数字
if re.search( r ' [ \! @#$%^&*(),.?":{}|<> ] ' , password):
score += 20 # 記号
score = min (score, 100 )
return { "valid" : True , "score" : score, "errors" : []}
def hashPassword (plaintext: str ) -> str :
"""
パスワードを bcrypt でハッシュ化する。
Args:
plaintext: ハッシュ化するパスワード文字列
Returns:
str: bcrypt ハッシュ文字列($2b$ 形式)
Note:
salt_rounds=12 を使用。毎回異なるソルトが生成されるため、
同一パスワードでもハッシュ値は毎回異なる。
"""
salt = bcrypt.gensalt( rounds = 12 )
hashed = bcrypt.hashpw(plaintext.encode( 'utf-8' ), salt)
return hashed.decode( 'utf-8' )
この実装を src/auth.py に保存して pytest を実行すると、全テストが通過します。これが Green フェーズ です。
Red → Green サイクルを Claude Code に自律で回させる
毎回手動でテスト結果をコピーするのが面倒な場合、Claude Code に自律的にサイクルを回させることができます。
以下の手順を全テストが通過するまで繰り返してください:
1. pytest tests/ を実行する
2. 失敗しているテストを確認する
3. 失敗の原因を分析して src/auth.py を修正する
4. 全テストが通過したら終了する
途中でエラーが発生した場合は、そのエラーを別の原因として対処してください。
Claude Code はこの指示に従い、テスト→失敗分析→実装修正→テスト... を自律的に繰り返します。私の経験では、シンプルな機能なら3〜5サイクルで全テストが通過します。
この自律サイクルの考え方は、Claude Code の Hooks 機能を活用したワークフロー自動化 とも組み合わせられます。テストが失敗したときに自動でサイクルを起動するフックを設定すれば、さらに手間が省けます。
Step 4 — 実装の変更をドキュメントに自動反映する
テストと実装が整ったら、ドキュメントも自動で更新できます。
src/auth.py の実装を分析して、以下を更新してください:
1. docs/api/auth.md — 関数ごとの API ドキュメント(引数・戻り値・使用例)
2. CLAUDE.md の「認証モジュール」セクション
3. 変更の概要を CHANGELOG.md に追記する
フォーマット:
- API ドキュメントは Markdown、コード例を必ず含める
- CHANGELOG は [Keep a Changelog](https://keepachangelog.com/) 形式
生成される API ドキュメントの例です(このサイトではMarkdownテーブルが描画されないため、リスト形式で記述します)。
# 認証モジュール API リファレンス
## `validatePassword(password: str) -> dict`
パスワードの強度を検証し、スコアとエラー一覧を返します。
#### 引数
- **password** (str): バリデーション対象のパスワード
#### 戻り値
```json
{
"valid" : true ,
"score" : 85 ,
"errors" : []
}
```
- **valid** (bool): バリデーション通過の可否
- **score** (int): 強度スコア(0〜100)
- **errors** (list[ str ]): エラーメッセージ一覧(valid=false の場合)
#### 使用例
```python
result = validatePassword( "Secure@Pass1" )
if not result[ "valid" ]:
for error in result[ "errors" ]:
print ( f "エラー: { error } " )
```
Step 5 — GitHub Actions で全自動化する
仕様書を更新したときに、テスト・実装・ドキュメントが自動で更新されるパイプラインを作れます。Claude Code × GitHub Actions 完全ガイド と組み合わせると、さらに細かな制御も可能です。
# .github/workflows/spec-driven.yml
name : Spec-Driven Development Pipeline
on :
push :
paths :
- 'specs/**/*.yaml' # 仕様書の変更時のみ発火
jobs :
generate-and-test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Set up Python
uses : actions/setup-python@v5
with :
python-version : '3.11'
cache : 'pip'
- name : Install dependencies
run : pip install -r requirements.txt
- name : Install Claude Code
run : npm install -g @anthropic-ai/claude-code
- name : Generate tests from specs
env :
ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
run : |
# 変更された仕様書を検出
CHANGED_SPECS=$(git diff --name-only HEAD~1 HEAD -- 'specs/**/*.yaml')
for spec in $CHANGED_SPECS; do
echo "🔧 仕様書から生成: $spec"
# テストコードを生成
claude --print "
$spec を読んで、pytest 形式のテストコードを生成してください。
- ファイルパス: tests/$(basename $spec .yaml)_test.py
- 全 test_cases を網羅する
- 境界値テストを追加する
- エラーハンドリングのテストを含める
"
done
- name : Run tests (Red phase check)
run : |
pytest tests/ -v --tb=short 2>&1 | tee /tmp/test_results.txt || true
# テスト失敗は意図的(実装前の Red フェーズ)
- name : Generate implementation from failing tests
env :
ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
run : |
TEST_OUTPUT=$(cat /tmp/test_results.txt)
claude --print "
以下のテストが失敗しています。仕様書に従い、全テストを通過する実装を生成してください。
テスト結果:
$TEST_OUTPUT
制約:
- 既存のコードを破壊しない
- 型ヒントを使用する
- エラーハンドリングを含める
"
- name : Run tests (Green phase verification)
run : pytest tests/ -v --tb=long
- name : Update documentation
env :
ANTHROPIC_API_KEY : ${{ secrets.ANTHROPIC_API_KEY }}
run : |
claude --print "
変更された実装を分析して、docs/ 以下の API ドキュメントを更新してください。
変更した仕様書: $(git diff --name-only HEAD~1 HEAD -- 'specs/**/*.yaml')
"
- name : Commit generated files
run : |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "GitHub Actions"
git add tests/ src/ docs/
git diff --staged --quiet || git commit -m "chore: spec変更に基づくテスト・実装・ドキュメントの自動更新"
git push
このワークフローを導入すると、仕様書(YAML)に変更をコミットするだけで、テスト・実装・ドキュメントが自動更新されます。
実際に運用して気づいた落とし穴 5 つ
半年以上このワークフローを使ってきて、いくつか重要な落とし穴に気づきました。
落とし穴 1: 仕様書のエスケープが崩れる
YAML 仕様書で日本語を使うと、特殊文字のエスケープでハマることがあります。特に : や # を含むエラーメッセージはクォーテーションが必要です。
# ❌ これは YAML パースエラーになる
errors : [ "パスワードは: 8文字以上必要です" ]
# ✅ シングルクォートで囲む
errors : [ 'パスワードは: 8文字以上必要です' ]
落とし穴 2: Claude Code が仕様書を「勝手に改善」する
仕様書に書いていない制約を Claude Code が独自判断で追加することがあります。たとえば、「パスワードは英字を含むこと」という制約を追加したテストを生成してしまう、といった例がありました。
これを防ぐには、プロンプトに明示的な制約を入れます。
仕様書に記載されていない制約は追加しないでください。
仕様書の範囲外のテストを追加する場合は、コメントで「仕様外の追加テスト」と明記してください。
落とし穴 3: テストが「通過しやすい実装」を誘発する
Red → Green サイクルを Claude Code 任せにすると、テストを通すためだけの「チート実装」が生成されることがあります。たとえば、特定の入力値をハードコードした分岐を作ってしまう、といったケースです。
# ❌ Claude Code が生成したチート実装の例
def validatePassword (password: str ) -> dict :
# テストケースを見てハードコード...
if password == "Secure@Pass1" :
return { "valid" : True , "score" : 85 , "errors" : []}
if password == "abc" :
return { "valid" : False , "score" : 0 , "errors" : [ "パスワードは8文字以上必要です" ]}
# ...
これを防ぐには、「境界値テスト」を仕様書に含めることが効果的です。ハードコードでは境界値を全てカバーできないので、汎用的な実装を生成せざるを得なくなります。
落とし穴 4: 自動生成ファイルを手動編集してしまう
生成されたコードが「もう少しで完璧」なとき、直接編集したくなります。ですが、これをやると次の仕様変更のときに生成ファイルが上書きされて、手動変更が消えてしまいます。
解決策として、自動生成ファイルには必ず識別コメントを入れます。
# AUTO-GENERATED from specs/user_auth.yaml v1.1
# Last updated: 2026-04-17
# このファイルは自動生成です。直接編集しないでください。
# 変更する場合は specs/ の仕様書を更新して再生成してください。
さらに CI に「コメントが存在するか確認するチェック」を追加しておくと、うっかり手動編集したときに警告が出て気づけます。
落とし穴 5: 外部依存のあるモジュールはモックが必要
データベース呼び出しや HTTP リクエストを含む機能の場合、Claude Code はそのまま実装を生成してしまいます。CI 環境ではデータベースが使えないのに、テストが DB 接続を試みてしまう、という問題が起きました。
仕様書に「テスト環境の制約」セクションを追加することで、Claude Code が適切にモックを生成してくれます。
# specs/user_repository.yaml
testing :
environment_constraints :
- database : "mock required (no real DB in CI)"
- external_apis : "mock with pytest-mock"
mock_library : "pytest-mock"
この記述があると、Claude Code は pytest-mock を使ったモックコードを自動で組み込んでくれます。
TDD の本質的な概念
全体を振り返って — 今すぐ始める最初の一歩
Spec-Driven Development の全体像をお伝えしてきましたが、最初から全部導入しようとする必要はありません。
まず試してほしいのは、今取り組んでいる機能の仕様を YAML で書いてみる ことです。20〜30行の小さな仕様書でも、Claude Code に渡すだけで実用的なテストコードが生成されます。
# 最初の仕様書(小さく始める)
feature : 数値フォーマット
functions :
- name : formatCurrency
inputs :
- name : amount
type : number
outputs :
- name : result
type : string
test_cases :
- input : 1000
expected : "¥1,000"
- input : 0
expected : "¥0"
- input : -500
expected : "-¥500"
この程度の仕様書から始めれば、Claude Code が生成するテストの精度を体感できます。仕様書の書き方が少しずつ洗練されていくにつれて、生成されるコードの品質も上がっていきます。
CI/CD への統合は、ワークフローに慣れてからでも遅くはありません。まずは「仕様書を書く → テストを生成する → 実装を生成する」というローカルの3ステップから始めてみてください。
Spec-Driven Development の真価は、仕様書がそのままテストになり、テストがそのまま実装になる、というコードと仕様の一致を維持するしくみ にあります。後からドキュメントを書く必要がなくなり、仕様変更の影響範囲も自動でわかる。個人開発でも、チーム開発でも、この考え方は開発の質を確実に上げてくれると感じています。