ðºïž FastAPI â èšèšæžãš TDD
ãã¯ãããžç³» ïŒ L19 design-tdd â èšèšæžãš TDD
ðºïž FastAPI â èšèšæžãš TDD
API èšèšæžãšã¯äœã + ãã¹ãé§åéçº (TDD) ã§å®è£ ãé²ãã
äœããã®
ã€ãã³ãåå ç»é² API
瀟å å匷äŒã«ãåå ç³èŸŒãã§ãã Web API
åå è
ãç³ã蟌ã â POST /registrations
ç³èŸŒäžèЧãèŠã â GET /registrations
ç³èŸŒããã£ã³ã»ã«ãã â DELETE /registrations/{id}
管çè
ã ããèŠããã â X-API-Key èªèšŒãŽãŒã«ããã â ã©ã® STEP ã§ãäœããæžãã®ã
| STEP | ããŒã | è§Šããã¡ã€ã« | ããããš |
|---|---|---|---|
| 1 | èšèšæžãèªã | main.py (ã¢ããªå®è£
) | æå°ã¢ããªãåãã |
| 2 | èšèšæž â ãã¹ã | test_main.py ã®ã¿ (ãã¹ãã³ãŒã) | 仿§ããã¹ãã³ãŒãã«ç¿»èš³ (Red) |
| 3 | Green ã«ãã | main.py ã®ã¿ (ã¢ããªå®è£
) | ã¢ããªå®è£ ãæžã㊠STEP2 ã®ãã¹ããéã |
| 4 | ãã¹ãå è¡ TDD | test_main.py â main.py | æ°æ©èœã®ãã¹ããå ã«æžã â ã¢ããªå®è£ ã§éã |
| 5 | èªèšŒã远å | test_main.py â main.py | èªèšŒãã¹ããæžã â Depends ã§ã¢ããªå®è£ |
> çšèª: æ¬è³æã§ã¯ ããã¹ãã³ãŒãã = test_main.py ã®å
容 / ãã¢ããªå®è£
ã = main.py ã®å
容 ãšæžãåããŸãã
> ãå®è£ ã ãšæžãããŠããã ã¢ããªå®è£ (main.py) ã®ããšã ãã¹ãã³ãŒããæžãããšã¯ ããã¹ããæžãã ãšè¡šçŸããŸãã
starter / answer ã®äœ¿ãæ¹
ãµã³ãã«ã³ãŒã/ âââ step1/ â âââ starter/main.py â ããããæžãå§ãã â âââ answer/main.py â å®æåœ¢ (è©°ãŸã£ããåèã«) âââ step2/ ... âââ 宿å/main.py
å STEP ã® starter ã¯å STEP ã® answer ãšåãå
容ã
ãSTEP2 ã® answerããšãSTEP3 ã® starterãã¯åäžã
ð API èšèšæžãšã¯äœã
ãäœãåã«æ±ºãããããã®å°å³
ã³ãŒããæžãåã«ã以äžã決ããŠããææž:
- äœãã§ããã â æ©èœã®äžèЧ
- ã©ãåŒã¶ã â URL / ã¡ãœãã / ãã©ã¡ãŒã¿
- äœãè¿ãã â ã¬ã¹ãã³ã¹ã®åœ¢
- ã©ã倱æããã â ãšã©ãŒã®çš®é¡ãšæå³
> å°å³ãªãã§å±±ãç»ããšè¿·åã«ãªããAPI ãåãã
èšèšæžããªããšäœãèµ·ããã
â ãGET ãè¿ããšæã£ãŠããã POST ã ã£ãã
â ã404 ãè¿ã£ãŠãããã©ãªããåãããªãã
â ãããã³ããšããã¯ã§é ç®åãéãã
â ãçŽåçŽåã«ä»æ§å€æŽãåãã£ãã
â å šéšãå ã«æ±ºããŠããªãã£ããããèµ·ãã
èšèšæžããããšäœãå€ããã
â ããã³ããšããã¯ãåãèšèšæžãèŠãŠäžŠè¡éçºã§ãã
â ã仿§éãã«åããŠããããããã¹ãã§ç¢ºèªã§ãã
â åŸããåå ãã人ãå šäœåãå³åº§ã«ææ¡ã§ãã
â ããªããããªã£ãŠããããã®çç±ãæ®ã
èšèšæžã®5ã€ã®æ§æèŠçŽ
1. æŠèŠ â ãã®APIã§äœãã§ããã (1段èœ) 2. ããŒã¿èšèš â äœãä¿åããã (ããŒãã«åœ¢åŒ) 3. ãšã³ããã€ã³ãäžèЧ â ã©ããªæäœãããã 4. è©³çŽ°ä»æ§ â åãšã³ããã€ã³ãã® IN/OUT 5. ãšã©ãŒä»æ§ â ç°åžžæã«ã©ãè¿ãã
äŸ: ãšã³ããã€ã³ãäžèŠ§ã®æžãæ¹
| ã¡ãœãã | ãã¹ | äœããã | èªèšŒ |
|---|---|---|---|
| GET | /registrations | ç³èŸŒäžèЧãååŸ | å¿ èŠ |
| POST | /registrations | æ°èŠç³èŸŒ | äžèŠ |
| GET | /registrations/{id} | 1ä»¶ååŸ | å¿ èŠ |
| DELETE | /registrations/{id} | ç³èŸŒããã£ã³ã»ã« | å¿ èŠ |
â ããã ãã§ãã©ã㪠API ãããäžç®ã§ããã
äŸ: ãªã¯ãšã¹ã/ã¬ã¹ãã³ã¹ã®æžãæ¹
POST /registrations ãªã¯ãšã¹ã:
{
"name": "å±±ç°å€ªé",
"email": "yamada@example.com",
"event": "FastAPIå匷äŒ"
}ã¬ã¹ãã³ã¹ (201 Created):
{
"id": 1,
"name": "å±±ç°å€ªé",
"status": "registered",
"created_at": "2026-06-09T10:00:00"
}äŸ: ãšã©ãŒä»æ§ã®æžãæ¹
| ç¶æ³ | ã¹ããŒã¿ã¹ | è¿ããã® |
|---|---|---|
| å¿ é é ç®ãç¡ã | 422 | ããªããŒã·ã§ã³ãšã©ãŒ |
| ååšããªã ID | 404 | {"detail": "registration not found"} |
| èªèšŒããŒãééã | 401 | {"detail": "invalid api key"} |
â ãšã©ãŒã®ä»æ§ãå ã«æ±ºãããšãããã³ãã®ããšã©ãŒåŠçããå ã«æžãã
OpenAPI / Swagger ãšã®é¢ä¿
FastAPI ã¯èµ·åãããš èªåçã« OpenAPI 圢åŒã®èšèšæžãçæãã:
http://localhost:8000/docs â Swagger UI (ãã©ãŠã¶ã§æäœã§ããèšèšæž) http://localhost:8000/openapi.json â æ©æ¢°ãèªããèšèšæž (JSON)
ã€ãŸããFastAPI ã®ã³ãŒããæžããšèšèšæžãèªåã§çæãããã
â ã³ãŒããšèšèšæžãåžžã«äžèŽããŠããç¶æ ã«ãªãã
Swagger UI ã§ã§ããããš
- ãšã³ããã€ã³ãäžèЧã確èªãã
- ãªã¯ãšã¹ã / ã¬ã¹ãã³ã¹ã®ã¹ããŒãã確èªãã
- ãã©ãŠã¶ããå®éã«ãªã¯ãšã¹ããéã (Try it out)
- ããŒã ã¡ã³ããŒã«ããããèŠãŠããš URL ãæž¡ã
â curl ã³ãã³ããæžããªããŠãåäœç¢ºèªãã§ãã
èšèšæžã®æžãæ¹: å®åã®æµã
1. èŠä»¶ããã¢ãªã³ã° / æŽçãã ãäœãã§ããã°å¬ãããããç®æ¡æžãã§éãã 2. ããŒã¿ã¢ãã«ã決ãã ãäœãä¿åãããã= ããŒãã«ã®åãšåãæžã 3. ãšã³ããã€ã³ããæŽãåºã CRUD ã®äœãå¿ èŠã = äžèЧ / ååŸ / äœæ / æŽæ° / åé€ 4. IN/OUT ã®åœ¢ã決ãã ãªã¯ãšã¹ããšã¬ã¹ãã³ã¹ã® JSON ã®åœ¢ãæžã 5. ãšã©ãŒãæŽãåºã ã倱æãããšãããäœãã= 404 / 401 / 422 ã®æ¡ä»¶
èšèšæžãã©ãèªãã (èªã¿æ¹ã®æåŒã)
æ°äººãèšèšæžãåããŠèªããšãã ãäžããå šéšèªãããšã㊠30 åæº¶ããã ã®ã¯å žåãã¹ã
ç®çãæ±ºããŠèªã ã®ãéåã
| ãã®åŸããããš | èšèšæžã®ã©ããèªãã°ãã? |
|---|---|
| æå°å®è£ ãåãã (STEP 1) | §4 ãšã³ããã€ã³ãäžèЧ |
| ãã¹ããæžã (STEP 2) | §5 è©³çŽ°ä»æ§ + §8 åå ¥æ¡ä»¶ |
| èªèšŒãå®è£ ãã (STEP 5) | §6 èªèšŒã®æ±ºãŸã |
| ãšã©ãŒåŠçãæžã (å STEP) | §7 ãšã©ãŒã¬ã¹ãã³ã¹ã®çµ±äž |
â èšèšæžã¯ã仿§ã®åŒãåºããã å¿ èŠãªæã«å¿ èŠãªç¯ãåŒãã
é åžè³æã®èšèšæžãèªã
é
åžè³æ/èšèšæž/APIèšèšæž_ã€ãã³ãåå ç»é².pdf ãéããŠãã ããã
ãŸã 5 åã§å šäœãçºããã 以äžã確èª:
- [ ] ãšã³ããã€ã³ãã¯äœåãã? (§4)
- [ ] èªèšŒãå¿ èŠãªã®ã¯ã©ã®æäœ? (§4 / §6)
- [ ] POST ã®ã¬ã¹ãã³ã¹ã«å«ãŸãããã£ãŒã«ãã¯? (§5.1)
- [ ] 404 ã¯ã©ããªæã«è¿ã? (§7)
â çããããã STEP 1 ãžé²ãã çããããªãé ç®ãããã°è©²åœç¯ãããäžåºŠã
ðŽð¢ TDD â ãã¹ãé§åéçº
TDD ãšã¯
Test-Driven Development â ãã¹ãã³ãŒããå ã«æžããŠããã¢ããªå®è£ ãæžãéçºææ³
> æ¬è³æã§ã¯ãã¢ããªå®è£
= main.py ã®ã³ãŒãã ããã¹ãã³ãŒã = test_main.py ã®ã³ãŒãã ãšåŒã³åããŸãã
Red â ãã¹ãã³ãŒããæžã â FAIL ãã (åœç¶: main.py ã«ã¢ããªå®è£ ããªããã) Green â main.py ã«ã¢ããªå®è£ ãæžã â PASS ãã Refactor â ã³ãŒããæŽçãã (ãã¹ã㯠PASS ã®ãŸãŸ)
ãã®ãµã€ã¯ã«ãå°ããåãã
ãªããã¹ããå ã«æžãã®ã
ãã¹ãã³ãŒãã¯ãèšèšæžãæ©æ¢°ãèªãã圢ã«ãããã®ã
- ã¢ããªå®è£ ãå§ããåã«ãäœãäœããããæç¢ºã«ãªã
- ãã¹ããéãã°ã仿§éãã«åããŠããããšèšãã
- ãªãã¡ã¯ã¿ãªã³ã°ããŠãå£ããŠããªããšç¢ºèªã§ãã
- ãåãããšã仿§éãã«åããã¯éã
ãã¹ãå è¡ãªãã®éçºã§èµ·ããããš
main.py ãæžã â ãã©ãŠã¶ã§æåç¢ºèª â ããªããåããã
â
æ°é±éåŸ: ãããããã®æ©èœå£ããŠãã
â
ããã€å£ããã®ãåãããªãããäœãå€ãããçŽãã®ãåãããªããâ ãã¹ããããã°ãå£ããç¬éããåãã
FastAPI ã§ã®ãã¹ã: TestClient
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_post_registration():
res = client.post("/registrations", json={
"name": "å±±ç°å€ªé",
"email": "yamada@example.com",
"event": "FastAPIå匷äŒ",
})
assert res.status_code == 201â ãµãŒããŒãèµ·åããªããŠã API ããã¹ãã§ãã
ãªã TestClient ã¯ãµãŒãèµ·åãªãã§åãã®ã
| èŠ³ç¹ | éåžžã®ãªã¯ãšã¹ã | TestClient |
|---|---|---|
| çµè·¯ | ãã©ãŠã¶ â ãããã¯ãŒã¯ â uvicorn â FastAPI app | ãã¹ãããã»ã¹å ã§çŽæ¥ FastAPI app ãåŒã¶ |
| ããŒã | 8000 ã listen ããå¿ èŠãã | ããŒãäžèŠ |
| é床 | TCP / HTTP ãªãŒããŒããããã | ms åäœã§é£ç¶å®è¡å¯ |
| äžèº« | å¥ããã»ã¹ | åäž Python ããã»ã¹ (in-process) |
â TestClient(app) 㯠app ã ASGI ã¢ããªãšããŠçŽæ¥åŒã¶èãã©ãããŒã
æ¬ç©ã® HTTP ã¯æµããªããã åã Pydantic ããªããŒã·ã§ã³ã»åãäŸå解決ã»åãã¬ã¹ãã³ã¹ãè¿ãã
ã ãã client.post(...) ã®æ»ãå€ã¯ãã©ãŠã¶ã§å©ããæãšåã Responseã
pytest å®è¡ã®å šäœãããŒ
â ãŠãŒã¶ãŒãæãåããã®ã¯ â ã ⢠㮠3 ã¹ãããã
äžã®çްããæµã (from main import app ã client.post(...)) 㯠ãã¹ãã³ãŒããåæã«ããã
ãã¹ãã®æžãæ¹: èšèšæžãã³ãŒãã«ç¿»èš³ãã
èšèšæž:
> POST /registrations â 201 Created
ãã¹ã:
def test_post_registration_returns_201():
res = client.post("/registrations", json={...})
assert res.status_code == 201ãã¹ãã®æžãæ¹: ãšã©ãŒä»æ§ããã¹ãã«ãã
èšèšæž:
> å¿ é é ç®ãç¡ã â 422
ãã¹ã:
def test_post_missing_name_returns_422():
res = client.post("/registrations", json={
"email": "yamada@example.com",
"event": "FastAPIå匷äŒ",
# name ããªã
})
assert res.status_code == 422pytest ã®å®è¡æ¹æ³
# ãã¹ããå®è¡ãã python3 -m pytest test_main.py -v # -v = 詳现衚瀺 (ã©ã®ãã¹ãã PASS/FAIL ã)
PASS:
test_main.py::test_post_registration_returns_201 PASSED
FAIL:
test_main.py::test_post_registration_returns_201 FAILED
Red â Green ã®äœéš (STEP2 â STEP3)
STEP2 ã§ããããš:
step2/starter/main.pyã¯ãã®ãŸãŸ (ã¢ããªå®è£ ãè¶³ããªã)step2/starter/test_main.pyã«èšèšæžã®ä»æ§ãæžãpytestãå®è¡ â FAIL (Red) ã確èª
STEP3 ã§ããããš:
step3/starter/main.pyã«ã¢ããªå®è£ ã远å ããpytestãå®è¡ â PASS (Green) ã確èª
ð ïž STEP 1: èšèšæžãèªã
æå° FastAPI ãåãã
STEP 1 ã®æµã
STEP 1 ã§ããããš
> è§Šããã¡ã€ã«: step1/starter/main.py (ã¢ããªå®è£
)
> ãã¹ããã¡ã€ã«ã¯è§ŠããŸããã ãŸãã¢ããªãèµ·åããããšã確èªãã STEPã
é åžè³æ/èšèšæž/APIèšèšæž_ã€ãã³ãåå ç»é².pdfãèªã (5å)step1/starter/main.pyãéã- FastAPI ã¢ããªãäœã£ãŠ
GET /registrationsã远å ãã uvicorn main:app --reloadã§èµ·åhttp://localhost:8000/docsã§ Swagger UI ã確èª
æå°ã³ãŒã (ãã³ã)
from fastapi import FastAPI
app = FastAPI(title="ã€ãã³ãåå ç»é² API")
@app.get("/registrations")
def list_registrations():
return []â 空ãªã¹ããè¿ãã ãã§ OKãSTEP2 以éã§å®è£ ãè¶³ãã
STEP 1 宿ã€ã¡ãŒãž
http://localhost:8000/docs ãéããš:
/registrationsãšã³ããã€ã³ãã衚瀺ãããŠãã- "Try it out" â "Execute" ã§ã¬ã¹ãã³ã¹
[]ãè¿ã
ðŽ STEP 2: èšèšæž â ãã¹ã (Red)
ãã¹ããæžã㊠FAIL ããã
STEP 2 ã®æµã
STEP 2 ã§ããããš
> è§Šããã¡ã€ã«: step2/starter/test_main.py ã®ã¿ (ãã¹ãã³ãŒã)
> main.py 㯠STEP1 ã®ç¶æ
ã®ãŸãŸã ã¢ããªå®è£
ãè¶³ããªããããã¹ããèœã¡ã ̶ ããã Red ã®æ£è§£ã
step2/starter/test_main.py ãéããŠã以äžã®ãã¹ããæžã:
- POST /registrations ã§ 201 ãè¿ãããš (èšèšæž Â§5.1)
- POST ã®ã¬ã¹ãã³ã¹ã« id / status / created_at ãå«ãŸããããš (§5.1)
- name ãçç¥ãããš 422 ãè¿ãããš (§5.1 ãšã©ãŒ)
- GET /registrations ããªã¹ããè¿ãããš (§5.2)
- GET /registrations ã§ X-API-Key ç¡ããªã 401 ãè¿ãããš (§6 èªèšŒ / §8 åå ¥æ¡ä»¶)
â èšèšæžã® §5 è©³çŽ°ä»æ§ / §6 èªèšŒ / §8 åå ¥æ¡ä»¶ ããå°ããŠããã
> 5 ã€ç®ã¯ STEP 5 ã§ã¢ããªå®è£ ãæžããèªèšŒã ã® ä»æ§ã ãå ã«æžããŠãã ãã¿ãŒã³ã
> ãã¹ãã³ãŒãã¯æžãããã¢ããªå®è£ ãç¡ãã®ã§ Red ã«ãªã ̶ ããã TDD ã®èªç¶ãªé²ãæ¹ã
ãã¹ãã®æžãæ¹ (ãã³ãã¬ãŒã)
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_post_registration_returns_201():
res = client.post("/registrations", json={
"name": "å±±ç°å€ªé",
"email": "yamada@example.com",
"event": "FastAPIå匷äŒ",
})
assert res.status_code == 201å®è¡ã㊠Red ã確èª
cd step2/starter python3 -m pytest test_main.py -v
äºæ³ãããçµæ:
FAILED test_main.py::test_post_registration_returns_201
â FAIL ããŠåœç¶ã ã¢ããªå®è£ ããªãã®ã ããã ããã Redã
STEP 2 ã§ãããããããã€ã³ã
| çç¶ | åå | å¯ŸåŠ |
|---|---|---|
ModuleNotFoundError: No module named 'main' | step2/starter/ 以å€ã§å®è¡ããŠãã | cd step2/starter ããŠãã pytest |
ModuleNotFoundError: No module named 'fastapi' | venv ãæå¹åãããŠããªã | ããã³ããã« (.venv) ãä»ããŠãããç¢ºèª |
| ãã¹ãŠ PASS ããŠããŸã | ãã¹ã颿°åã test_ ã§å§ãŸã£ãŠããªã | pytest 㯠test_ ã§å§ãŸã颿°ã®ã¿èªåæ€åº |
| GET ãã¹ãã ã PASS ãã | STEP1 ã§ GET /registrations ã空é
åã§è¿ããŠãã | æ³å®å (POST ç³» 4 件㯠FAIL ãã) |
> ã©ããã pytest ãå®è¡ãã? â å¿
ã step2/starter/ å
ã
> test_main.py ã® from main import app 㯠ãåããã£ã¬ã¯ããªã® main.py ãèªãã æå³ã
ð¢ STEP 3: ãã¹ããéã (Green)
ã¢ããªå®è£ ãæžã㊠PASS ããã
STEP 3 ã®æµã
STEP 3 ã§ããããš
> è§Šããã¡ã€ã«: step3/starter/main.py ã®ã¿ (ã¢ããªå®è£
)
> test_main.py 㯠STEP2 ã§æžãããã®ãæ¢ã« step3/starter/ ã«ã³ããŒãããŠããã è§Šããªãã
step3/starter/main.py ã«ã¢ããªå®è£
ã远å ããŠã STEP2 ã®ãã¹ããå
šãŠ PASS ãããã
远å ããèŠçŽ :
RegistrationCreatePydantic ã¢ãã« (name / email / event)- POST
/registrationsâ 201 + ãã£ãŒã«ãå šéšè¿ã - ã€ã³ã¡ã¢ãªãªã¹ã (
_registrations) ã§ããŒã¿ãä¿æ
Pydantic ã§ãªã¯ãšã¹ãããã£ãå®çŸ©ãã
from pydantic import BaseModel
class RegistrationCreate(BaseModel):
name: str
email: str
event: strâ FastAPI ãèªåçã«ããªããŒã·ã§ã³ãname ãç¡ããã° 422 ãè¿ãã
POST ãšã³ããã€ã³ãã®å®è£
_registrations = []
_next_id = 1
@app.post("/registrations", status_code=201)
def create_registration(body: RegistrationCreate):
global _next_id
record = {
"id": _next_id,
"name": body.name,
"email": body.email,
"event": body.event,
"status": "registered",
"created_at": datetime.now().isoformat(timespec="seconds"),
}
_registrations.append(record)
_next_id += 1
return recordå®è¡ã㊠Green ã確èª
cd step3/answer python3 -m pytest test_main.py -v
äºæ³ãããçµæ:
test_main.py::test_post_registration_returns_201 PASSED test_main.py::test_post_registration_returns_fields PASSED test_main.py::test_post_registration_missing_name_returns_422 PASSED test_main.py::test_get_registrations_is_list PASSED test_main.py::test_delete_registration PASSED test_main.py::test_get_registration_not_found PASSED
â å šãŠ PASS = Green ð¢ (DELETE / 404 ãå«ã㊠6 ä»¶)
ð STEP 4: ãã¹ãå è¡ TDD
æ°æ©èœããã¹ãããæžã
STEP 4 ã®æµã
STEP 4 ã§ããããš
> è§Šããã¡ã€ã«: step4/starter/test_main.py â step4/starter/main.py (ãã¹ãå
è¡ â ã¢ããªå®è£
)
> ããã§åããŠã1 STEP å ã§ãã¹ãã³ãŒããšã¢ããªå®è£ ã®äž¡æ¹ãè§Šãã ã ããã TDD ã®æ¬çªãªãºã ã
æ°æ©èœãçµã蟌ã¿ãããã¹ãããæžã:
GET /registrations?event=FastAPIå匷äŒâ ãã®ã€ãã³ãã ãè¿ãGET /registrations?status=registeredâ æå¹ç³èŸŒã ãè¿ã
æé :
test_main.pyã«æ°æ©èœã®ãã¹ããæžãpytestâ FAIL (Red)main.pyã«ã¯ãšãªãã©ã¡ãŒã¿ã远åpytestâ PASS (Green)
ãã¹ããå ã«æžã (STEP4 ã®èµ·ç¹)
def test_filter_by_event():
# 2çš®é¡ã®ã€ãã³ããç»é²
client.post("/registrations", json={
"name": "Aãã", "email": "a@example.com", "event": "FastAPIå匷äŒ",
})
client.post("/registrations", json={
"name": "Bãã", "email": "b@example.com", "event": "TDDå
¥é",
})
# FastAPIå匷äŒã ãçµã蟌ã
res = client.get("/registrations", params={"event": "FastAPIå匷äŒ"})
assert res.status_code == 200
assert all(item["event"] == "FastAPIå匷äŒ" for item in res.json())ã¯ãšãªãã©ã¡ãŒã¿ã®å®è£
from typing import Optional
@app.get("/registrations")
def list_registrations(
event: Optional[str] = None,
status: Optional[str] = None,
):
result = _registrations
if event is not None:
result = [r for r in result if r["event"] == event]
if status is not None:
result = [r for r in result if r["status"] == status]
return resultTDD ãµã€ã¯ã«ã®ãŸãšã
â ãã¹ããæžã â "filter_by_event ããã¹ã" â¡ pytest å®è¡ â FAIL (Red) â åœç¶ ⢠ã¢ããªå®è£ ãæžã â "ã¯ãšãªãã©ã¡ãŒã¿ã远å " ⣠pytest å®è¡ â PASS (Green) â 宿 †æŽçãã â Refactor (ä»åã¯äžèŠ) â¥ æ¬¡ã®æ©èœãž â â ã«æ»ã
ð STEP 5: APIKey èªèšŒ
管çè æäœãå®ã
STEP 5 ã®æµã
STEP 5 ã§ããããš
> è§Šããã¡ã€ã«: step5/starter/test_main.py â step5/starter/main.py (ãã¹ãå
è¡ â ã¢ããªå®è£
)
> èªèšŒãã¹ã 5 çš®é¡ãå
ã«æžã㊠Red â Depends ã§ã¢ããªå®è£
ã㊠Greenã STEP4 ãšåããªãºã ã
èšèšæž Â§6ãèªèšŒã®æ±ºãŸããã®ã¢ããªå®è£ ãæžã:
X-API-Key: secret-key-123ããããããã°éã- ãªããã° 401 ãè¿ã
- POST ã ãã¯èªèšŒäžèŠ (誰ã§ãç³ã蟌ãã)
Depends ã§èªèšŒãå
±éåãã
from fastapi import Depends, Header, HTTPException
_VALID_API_KEY = "secret-key-123"
def require_api_key(x_api_key: str = Header(default=None)):
if x_api_key != _VALID_API_KEY:
raise HTTPException(status_code=401, detail="invalid api key")â ãã®é¢æ°ãäžåºŠæžãã°ãè€æ°ã®ãšã³ããã€ã³ãã§äœ¿ãåããã
èªèšŒãå¿ èŠãªãšã³ããã€ã³ãã«é©çšãã
@app.get("/registrations", dependencies=[Depends(require_api_key)])
def list_registrations(...):
...
@app.get("/registrations/{reg_id}", dependencies=[Depends(require_api_key)])
def get_registration(reg_id: int):
...
@app.delete("/registrations/{reg_id}", ..., dependencies=[Depends(require_api_key)])
def cancel_registration(reg_id: int):
...â POST 㯠dependencies ãä»ããªã â èªèšŒäžèŠã
ãã¹ãã§èªèšŒã確èªãã
HEADERS = {"X-API-Key": "secret-key-123"}
def test_get_list_without_key_returns_401():
res = client.get("/registrations") # ããŒãªã
assert res.status_code == 401
def test_get_list_with_valid_key():
res = client.get("/registrations", headers=HEADERS)
assert res.status_code == 200X-API-Key ã®ä»çµã¿
ã¯ã©ã€ã¢ã³ã FastAPI ãµãŒããŒ
| |
| GET /registrations |
| X-API-Key: secret-key-123 |
|------------------------------>|
| | require_api_key() ã§æ€èšŒ
| 200 OK |
|<------------------------------| | GET /registrations (ããŒãªã) |
|------------------------------>|
| 401 Unauthorized |
|<------------------------------|STEP 5 åäœç¢ºèª
# ããŒãªã â 401
curl http://localhost:8000/registrations
# â {"detail": "invalid api key"}
# æ£ããã㌠â 200
curl http://localhost:8000/registrations \
-H "X-API-Key: secret-key-123"
# ç³èŸŒã¯èªèšŒäžèŠ
curl -X POST http://localhost:8000/registrations \
-H "Content-Type: application/json" \
-d '{"name":"å±±ç°","email":"a@b.com","event":"å匷äŒ"}'â å®æåœ¢ãã§ãã¯
å®è£ æžã¿ãšã³ããã€ã³ã確èª
| ãšã³ããã€ã³ã | èªèšŒ | ãã¹ã |
|---|---|---|
| POST /registrations | ãªã | â |
| GET /registrations | ãã | â |
| GET /registrations/{id} | ãã | â |
| DELETE /registrations/{id} | ãã | â |
| ã¯ãšãªçµã蟌㿠(event/status) | â | â |
| 401 (èªèšŒãšã©ãŒ) | â | â |
| 404 (æªååš) | â | â |
å šãã¹ãäžæ¬å®è¡
# step5/answer ã«ç§»åã㊠9 ä»¶ã®ãã¹ããå®è¡ãã cd é åžè³æ/ãµã³ãã«ã³ãŒã/step5/answer python3 -m pytest test_main.py -v
äºæ³ãããçµæ: 9 ä»¶ãã¹ãŠ PASSED (èªèšŒ 5 ä»¶ + éåžžæäœ 4 ä»¶)
â å šãŠ PASS ããã°å®æ ð
> 宿å/main.py 㯠step5/answer/main.py ãšåãå
容ã
> ãæçµçã«ããããã³ãŒãã«ãªãããšããåç §çšãªã®ã§ãã¹ãã¯æããªãã
ãã£ã¬ã³ãžé¡
äœè£ãããã°:
- ã¡ãŒã«ã¢ãã¬ã¹æ€èšŒ: Pydantic ã®
EmailStrã䜿ã£ãŠ email ãã©ãŒããããæ€èšŒãã
from pydantic import EmailStr
class RegistrationCreate(BaseModel):
email: EmailStr # äžæ£ãªã¡ãŒã«ã¢ãã¬ã¹ã匟ã
- åãã¡ãŒã«ã¢ãã¬ã¹ã§äºéç³èŸŒãçŠæ¢: POST æã« email ãæ€çŽ¢ã㊠409 Conflict ãè¿ã
- ããŒãžããŒã·ã§ã³:
GET /registrations?limit=10&offset=0ã远å ãã
FAQ
Q: ã€ã³ã¡ã¢ãªã«ä¿åããŠããã®ã§åèµ·åãããšæ¶ãã
A: ããã§ããæ¬¡ã®ã¹ããããšã㊠SQLite (sqlite3) ã JSON ãã¡ã€ã«ãžã®æ°žç¶åããããŸãã
Q: _VALID_API_KEY ãã³ãŒãã«çŽæžãããŠããã®?
A: åŠç¿çšãªã®ã§çŽæžãããŠããŸããæ¬çªã§ã¯ os.environ["API_KEY"] ã§ç°å¢å€æ°ããèªã¿ãŸãã
Q: Optional[str] ã¯äœ?
A: ãã©ã¡ãŒã¿ãçç¥å¯èœ (= æå®ããªããŠããã) ã§ããããšã瀺ããŸãã= None ãšã»ããã§äœ¿ããŸãã
æ¯ãè¿ã
ä»ååŠãã ããš:
- API èšèšæžãšã¯ãäœãåã«äœãäœããæ±ºããææžã
- èšèšæž â ãã¹ã â å®è£ ã®é ã§äœããšä»æ§ãã¬ãèµ·ããªã
- TestClient ã䜿ãã°ãµãŒããŒèµ·åãªãã« API ããã¹ãã§ãã
- TDD (Red â Green â Refactor) ã§ãã¹ããå ã«æžãæå³
- Depends ã§èªèšŒãªã©ã®å ±éåŠçãè€æ°ãšã³ããã€ã³ãã«é©çšã§ãã
次ã®ã¹ããã
- ããŒã¿æ°žç¶å: ã€ã³ã¡ã¢ãª â SQLite (åèµ·åããŠãããŒã¿ãæ®ã)
- åããªããŒã·ã§ã³åŒ·å: EmailStr / æå€§æåæ° / æ£èŠè¡šçŸ
- OpenAPI YAML åºå:
GET /openapi.jsonã®å 容ãä¿åããŠä»ã®ããŒã«ã§äœ¿ã - ç°å¢å€æ°ã§ã®èšå®ç®¡ç:
python-dotenv/pydantic-settings
ãç²ãæ§ã§ããïŒ
èšèšæž â ãã¹ã â å®è£ ã®ãµã€ã¯ã«ãäœéšã§ããŸããã
> ãèšèšæžã¯ã³ãŒãããå ã«æžãããã¹ãã¯å®è£ ããå ã«æžããã