πŸš€ FastAPI API ν…ŒμŠ€νŠΈ (pytest + pytest-cov)

FastAPIλŠ” Python 기반의 비동기(Asynchronous) μ›Ή ν”„λ ˆμž„μ›Œν¬λ‘œ, λΉ λ₯Έ μ„±λŠ₯κ³Ό κ°„κ²°ν•œ μ½”λ“œ ꡬ쑰 덕뢄에 졜근 λ§Žμ€ κ°œλ°œμžλ“€μ΄ μ„ νƒν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
ν•˜μ§€λ§Œ ν”„λ‘œμ νŠΈκ°€ 컀지고 APIκ°€ λ³΅μž‘ν•΄μ§ˆμˆ˜λ‘ μ½”λ“œ λ³€κ²½ μ‹œ λ²„κ·Έλ‚˜ μ˜ˆμ™Έ 상황이 λ°œμƒν•  κ°€λŠ₯성도 λ†’μ•„μ§‘λ‹ˆλ‹€.

μ΄λŸ¬ν•œ 문제λ₯Ό μ˜ˆλ°©ν•˜κΈ° μœ„ν•΄μ„œλŠ” FastAPI ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„±μ΄ ν•„μˆ˜μ μž…λ‹ˆλ‹€.
ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” λ‹¨μˆœν•œ 검증 단계λ₯Ό λ„˜μ–΄, μ„œλΉ„μŠ€ μ•ˆμ •μ„±κ³Ό μœ μ§€λ³΄μˆ˜ νš¨μœ¨μ„ λ†’μ΄λŠ” 핡심 μš”μ†Œμž…λ‹ˆλ‹€.
특히 pytestλ₯Ό ν™œμš©ν•˜λ©΄ API λ™μž‘μ„ μžλ™μœΌλ‘œ κ²€μ¦ν•˜κ³ , λ°μ΄ν„°λ² μ΄μŠ€ μ—°λ™μ΄λ‚˜ 비동기 μ²˜λ¦¬κΉŒμ§€ κ°„λ‹¨νžˆ ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

이번 ν¬μŠ€νŒ…μ—μ„œλŠ” FastAPI ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± 방법뢀터 pytestλ₯Ό μ΄μš©ν•œ μ‹€ν–‰ 및 컀버리지(coverage) μΈ‘μ • 방법, 그리고 ν…ŒμŠ€νŠΈ λ°μ΄ν„°μ˜ μƒμ„±Β·μ‚­μ œλ₯Ό μ•ˆμ „ν•˜κ²Œ μ²˜λ¦¬ν•˜λŠ” λ°©λ²•κΉŒμ§€ λ‹¨κ³„λ³„λ‘œ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

🧭 λͺ©μ°¨

  1. FastAPI ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ ν•„μš”μ„±
  2. ν•„μš” νŒ¨ν‚€μ§€ μ„€μΉ˜ 및 ν™˜κ²½ μ„ΈνŒ…
  3. Pytestλ₯Ό ν™œμš©ν•œ FastAPI ν…ŒμŠ€νŠΈ ꡬ쑰
  4. ν…ŒμŠ€νŠΈ λ°μ΄ν„°μ˜ μΌμ‹œμ  생성 및 정리
  5. μžμ‚° API ν…ŒμŠ€νŠΈ μ½”λ“œ 예제 뢄석
  6. ν…ŒμŠ€νŠΈ μ‹€ν–‰ 및 κ²°κ³Ό 확인
  7. pytest-cov둜 ν…ŒμŠ€νŠΈ 컀버리지 ν™•μΈν•˜κΈ°
  8. 마무리: μ•ˆμ •μ μΈ 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 κ°œλ°œμ—μ„œ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” 선택이 μ•„λ‹Œ ν•„μˆ˜μž…λ‹ˆλ‹€.
특히 λ‹€μŒ μ„Έ κ°€μ§€ 원칙을 μ§€ν‚€λ©΄ ν’ˆμ§ˆμ΄ 크게 ν–₯μƒλ©λ‹ˆλ‹€.

  1. ν…ŒμŠ€νŠΈμš© λ°μ΄ν„°λŠ” μž„μ‹œλ‘œ μƒμ„±ν•˜κ³  ν…ŒμŠ€νŠΈ ν›„ μ‚­μ œν•œλ‹€.
  2. pytestλ₯Ό 톡해 λͺ¨λ“  μ—”λ“œν¬μΈνŠΈλ₯Ό μžλ™ν™” ν…ŒμŠ€νŠΈν•œλ‹€.
  3. pytest-cov둜 컀버리지λ₯Ό μΈ‘μ •ν•΄ ν…ŒμŠ€νŠΈ λˆ„λ½ ꡬ간을 μ κ²€ν•œλ‹€.

ν…ŒμŠ€νŠΈλŠ” λ‹¨μˆœνžˆ β€œμ½”λ“œκ°€ 잘 μž‘λ™ν•˜λŠ”μ§€ ν™•μΈβ€ν•˜λŠ” 것이 μ•„λ‹ˆλΌ,
πŸ’‘ β€œμ½”λ“œλ₯Ό μ‹ λ’°ν•  수 있게 λ§Œλ“œλŠ” 과정” μž…λ‹ˆλ‹€.

더 μžμ„Έν•œ μ„€μ •μ΄λ‚˜ μ˜΅μ…˜μ€ μ•„λž˜ 곡식 λ¬Έμ„œλ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

πŸ”— μ°Έκ³ 

λŒ“κΈ€ 남기기