TDDでAPIを実装する流れ
- ユースケースを口頭で確認
- 4行仕様に落とす(契約)
- 成功テストを1本書く(赤)
- ルーターと型の器を置く
- 最小実装で成功テストを通す(緑)
- 失敗テストを追加して実装を拡張
- リファクタ
では実際に 「タスクを完了する」 ユースケース を題材に、
ステップごとに「考え方 → コード例」を順に落とし込んでいく。
1. ユースケース発見(口頭)
- ユーザーは「未完了タスクを完了にしたい」
- 完了したら「いつ完了したか」を見たい
- すでに完了している場合は「同じ completedAt を返す(冪等)」
- 存在しないIDを指定されたら「Not Found」
2. 4行仕様(契約に落とす)
1. メソッド/URL: POST /tasks/{id}/complete
2. 入力: なし
3. 成功: 200 + { id, content, completedAt } (completedAtはUTC ISO8601文字列)
4. 失敗: 404 Not Found (存在しないid)
3. テストを書く(まず成功ケース 1本)
# tests/test_complete_task.py
def test_post_success(client, seeded_task):
task_id = seeded_task.id
res = client.post(f"/tasks/{task_id}/complete")
assert res.status_code == 200
body = res.json()
# 最小限の契約を検証
assert "completedAt" in body
assert body["id"] == task_id
→ ここでまず**赤(失敗)**になる。
4. 配線を作る(ルーター+型の器)
# apps/api/routes/tasks_complete.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from apps.api.deps import repo_dep
r = APIRouter()
class TaskOut(BaseModel):
id: int
content: str
completedAt: str # UTC ISO8601文字列
@r.post("/tasks/{id}/complete", response_model=TaskOut)
def complete_task(id: int, repo=Depends(repo_dep)):
# まずは「器」。中身は空返却でもコンパイルが通る
return {}
→ この時点で FastAPI は動くが、テストはまだ赤。
5. 最小実装で成功テストを通す
from datetime import datetime, timezone
from fastapi import HTTPException, status
def now_iso_utc() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@r.post("/tasks/{id}/complete", response_model=TaskOut)
def complete_task(id: int, repo=Depends(repo_dep)):
task = repo.get(id)
if task is None:
# 失敗ケース(404)はまだ書いてなくてもよいが、
# すぐ次に足すので用意しておいてOK
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
if task.completedAt is None: # 冪等性のため
task.completedAt = now_iso_utc()
repo.update(task)
return TaskOut(
id=task.id,
content=task.content,
completedAt=task.completedAt,
)
→ この時点で 成功テストが緑になる。
6. 失敗テストを足す
def test_post_404_when_not_found(client):
res = client.post("/tasks/999999/complete")
assert res.status_code == 404
body = res.json()
assert body["detail"] == "Task not found"
→ 既に実装に 404 を入れてあるので、これも緑になる。
7. リファクタ
-
completedAtの生成をユーティリティ関数にまとめる - レスポンススキーマを共通の
schemas/task.pyに移す -
repo.update()の扱いをサービス層に寄せる …など
契約(4行)とテストは変えないまま内部整理。
まとめ
今回の一連の流れをコードで具体化すると:
- ユースケースを口頭で確認
- 4行仕様に落とす(契約)
- 成功テストを1本書く(赤)
- ルーターと型の器を置く
- 最小実装で成功テストを通す(緑)
- 失敗テストを追加して実装を拡張
- リファクタ