๐Ÿงช FastAPI ์•ฑ ํ…Œ์ŠคํŠธ – Pytest๋กœ ์•ˆ์ „ํ•œ ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ

๐Ÿ•’ ์•ฝ 2๋ถ„ ์ฝ๋Š” ๋ฐ ์†Œ์š”๋ฉ๋‹ˆ๋‹ค

์ด์ „ ๊ธ€ ๋ณด๊ธฐ(FastAPI MySQL ์—ฐ๋™ ๋ฐ CRUD)

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์ˆœํ•œ ์„ ํƒ์ด ์•„๋‹Œ ์ƒ์กด์ด๋‹ค. ๋ฒ„๊ทธ๋ฅผ ์ค„์ด๊ณ , ์ „์ฒด ์„œ๋น„์Šค์˜ ์•ˆ์ •์„ฑ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ์ค‘์š”ํ•œ ๋„๊ตฌ๋‹ค. ํŠนํžˆ FastAPI๋Š” ํ…Œ์ŠคํŠธ ์นœํ™”์ ์ธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ์†์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” Pytest๋ฅผ ์ค‘์‹ฌ์œผ๋กœ FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ ํ†ตํ•ด ์–ด๋–ค ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ Windows ํ™˜๊ฒฝ ๊ธฐ์ค€์œผ๋กœ ์ •๋ฆฌํ•ด๋ณด์ž.


๐Ÿงฐ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

FastAPI์—์„œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋‘ ๊ฐ€์ง€ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•œ๋‹ค. ํ•˜๋‚˜๋Š” pytest, ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” FastAPI์—์„œ ์ œ๊ณตํ•˜๋Š” TestClient์ด๋‹ค. ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜์ž.

pip install pytest httpx
  • pytest: Python์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค.
  • httpx: TestClient๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์˜์กดํ•˜๋Š” ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ์ด๋‹ค.

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ค€๋น„๋Š” ๋์ด๋‹ค. ์ด์ œ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž.


๐Ÿงช ํ…Œ์ŠคํŠธํ•  ๊ธฐ๋ณธ API ๋งŒ๋“ค๊ธฐ

์•„๋ž˜๋Š” ์˜ˆ์ œ๋กœ ์‚ฌ์šฉํ•  FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด๋‹ค. main.py๋ผ๋Š” ์ด๋ฆ„์˜ ํŒŒ์ผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

์ด์ œ ์ด API์˜ ๊ธฐ๋Šฅ์„ pytest๋กœ ํ…Œ์ŠคํŠธํ•ด๋ณผ ๊ฒƒ์ด๋‹ค.


๐Ÿงช TestClient๋กœ API ํ…Œ์ŠคํŠธํ•˜๊ธฐ

FastAPI๋Š” Starlette ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๊ธฐ ๋•Œ๋ฌธ์—, ์ž์ฒด์ ์œผ๋กœ TestClient๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์ด ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ณ„๋„๋กœ ์„œ๋ฒ„๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ ๋„ API ์š”์ฒญ์„ ํ‰๋‚ด๋‚ผ ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ด๋‹ค. test_main.py ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์ž‘์„ฑํ•œ๋‹ค.

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}
  • TestClient(app): ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์ค€๋‹ค.
  • client.get("/"): ์‹ค์ œ GET ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํ–‰๋™ํ•œ๋‹ค.
  • assert: ์‘๋‹ต์ด ๊ธฐ๋Œ€ํ•œ ํ˜•ํƒœ์ธ์ง€ ๊ฒ€์ฆํ•œ๋‹ค.

๐Ÿ” ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐ ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ

๋‹ค์Œ์€ ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์˜ˆ์‹œ์ด๋‹ค.

def test_read_item():
    response = client.get("/items/42?q=fastapi")
    assert response.status_code == 200
    assert response.json() == {"item_id": 42, "q": "fastapi"}

๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์ œ๋Œ€๋กœ ๋ฐ”์ธ๋”ฉ๋˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด๋„ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.


โœ‰๏ธ POST ์š”์ฒญ ํ…Œ์ŠคํŠธ

POST ์š”์ฒญ์—์„œ๋Š” json= ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ฐ”๋””๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜๋Š” POST API์™€ ์ด์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์˜ˆ์‹œ์ด๋‹ค.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float

@app.post("/items/")
def create_item(item: Item):
    return {"name": item.name, "price": item.price}

์ด์ œ ํ•ด๋‹น API์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

def test_create_item():
    response = client.post("/items/", json={"name": "Keyboard", "price": 49.99})
    assert response.status_code == 200
    assert response.json() == {"name": "Keyboard", "price": 49.99}

FastAPI๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์„ ์ž๋™์œผ๋กœ Pydantic ๋ชจ๋ธ๋กœ ํŒŒ์‹ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ ์—ญ์‹œ ์ง๊ด€์ ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿงต ๋น„๋™๊ธฐ ํ•จ์ˆ˜ ํ…Œ์ŠคํŠธ

