FastAPIλ Python κΈ°λ°μ λΉλκΈ°(Asynchronous) μΉ νλ μμν¬λ‘, λΉ λ₯Έ μ±λ₯κ³Ό κ°κ²°ν μ½λ ꡬ쑰 λλΆμ μ΅κ·Ό λ§μ κ°λ°μλ€μ΄ μ ννκ³ μμ΅λλ€.
νμ§λ§ νλ‘μ νΈκ° 컀μ§κ³ APIκ° λ³΅μ‘ν΄μ§μλ‘ μ½λ λ³κ²½ μ λ²κ·Έλ μμΈ μν©μ΄ λ°μν κ°λ₯μ±λ λμμ§λλ€.
μ΄λ¬ν λ¬Έμ λ₯Ό μλ°©νκΈ° μν΄μλ FastAPI ν
μ€νΈ μ½λ μμ±μ΄ νμμ μ
λλ€.
ν
μ€νΈ μ½λλ λ¨μν κ²μ¦ λ¨κ³λ₯Ό λμ΄, μλΉμ€ μμ μ±κ³Ό μ μ§λ³΄μ ν¨μ¨μ λμ΄λ ν΅μ¬ μμμ
λλ€.
νΉν pytestλ₯Ό νμ©νλ©΄ API λμμ μλμΌλ‘ κ²μ¦νκ³ , λ°μ΄ν°λ² μ΄μ€ μ°λμ΄λ λΉλκΈ° μ²λ¦¬κΉμ§ κ°λ¨ν ν
μ€νΈν μ μμ΅λλ€.
μ΄λ² ν¬μ€ν
μμλ FastAPI ν
μ€νΈ μ½λ μμ± λ°©λ²λΆν° pytestλ₯Ό μ΄μ©ν μ€ν λ° μ»€λ²λ¦¬μ§(coverage) μΈ‘μ λ°©λ², κ·Έλ¦¬κ³ ν
μ€νΈ λ°μ΄ν°μ μμ±Β·μμ λ₯Ό μμ νκ² μ²λ¦¬νλ λ°©λ²κΉμ§ λ¨κ³λ³λ‘ μμλ³΄κ² μ΅λλ€.
π§ λͺ©μ°¨
- FastAPI ν μ€νΈ μ½λμ νμμ±
- νμ ν¨ν€μ§ μ€μΉ λ° νκ²½ μΈν
- Pytestλ₯Ό νμ©ν FastAPI ν μ€νΈ ꡬ쑰
- ν μ€νΈ λ°μ΄ν°μ μΌμμ μμ± λ° μ 리
- μμ° API ν μ€νΈ μ½λ μμ λΆμ
- ν μ€νΈ μ€ν λ° κ²°κ³Ό νμΈ
- pytest-covλ‘ ν μ€νΈ 컀λ²λ¦¬μ§ νμΈνκΈ°
- λ§λ¬΄λ¦¬: μμ μ μΈ FastAPI κ°λ°μ μν ν μ€νΈ μ΅κ΄
π§© FastAPI ν μ€νΈ μ½λμ νμμ±
ν μ€νΈ μ½λλ λ€μκ³Ό κ°μ μ₯μ μ μ 곡ν©λλ€:
- β λ°°ν¬ μ κΈ°λ₯ μ΄μ μ¬λΆλ₯Ό μλμΌλ‘ κ²μ¦
- β μ½λ 리ν©ν λ§ μ μμ μ± λ³΄μ₯
βοΈ νμ ν¨ν€μ§ μ€μΉ λ° νκ²½ μΈν
ν μ€νΈλ₯Ό μννκΈ° μ μ μλ λͺ λ ΉμΌλ‘ νμν ν¨ν€μ§λ₯Ό μ€μΉν©λλ€.
# ν
μ€νΈ κ΄λ ¨ νμ ν¨ν€μ§
pip install pytest httpx
# DB μ°λμ© ν¨ν€μ§
pip install sqlalchemy sqlmodel psycopg2-binary
# ν
μ€νΈ 컀λ²λ¦¬μ§ μΈ‘μ μ© (μ ν)
pip install pytest-cov
π‘ pytest-covλ ν μ€νΈ μ€ν μ€ μ€μ μ½λμ μ€ν λΉμ¨(coverage)μ μΈ‘μ νμ¬,
μ΄λ€ ν¨μκ° ν μ€νΈλμ§ μμλμ§ νλμ νμΈν μ μκ² λμμ€λλ€.
π§± Pytestλ₯Ό νμ©ν FastAPI ν μ€νΈ ꡬ쑰
FastAPI νλ‘μ νΈλ λ³΄ν΅ λ€μκ³Ό κ°μ΄ ꡬμ±λ©λλ€.
project/
βββ app/
β βββ main.py
β βββ routers/
β βββ models/
βββ tests/
β βββ __init__.py
β βββ conftest.py
β βββ test_asset.py
βββ requirements.txt
πΉ conftest.py β κ³΅ν΅ ν΄λΌμ΄μΈνΈ μ μ
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
with TestClient(app) as c:
yield c
TestClientλ FastAPIμ μλν¬μΈνΈλ₯Ό μ€μ μλ² μμ΄ ν
μ€νΈν μ μλλ‘ λμμ£Όλ λꡬμ
λλ€.
π§ͺ ν μ€νΈ λ°μ΄ν°μ μΌμμ μμ± λ° μ 리
ν
μ€νΈ νκ²½μμ μ€μν μ μ βν
μ€νΈμ© λ°μ΄ν°κ° μ€μ DBλ₯Ό μ€μΌμν€μ§ μλ κ²β μ
λλ€.
μ΄λ₯Ό μν΄ pytest.fixtureλ₯Ό νμ©νμ¬ ν
μ€νΈ λ°μ΄ν° μμ±κ³Ό μ 리λ₯Ό μλνν©λλ€.
@pytest.fixture
def sample_asset(client):
# ν
μ€νΈμ© ν¬νΈν΄λ¦¬μ€ μμ±
portfolio_response = client.post("/portfolios/", json={"name": "Test Portfolio", "user_id": 29})
portfolio_id = portfolio_response.json()["id"]
# ν
μ€νΈ μμ° λ°μ΄ν° λ°ν
return {
"name": "Test Asset",
"category": "νκΈ",
"amount": 1000,
"description": "Test Description",
"portfolio_id": portfolio_id,
"average_price": 100.0
}
ν
μ€νΈκ° λλλ©΄ FastAPIμ ν
μ€νΈ ν΄λΌμ΄μΈνΈκ° μΈμ
μ λ«κ³ ,
Docker 컨ν
μ΄λ λ΄ PostgreSQL λ°μ΄ν°λ λ‘€λ°±λλ―λ‘ μ€μ λ°μ΄ν°λ² μ΄μ€μλ μν₯μ΄ μμ΅λλ€.
π§° μμ° API ν μ€νΈ μ½λ μμ λΆμ
μλλ μ€μ μ¬μ© μ€μΈ μμ°(Asset) APIμ ν μ€νΈ μ½λ μμμ λλ€.
# tests/test_asset.py
import pytest
@pytest.fixture
def sample_asset(client):
portfolio_response = client.post("/portfolios/", json={"name": "Test Portfolio", "user_id": 29})
portfolio_id = portfolio_response.json()["id"]
return {
"name": "Test Asset",
"category": "νκΈ",
"amount": 1000,
"description": "Test Description",
"portfolio_id": portfolio_id,
"average_price": 100.0
}
# β
μμ° μμ± ν
μ€νΈ
def test_create_asset(client, sample_asset):
response = client.post("/assets/", json=sample_asset)
assert response.status_code == 200
assert response.json()["name"] == sample_asset["name"]
# β
μμ° μ‘°ν ν
μ€νΈ
def test_read_assets(client):
response = client.get("/assets/")
assert response.status_code == 200
assert isinstance(response.json(), list)
# β
μμ° μμ ν
μ€νΈ
def test_update_asset(client, sample_asset):
create_response = client.post("/assets/", json=sample_asset)
asset_id = create_response.json()["id"]
updated_asset = sample_asset.copy()
updated_asset["name"] = "Updated Asset"
update_response = client.put(f"/assets/{asset_id}", json=updated_asset)
assert update_response.json()["name"] == "Updated Asset"
# β
μμ° μμ ν
μ€νΈ
def test_delete_asset(client, sample_asset):
create_response = client.post("/assets/", json=sample_asset)
asset_id = create_response.json()["id"]
delete_response = client.delete(f"/assets/{asset_id}")
assert delete_response.status_code in (200, 204)
get_response = client.get("/assets/")
ids = [asset["id"] for asset in get_response.json()]
assert asset_id not in ids
κ° ν
μ€νΈλ λ
립μ μΌλ‘ μνλλ©°,
μ€ν μλ§λ€ μμ λ°μ΄ν°κ° μμ±λκ³ ν
μ€νΈ μ’
λ£ ν μλ μμ λ©λλ€.
π§ ν μ€νΈ μ€ν λ° κ²°κ³Ό νμΈ
ν μ€νΈ μ€νμ κ°λ¨ν©λλ€.
# μ 체 λͺ¨λ ν
μ€νΈ
pytest -v
# μΌλΆ λͺ¨λ ν
μ€νΈ
pytest -v backend/tests/test_asset.py
-v: ν μ€νΈ μ΄λ¦κ³Ό κ²°κ³Όλ₯Ό μμΈν μΆλ ₯--maxfail=1: 첫 λ²μ§Έ μ€ν¨ μ μ¦μ μ€λ¨--disable-warnings: κ²½κ³ μ¨κΉ
β μ€ν κ²°κ³Ό μμ

