実践ワークフロー ── 仕様→テスト→実装のループ
この章では、SDDの実際のワークフローを手順ごとに追う。「仕様を書いてからAIに渡す」が具体的にどう機能するかを体験しよう。
SDDの全体フロー
flowchart TD
A[仕様作成\nSPEC.md を書く] --> B[AIにSPEC.mdを渡す]
B --> C[テスト生成\nAIがテストファイルを先に作る]
C --> D[実装生成\nAIがテストをパスする実装を作る]
D --> E{テスト実行}
E -->|PASS| F[コードレビュー]
E -->|FAIL| G{原因分析}
G -->|仕様の問題| A
G -->|実装の問題| D
F --> H[マージ・デプロイ]
重要なのは「テスト生成を先に行う」点だ。テストが先に存在することで、実装が仕様を満たしているかどうかを機械的に検証できる。
ステップ1: SPEC.mdを書く
第5章のテンプレートを使い、SPEC.mdを作成する。
# プロジェクト構成
my-app/
├── CLAUDE.md # プロジェクトコンテキスト(常駐)
├── specs/
│ └── wishlist.md # 今回のSPEC.md
├── src/
│ ├── routes/
│ ├── services/
│ └── repositories/
└── tests/
ステップ2: AIにSPEC.mdを渡してテストを先に生成させる
Claude Codeを使った実際のやり取りの例:
User:
@specs/wishlist.md を読んで、まずテストファイルだけ作って。
実装はまだ書かないで。
tests/wishlist.integration.test.ts に以下の統合テストを作成して:
- 全エンドポイントの正常系
- 異常系(401、404、409、バリデーションエラー)
- パフォーマンステスト(100msチェック)
Claudeが生成するテストファイル(例):
// tests/wishlist.integration.test.ts
import request from 'supertest';
import app from '../src/app';
import { prisma } from '../src/lib/db';
import { generateToken } from '../src/utils/jwt';
describe('Wishlist API', () => {
let authToken: string;
let userId: string;
beforeEach(async () => {
// テスト用ユーザーとデータのセットアップ
const user = await prisma.user.create({
data: { email: '[email protected]', passwordHash: 'hash' },
});
userId = user.id;
authToken = generateToken(user.id);
});
afterEach(async () => {
await prisma.wishlistItem.deleteMany();
await prisma.wishlist.deleteMany();
await prisma.user.deleteMany();
});
// ✅ 正常系: 商品追加
describe('POST /api/wishlist/items', () => {
it('商品をウィッシュリストに追加できる', async () => {
const product = await prisma.product.create({
data: { name: 'テスト商品', price: 1000, imageUrl: 'https://example.com/img.jpg' },
});
const res = await request(app)
.post('/api/wishlist/items')
.set('Authorization', `Bearer ${authToken}`)
.send({ productId: product.id });
expect(res.status).toBe(201);
expect(res.body.data.productId).toBe(product.id);
});
it('未認証アクセスは401を返す', async () => {
const res = await request(app)
.post('/api/wishlist/items')
.send({ productId: 'some-id' });
expect(res.status).toBe(401);
expect(res.body.error.code).toBe('UNAUTHORIZED');
});
it('100件超で409を返す', async () => {
// 100件のアイテムを事前に作成
const wishlist = await prisma.wishlist.create({ data: { userId } });
await prisma.wishlistItem.createMany({
data: Array.from({ length: 100 }, (_, i) => ({
wishlistId: wishlist.id,
productId: `product-${i}`,
})),
});
const newProduct = await prisma.product.create({
data: { name: '101番目', price: 100, imageUrl: 'https://example.com/img.jpg' },
});
const res = await request(app)
.post('/api/wishlist/items')
.set('Authorization', `Bearer ${authToken}`)
.send({ productId: newProduct.id });
expect(res.status).toBe(409);
expect(res.body.error.code).toBe('WISHLIST_LIMIT_EXCEEDED');
});
});
// ✅ 正常系: ウィッシュリスト取得
describe('GET /api/wishlist', () => {
it('自分のウィッシュリストを取得できる', async () => {
const start = Date.now();
const res = await request(app)
.get('/api/wishlist')
.set('Authorization', `Bearer ${authToken}`);
const elapsed = Date.now() - start;
expect(res.status).toBe(200);
expect(Array.isArray(res.body.data.items)).toBe(true);
// パフォーマンス: 100ms以内
expect(elapsed).toBeLessThan(100);
});
});
// ✅ 共有URL
describe('GET /api/wishlist/share/:token', () => {
it('認証なしで共有ウィッシュリストを閲覧できる', async () => {
const wishlist = await prisma.wishlist.create({
data: { userId, isPublic: true, shareToken: 'test-token-123' },
});
const res = await request(app)
.get(`/api/wishlist/share/test-token-123`);
expect(res.status).toBe(200);
expect(res.body.data.id).toBe(wishlist.id);
});
it('Privateリストは401を返す', async () => {
await prisma.wishlist.create({
data: { userId, isPublic: false, shareToken: 'private-token' },
});
const res = await request(app)
.get(`/api/wishlist/share/private-token`);
expect(res.status).toBe(403);
});
});
});
この時点でテストはすべてFAILする。それが正しい。実装がまだないからだ。
ステップ3: テストをパスする実装を生成させる
User:
@specs/wishlist.md と tests/wishlist.integration.test.ts を読んで、
テストが全てPASSするように実装して。
生成するファイル:
1. prisma/schema.prisma(wishlists, wishlist_items テーブル追加)
2. src/routes/wishlist.ts
3. src/services/wishlistService.ts
4. src/repositories/wishlistRepository.ts
Claudeが生成する実装(一部抜粋):
// src/services/wishlistService.ts
import { WishlistRepository } from '../repositories/wishlistRepository';
const WISHLIST_LIMIT = 100;
export class WishlistService {
constructor(private repo: WishlistRepository) {}
async addItem(userId: string, productId: string) {
const wishlist = await this.repo.findOrCreateByUserId(userId);
const itemCount = await this.repo.countItems(wishlist.id);
if (itemCount >= WISHLIST_LIMIT) {
throw new WishlistLimitError('WISHLIST_LIMIT_EXCEEDED');
}
return this.repo.addItem(wishlist.id, productId);
}
async getWishlist(userId: string) {
return this.repo.findByUserId(userId);
}
async getPublicWishlist(shareToken: string) {
const wishlist = await this.repo.findByShareToken(shareToken);
if (!wishlist) throw new NotFoundError('Wishlist not found');
if (!wishlist.isPublic) throw new ForbiddenError('This wishlist is private');
return wishlist;
}
}
ステップ4: テストを実行して検証
npm test tests/wishlist.integration.test.ts
# 期待される出力:
# ✓ POST /api/wishlist/items > 商品をウィッシュリストに追加できる (45ms)
# ✓ POST /api/wishlist/items > 未認証アクセスは401を返す (12ms)
# ✓ POST /api/wishlist/items > 100件超で409を返す (89ms)
# ✓ GET /api/wishlist > 自分のウィッシュリストを取得できる (34ms)
# ✓ GET /api/wishlist/share/:token > 認証なしで共有ウィッシュリストを閲覧できる (28ms)
# ✓ GET /api/wishlist/share/:token > Privateリストは401を返す (15ms)
#
# Tests: 6 passed, 6 total
# Coverage: statements 87.3%, branches 82.1%, functions 91.2%, lines 86.8%
すべてPASSしたらSPEC.mdの Verification チェックボックスを確認する。
ステップ5: Verificationチェック
## Verification(更新後)
- [x] 統合テスト: 全エンドポイントの正常系・異常系がPASS ✓
- [x] 100件超で409エラーが返る ✓
- [x] 未認証アクセスで401が返る(共有URLを除く)✓
- [x] GET /api/wishlist のレスポンスタイムが100ms以内 ✓(34ms)
- [x] 共有URLから認証なしで閲覧できる ✓
- [x] カバレッジ: ビジネスロジック層 80%以上 ✓(87.3%)
全項目がチェックされたら、実装は完了だ。
FAILした時の対応
テストがFAILした場合の判断フローはこうだ。
flowchart TD
F[テストFAIL] --> Q1{何が原因か?}
Q1 -->|仕様の記述が曖昧だった| A[SPEC.mdを修正して再実行]
Q1 -->|実装のバグ| B[AIに修正を依頼]
Q1 -->|テスト自体の誤り| C[テストを修正]
Q1 -->|仕様に書いてない前提条件がある| D[SPEC.mdに追記してAI再実行]
重要な原則:テストが失敗してもSPEC.mdを修正するのは「仕様に問題がある時だけ」だ。 「テストをパスさせるためにテストを甘くする」は仕様駆動開発の崩壊につながる。
BDDとの統合
より振る舞い重視の仕様を書く場合、SPEC.mdにGherkinシナリオを組み込める。
# Feature: ウィッシュリスト管理
## Scenarios
```gherkin
Feature: ウィッシュリスト管理
Scenario: 商品をウィッシュリストに追加する
Given ログイン済みユーザーが存在する
When 商品ID "prod-001" をウィッシュリストに追加する
Then ウィッシュリストに商品が1件追加されている
And レスポンスコードは201
Scenario: 上限を超えて追加しようとする
Given ウィッシュリストに100件の商品がある
When 101件目の商品を追加しようとする
Then レスポンスコードは409
And エラーコードは "WISHLIST_LIMIT_EXCEEDED"
これをCucumberと組み合わせると、Gherkinシナリオが直接テストになる。
## ループ回数の目安
仕様→テスト→実装のサイクルを何回回すか。
| 機能の複雑さ | 想定ループ回数 |
|------------|--------------|
| シンプルなCRUD | 1回(一発でPASS) |
| 複数テーブル参照 | 2〜3回 |
| 複雑なビジネスロジック | 3〜5回 |
| セキュリティ重要な機能 | 3〜5回(セキュリティレビュー込み) |
---
次の章では、SDDでよくやってしまう失敗パターンと、その脱出法を解説する。