ํ ์คํธ¶
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>