λͺ¨λ ν μ€νΈκ° PASSEDλΌλ©΄ APIκ° μ μμ μΌλ‘ λμνκ³ μμμ μλ―Έν©λλ€.
π pytest-covλ‘ ν μ€νΈ 컀λ²λ¦¬μ§ νμΈνκΈ°
ν
μ€νΈκ° ν΅κ³Όνλ€κ³ ν΄μ μμ¬νκΈ΄ μ΄λ¦
λλ€.
μ΄λ€ μ½λκ° ν
μ€νΈλμ§ μμλμ§λ₯Ό νμ
νκΈ° μν΄μ ν
μ€νΈ 컀λ²λ¦¬μ§(coverage) μΈ‘μ μ΄ νμν©λλ€.
πΉ 1. pytest-cov μ€μΉ
pip install pytest-cov
πΉ 2. 컀λ²λ¦¬μ§ μΈ‘μ μ€ν
# μ 체 λͺ¨λ 컀λ²λ¦¬μ§ μΈ‘μ
pytest --cov=app --cov-report=term-missing -v
# μΌλΆ λͺ¨λ 컀λ²λ¦¬μ§ μΈ‘μ
pytest --cov=routes.asset --cov-report=term-missing -v
μ΅μ μ€λͺ :
--cov=app: 컀λ²λ¦¬μ§λ₯Ό μΈ‘μ ν λͺ¨λ(ν΄λ) μ§μ --cov-report=term-missing: ν μ€νΈλμ§ μμ μ½λ λΌμΈμ μ½μμ νμ-v: μμΈ λͺ¨λ
π μμ μΆλ ₯

