0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TDDでAPIを実装できちゃうんですか?

Last updated at Posted at 2025-10-03

TDDでAPIを実装する流れ

  1. ユースケースを口頭で確認
  2. 4行仕様に落とす(契約)
  3. 成功テストを1本書く(赤)
  4. ルーターと型の器を置く
  5. 最小実装で成功テストを通す(緑)
  6. 失敗テストを追加して実装を拡張
  7. リファクタ

では実際に 「タスクを完了する」 ユースケース を題材に、
ステップごとに「考え方 → コード例」を順に落とし込んでいく。


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行)とテストは変えないまま内部整理。


まとめ

今回の一連の流れをコードで具体化すると:

  1. ユースケースを口頭で確認
  2. 4行仕様に落とす(契約)
  3. 成功テストを1本書く(赤)
  4. ルーターと型の器を置く
  5. 最小実装で成功テストを通す(緑)
  6. 失敗テストを追加して実装を拡張
  7. リファクタ
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?