ํ ์คํธ¶
Starlette ๋๋ถ์ FastAPI ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฝ๊ณ ์ฆ๊ฒ๊ฒ ํ ์คํธํ ์ ์์ต๋๋ค. .
์์ฒญ์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ฏ๋ก ๋งค์ฐ ์น์ํ๊ณ ์ง๊ด์ ์ ๋๋ค.
์ด๋ฅผ ํตํด FastAPI์ ํจ๊ป pytest๋ฅผ ์ง์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
TestClient ์ฌ์ฉ¶
TestClient๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
FastAPI ์ ํ๋ฆฌ์ผ์ด์
์ ์ ๋ฌํ๋ TestClient๋ฅผ ์์ฑํฉ๋๋ค.
test_๋ก ์์ํ๋ ์ด๋ฆ์ผ๋ก ํจ์๋ฅผ ์์ฑํฉ๋๋ค (ํ์ค pytest ๊ท์น).
requests์ ๋์ผํ ๋ฐฉ์์ผ๋ก TestClient ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํฉ๋๋ค.
ํ์ธํด์ผ ํ๋ ํ์ค Python ํํ์์ผ๋ก ๊ฐ๋จํ assert ๋ฌธ์ ์์ฑํฉ๋๋ค(ํ์ค pytest ๊ท์น).
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
ํ
ํ
์คํธ ํจ์๋ async def๊ฐ ์๋ ์ผ๋ฐ def์
๋๋ค.
๊ทธ๋ฆฌ๊ณ ํด๋ผ์ด์ธํธ์ ๋ํ ํธ์ถ๋ await๋ฅผ ์ฌ์ฉํ์ง ์๋ ์ผ๋ฐ ํธ์ถ์
๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด ๋ณต์กํจ ์์ด pytest๋ฅผ ์ง์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๊ธฐ์ ์ ์ธ๋ถ ์ฌํญ
from starlette.testclient import TestClient๋ฅผ ํตํด ์ฌ์ฉํ ์ ์์ต๋๋ค.
FastAPI๋ ๊ฐ๋ฐ์ ์ฌ๋ฌ๋ถ์ ํธ์๋ฅผ ์ํด fastapi.testclient์ ๋์ผํ starlette.testclient๋ฅผ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฌ๋ ๊ทธ๊ฒ์ Starlette์์ ์ง์ ์ ๊ณต๋ฉ๋๋ค.
ํ
FastAPI ์ ํ๋ฆฌ์ผ์ด์
(์: ๋น๋๊ธฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํจ์)์ ์์ฒญ์ ๋ณด๋ด๋ ๊ฒ๊ณผ ๋ณ๋๋ก ํ
์คํธ์์ async ํจ์๋ฅผ ํธ์ถํ๋ ค๋ฉด ๊ณ ๊ธ ์์ต์์ Async Tests ๋ฅผ ์ดํด๋ณด์ญ์์ค.
ํ ์คํธ ๋ถ๋ฆฌ¶
์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ํ ์คํธ๊ฐ ๋ค๋ฅธ ํ์ผ์ ์์ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ FastAPI ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ๋ฌ ํ์ผ/๋ชจ๋ ๋ฑ์ผ๋ก ๊ตฌ์ฑ๋ ์๋ ์์ต๋๋ค.
FastAPI ์ฑ ํ์ผ¶
FastAPI ์ฑ์ main.py ํ์ผ์ด ์๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
ํ ์คํธ ํ์ผ¶
๊ทธ๋ฐ ๋ค์ ํ
์คํธ์ ํจ๊ป test_main.py ํ์ผ์ ๋ง๋ค๊ณ main ๋ชจ๋(main.py)์์ app์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
ํ ์คํธ: ํ์ฅ๋ ์์ ¶
์ด์ ์ด ์์ ๋ฅผ ํ์ฅํ๊ณ ์ธ๋ถ ์ฌํญ์ ์ถ๊ฐํ์ฌ ๋ค๋ฅธ ๋ถ๋ถ์ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
ํ์ฅ๋ FastAPI ์ฑ ํ์ผ¶
FastAPI ์ฑ์ main_b.py ํ์ผ์ด ์๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค.
์ค๋ฅ๋ฅผ ๋ฐํํ ์ ์๋ GET ์์
์ด ์์ต๋๋ค.
์ฌ๋ฌ ์ค๋ฅ๋ฅผ ๋ฐํํ ์ ์๋ POST ์์
์ด ์์ต๋๋ค.
๋ ๊ฒฝ๋ก ์์
๋ชจ๋ X-Token ํค๋๊ฐ ํ์ํฉ๋๋ค.
from typing import Union
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: Union[str, None] = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item
ํ์ฅ ํ ์คํธ ํ์ผ¶
๋ค์์ผ๋ก ํ์ฅ ํ
์คํธ๋ฅผ ํตํด ์ด์ ๊ณผ ๋์ผํ test_main_b.py๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "There goes my hero",
}
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_read_inexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
)
assert response.status_code == 200
assert response.json() == {
"id": "foobar",
"title": "Foo Bar",
"description": "The Foo Barters",
}
def test_create_item_bad_token():
response = client.post(
"/items/",
headers={"X-Token": "hailhydra"},
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_create_existing_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={
"id": "foo",
"title": "The Foo ID Stealers",
"description": "There goes my stealer",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Item already exists"}
ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ์ ๋ณด๋ฅผ ์ ๋ฌํด์ผ ํ๋๋ฐ ๋ฐฉ๋ฒ์ ๋ชจ๋ฅผ ๋๋ง๋ค '์์ฒญ'์์ ๋ฐฉ๋ฒ์ ๊ฒ์ํ ์ ์์ต๋๋ค(Google).
๊ทธ๋ฐ ๋ค์ ํ ์คํธ์์ ๋์ผํ ์์ ์ ์ํํฉ๋๋ค.
์:
- ๊ฒฝ๋ก ํน์ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํ๋ ค๋ฉด URL ์์ฒด์ ์ถ๊ฐํ์ญ์์ค.
- JSON ๋ณธ๋ฌธ์ ์ ๋ฌํ๋ ค๋ฉด ํ์ด์ฌ ๊ฐ์ฒด(์:
dict)๋ฅผ ๋งค๊ฐ๋ณ์json์ ์ ๋ฌํฉ๋๋ค. - JSON ๋์ ๋ฐ์ดํฐ์์ ๋ณด๋ด์ผ ํ๋ค๋ฉด
data๋งค๊ฐ๋ณ์๋ฅผ ๋์ ์ฌ์ฉํ์ญ์์ค. - headers๋ฅผ ์ ๋ฌํ๋ ค๋ฉด
headers๋งค๊ฐ๋ณ์์dict๋ฅผ ์ฌ์ฉํ์ญ์์ค. - cookies์ ๊ฒฝ์ฐ
cookies๋งค๊ฐ๋ณ์์dict.
๋ฐ์ดํฐ๋ฅผ ๋ฐฑ์๋์ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ(requests ๋๋ TestClient ์ฌ์ฉ)์ ๋ํ ์์ธํ ๋ด์ฉ์ ๋ฌธ์ ์์ฒญ์ ํ์ธํ์ญ์์ค.
์ ๋ณด
TestClient๋ Pydantic ๋ชจ๋ธ์ด ์๋ JSON์ผ๋ก ๋ณํํ ์ ์๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํฉ๋๋ค.
ํ
์คํธ์ Pydantic ๋ชจ๋ธ์ด ์๊ณ ํ
์คํธ ์ค์ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ ํ๋ฆฌ์ผ์ด์
์ผ๋ก ๋ณด๋ด๋ ค๋ฉด JSON ํธํ ์ธ์ฝ๋์ ์ค๋ช
๋ jsonable_encoder๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. .
์คํ¶
๊ทธ๋ฐ ๋ค์ pytest๋ฅผ ์ค์นํ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
$ pip install pytest
---> 100%
ํ์ผ๊ณผ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ๊ฐ์งํ๊ณ ์คํํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ๋ณด๊ณ ํฉ๋๋ค.
๋ค์์ ์ฌ์ฉํ์ฌ ํ ์คํธ๋ฅผ ์คํํฉ๋๋ค.
$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>