μ μμμμ backend/routers/asset.pyκ° **74%**λ§ μ»€λ²λ κ²μ νμΈν μ μμ΅λλ€.
μ¦, νΉμ 쑰건 λΆκΈ°(μ: μμΈ μ²λ¦¬ ꡬ문 λ±)κ° ν
μ€νΈλμ§ μμλ€λ μλ―Έμ
λλ€.
πΉ 3. HTML 리ν¬νΈ μμ±
μ’ λ μκ°μ μΌλ‘ νμΈνκ³ μΆλ€λ©΄ HTML 리ν¬νΈλ₯Ό μμ±ν μ μμ΅λλ€.
pytest --cov=app --cov-report=html
μ€ν ν νλ‘μ νΈ λ£¨νΈμ htmlcov/ ν΄λκ° μμ±λλ©°,
λΈλΌμ°μ μμ htmlcov/index.htmlμ μ΄λ©΄ κ° νμΌλ³ 컀λ²λ¦¬μ§μ λλ½λ λΌμΈμ νλμ λ³Ό μ μμ΅λλ€.
π§ λ§λ¬΄λ¦¬: μμ μ μΈ FastAPI κ°λ°μ μν ν μ€νΈ μ΅κ΄
FastAPI κ°λ°μμ ν
μ€νΈ μ½λλ μ νμ΄ μλ νμμ
λλ€.
νΉν λ€μ μΈ κ°μ§ μμΉμ μ§ν€λ©΄ νμ§μ΄ ν¬κ² ν₯μλ©λλ€.
- ν μ€νΈμ© λ°μ΄ν°λ μμλ‘ μμ±νκ³ ν μ€νΈ ν μμ νλ€.
- pytestλ₯Ό ν΅ν΄ λͺ¨λ μλν¬μΈνΈλ₯Ό μλν ν μ€νΈνλ€.
- pytest-covλ‘ μ»€λ²λ¦¬μ§λ₯Ό μΈ‘μ ν΄ ν μ€νΈ λλ½ κ΅¬κ°μ μ κ²νλ€.
ν
μ€νΈλ λ¨μν βμ½λκ° μ μλνλμ§ νμΈβνλ κ²μ΄ μλλΌ,
π‘ βμ½λλ₯Ό μ λ’°ν μ μκ² λ§λλ κ³Όμ β μ
λλ€.
λ μμΈν μ€μ μ΄λ μ΅μ
μ μλ 곡μ λ¬Έμλ₯Ό μ°Έκ³ νμΈμ.