FastAPI๋Š” ๋น„๋™๊ธฐ๋ฅผ ์ง€์›ํ•˜๋ฏ€๋กœ async def๋กœ ์ž‘์„ฑ๋œ ์—”๋“œํฌ์ธํŠธ๋„ ๋งŽ๋‹ค. ๋‹คํ–‰ํžˆ๋„ TestClient๋Š” ์ด ๋ถ€๋ถ„๋„ ๋ฌธ์ œ์—†์ด ์ฒ˜๋ฆฌํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ์ฒ˜๋Ÿผ ์ž‘์„ฑ๋œ ๋น„๋™๊ธฐ API๋„ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

@app.get("/async-endpoint")
async def async_endpoint():
    return {"message": "๋น„๋™๊ธฐ ์ž‘๋™ ์™„๋ฃŒ!"}
def test_async_endpoint():
    response = client.get("/async-endpoint")
    assert response.status_code == 200
    assert response.json() == {"message": "๋น„๋™๊ธฐ ์ž‘๋™ ์™„๋ฃŒ!"}

๐Ÿ” ๋ฐ˜๋ณต ํ…Œ์ŠคํŠธ – ํŒŒ๋ผ๋ฏธํ„ฐํ™”

๊ฐ™์€ ๊ตฌ์กฐ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์—ฌ๋Ÿฌ ์ผ€์ด์Šค๋กœ ๋ฐ˜๋ณตํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด pytest.mark.parametrize๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

import pytest

@pytest.mark.parametrize("item_id, q", [
    (1, "apple"),
    (2, "banana"),
    (3, "cherry")
])
def test_read_item_param(item_id, q):
    response = client.get(f"/items/{item_id}?q={q}")
    assert response.status_code == 200
    assert response.json() == {"item_id": item_id, "q": q}

์ด ๋ฐฉ์‹์€ ์—ฌ๋Ÿฌ ์ž…๋ ฅ๊ฐ’์— ๋Œ€ํ•ด ๋™์ผํ•œ ๋กœ์ง์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋ฐ˜๋ณต ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด ์ƒ์‚ฐ์„ฑ์ด ๋งค์šฐ ๋†’๋‹ค.


๐Ÿงพ ํ…Œ์ŠคํŠธ ์‹คํ–‰ํ•˜๊ธฐ

์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ์‹คํ–‰ํ•œ๋‹ค. Windows ํ™˜๊ฒฝ์—์„œ๋Š” PowerShell ๋˜๋Š” CMD์—์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ์ž…๋ ฅํ•˜๋ฉด ๋œ๋‹ค.

pytest

๋ณด๋‹ค ์ƒ์„ธํ•œ ๋กœ๊ทธ๋ฅผ ์›ํ•œ๋‹ค๋ฉด -v ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

pytest -v

๐Ÿงผ ํ…Œ์ŠคํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์„ฑ ํŒ

์‹ค๋ฌด์—์„œ๋Š” ํ…Œ์ŠคํŠธ ํŒŒ์ผ์ด ๋งŽ์•„์ง€๋ฏ€๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

project/
โ”œโ”€โ”€ app/
โ”‚   โ””โ”€โ”€ main.py
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ test_main.py

์ด ๊ตฌ์กฐ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด pytest๋Š” tests/ ๋””๋ ‰ํ„ฐ๋ฆฌ ์•„๋ž˜์˜ ๋ชจ๋“  test_*.py ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ธ์‹ํ•˜๊ณ  ์‹คํ–‰ํ•œ๋‹ค.


๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

FastAPI์˜ ํ…Œ์ŠคํŠธ๋Š” ์‹œ์ž‘์ด ์‰ฝ๊ณ , ํ™•์žฅ๋„ ๋งค์šฐ ์œ ์—ฐํ•˜๋‹ค. Pytest์™€ TestClient๋ฅผ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด API์˜ ๋™์ž‘ ์—ฌ๋ถ€๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฆฌํŒฉํ† ๋ง์ด๋‚˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€์—๋„ ์ž์‹ ๊ฐ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” ์ฝ”๋“œ๋ฅผ ๋ฏฟ๊ฒŒ ๋งŒ๋“œ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค. ๊ฐœ๋ฐœ ์ดˆ๊ธฐ๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์Šต๊ด€์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๋†’์€ ํ’ˆ์งˆ์˜ ๋ฐฑ์—”๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฐ€์žฅ ๋น ๋ฅธ ๊ธธ์ด๋‹ค.

FastAPI ๊ณต์‹ ๋ฌธ์„œ : https://fastapi.tiangolo.com/ko/

fastapi-logo,fastapi-๊ฐœ๋ฐœ-ํ™˜๊ฒฝ-์„ค์ •, fastapi-์•ฑ-๋งŒ๋“ค๊ธฐ, fastapi-๋ผ์šฐํŒ…, fastapi-request-response, fastapi-์˜ˆ์™ธ-์ฒ˜๋ฆฌ, fastapi-mysql-์—ฐ๋™, fastapi-ํ…Œ์ŠคํŠธ-pytest