๋ณธ ํฌ์คํ ์ ์๋ ์๋ฃ๋ค์ ์ฐธ๊ณ ํด์ ์์ฑ๋์์ต๋๋ค.
- pytest๋?
- pytest๋ ํ์ด์ฌ ๋ํ ํ ์คํธ ํ๋ ์์ํฌ
- ํ์ผ/ํจ์ ๋ค์ด๋ฐ๋ง ์ง์ผ๋ ์๋์ผ๋ก ํ ์คํธ ํ์ง(autodiscovery)
- ๋จ์
assert
๋ฌธ์ผ๋ก๋ ํ๋ถํ ์คํจ ๋ฆฌํฌํธ ์ ๊ณต (Rich Assertion Introspection)- Fixture/Parametrize/Markers ๋ฑ์ผ๋ก ์ฌ์ฌ์ฉ์ฑยท์ ์ฐ์ฑ โ
- ํต์ฌ ํ ์คํธ ์ ํ
- ์ ๋ ํ ์คํธ: ์์ ํจ์/๋ฉ์๋์ ์ ํ์ฑ ๊ฒ์ฆ
- ๊ฒฝ๊ณ๊ฐ ํ ์คํธ: ์๊ณ์นยท์ฃ์ง ์ ๋ ฅ์์ ์์ ์ฑ ํ์ธ
- ์์ธ ์ฒ๋ฆฌ: ์๋ชป๋ ์ ๋ ฅ โ ๋ช ์์ ์๋ฌ ๋ฐ์ ์ฌ๋ถ ํ์ธ
- ํฝ์ค์ฒ: ๋ฐ๋ณต๋๋ ์ค๋น/์ ๋ฆฌ ๋ก์ง ๋ถ๋ฆฌ
- ๋ง์ปค:
slow
,skip
,xfail
ํ๊น ์ผ๋ก ์คํ ์ ์ด- ํ๋ผ๋ฏธํฐํ: ๋ค์ํ ์ ๋ ฅ/์ถ๋ ฅ ์ผ์ด์ค๋ฅผ ํ ๋ฒ์ ๊ฒ์ฆ
- ๋ชจํน(Mock): ์ธ๋ถ API/DB ์์กด์ฑ ์ ๊ฑฐ, ๊ฐ์ง ๊ฐ์ฒด๋ก ๊ฒฉ๋ฆฌ
- ์ค์ฉ ํฌ์ธํธ
- IDE(Pycharm, VSCode)์์ ๋ฐ๋ก ์คํ ๋ฒํผ ์ ๊ณต
- unittest/nose ํธํ โ ๊ธฐ์กด ์ฝ๋๋ pytest๋ก ๋๋ฆด ์ ์์
- ์๋ง์ ํ๋ฌ๊ทธ์ธ(Django, Pandas ๋ฑ)์ผ๋ก ํ์ฅ์ฑ ํ๋ถ
- AI ํ์ฉ
- ChatGPT ๊ฐ์ LLM์๊ฒ ์ฝ๋ ๋ถ์ฌ๋ฃ๊ณ โpytest ์คํ์ผ๋ก ํ ์คํธ ์์ฑํด์คโ
โ ํ ์คํธ ์ค์บํด๋ ์๋ ์์ฑ- ๋๋ฝ๋ ์ผ์ด์ค๋ ํ๋ผ๋ฏธํฐํ ์์ด๋์ด ์ ์๋ฐ๊ธฐ ์ฉ์ด
์ํํธ์จ์ด ๊ฐ๋ฐ์์ ํ
์คํธ
๋ โ์ฝ๋๊ฐ ์ฝ์ํ ์กฐ๊ฑด์ ์งํค๋๊ฐ?โ๋ฅผ ์๋์ผ๋ก ๊ฒ์ฆํ๋ ๊ณผ์ ์
๋๋ค.
ํ ์คํธ๋ฅผ ์ ์ค๊ณํ๋ฉด ๋ฆฌํฉํฐ๋ง์ ์์ ๊ฐ์ ์ป๊ณ , ๋ฒ๊ทธ๋ฅผ ์ด๊ธฐ์ ์ก์ ์ ์์ผ๋ฉฐ, ํ์ ์ ์ ๋ขฐ์ฑ์ด ์ฌ๋ผ๊ฐ๋๋ค.
ํ ์คํธ์ ๊ธฐ๋ณธ ์ฒ ํ์ AAA(ArrangeโActโAssert, 3A)์ ๋๋ค.:
pytest๋ ํ์ด์ฌ์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ํ ์คํธ ํ๋ ์์ํฌ์ ๋๋ค.
test_*.py
๋๋ *_test.py
ํ์์ ํ์ผ, test_*
ํจ์๋ช
์ ์๋์ผ๋ก ํ์งassert
์คํจ ์ ์ค์ ๊ฐ๊ณผ ๊ธฐ๋ ๊ฐ์ ์ง๊ด์ ์ผ๋ก ์ถ๋ ฅassertEqual
๊ฐ์ ๋ฉ์๋๋ณด๋ค ์ง๊ด์ @pytest.mark.slow
, skip
, xfail
๋ก ์คํ/์ ์ธ ์ ์ด ๋ฐ ๋ฆฌํฌํธ์ ํ๊ทธ ํ์๊ทธ๋์ pytest๋ ์ด๋ป๊ฒ ์คํํ๋๊ฐ!?
# ํ๋ก์ ํธ ์ ์ฒด ํ
์คํธ ์คํ
pytest
# ํน์ ํ์ผ์ ํ
์คํธ๋ง ์คํ
pytest tests/test_unit.py
# ํน์ ํจ์๋ง ์คํ
pytest tests/test_unit.py::test_normalize_whitespace
# ์์ธํ ์ถ๋ ฅ์ผ๋ก ์คํ (๊ฐ ํ
์คํธ ์ด๋ฆ๊ณผ ๊ฒฐ๊ณผ ํ์)
pytest -v
# ์คํจ ์ ์ฆ์ ์ค๋จ
pytest -x
# print๋ฌธ ์ถ๋ ฅ ๋ณด๊ธฐ (๋๋ฒ๊น
์ฉ)
pytest -s
# ๋๋ฆฐ ํ
์คํธ ์ ์ธํ๊ณ ๋น ๋ฅธ ํ
์คํธ๋ง
pytest -m "not slow"
# ํตํฉ ํ
์คํธ๋ง ์คํ
pytest -m integration
# ์ธ๋ถ ์์กด์ฑ์ด ์๋ ํ
์คํธ๋ง
pytest -m "not external"
# ์ฌ๋ฌ ์กฐ๊ฑด ์กฐํฉ
pytest -m "not slow and not integration"
# ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง์ ํจ๊ป ์คํ
pytest --cov=app
# HTML ๋ฆฌํฌํธ ์์ฑ
pytest --cov=app --cov-report=html
# ์ต์ ์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ ์ค์ (80% ๋ฏธ๋ง์ ์คํจ)
pytest --cov=app --cov-fail-under=80
โ ์ฑ๊ณต์ ์ธ ์คํ ์์:
$ pytest tests/ -v
=================== test session starts ===================
platform darwin -- Python 3.11.0
cachedir: .pytest_cache
rootdir: /project
collected 8 items
tests/test_unit.py::test_normalize_whitespace PASSED [12%]
tests/test_unit.py::test_clip[-0.1-0.0] PASSED [25%]
tests/test_unit.py::test_clip[0-0] PASSED [37%]
tests/test_unit.py::test_clip[0.5-0.5] PASSED [50%]
tests/test_unit.py::test_clip[1-1] PASSED [62%]
tests/test_unit.py::test_clip[1.1-1] PASSED [75%]
tests/test_integration.py::test_db_flow PASSED [87%]
tests/test_integration.py::test_api_flow PASSED [100%]
=================== 8 passed in 1.23s ===================
๐ ํด์:
collected 8 items
: ์ด 8๊ฐ ํ
์คํธ ๋ฐ๊ฒฌPASSED
: ๊ฐ ํ
์คํธ ์ฑ๊ณต[25%]
: ์ ์ฒด ์งํ๋ฅ 8 passed in 1.23s
: ๋ชจ๋ ํ
์คํธ๊ฐ 1.23์ด ๋ง์ ์๋ฃโ ์คํจ๊ฐ ์๋ ๊ฒฝ์ฐ:
$ pytest tests/test_unit.py::test_safe_div_raises -v
=================== FAILURES ===================
_______ test_safe_div_raises _______
def test_safe_div_raises():
with pytest.raises(ValueError, match="nonzero"):
> safe_div(1, 0)
E Failed: DID NOT RAISE ValueError
tests/test_unit.py:23: Failed
=================== short test summary info ===================
FAILED tests/test_unit.py::test_safe_div_raises - Failed: DID NOT RAISE ValueError
=================== 1 failed in 0.08s ===================
๐ ํด์:
FAILURES
์น์
์์ ์คํจ ์์ธ ์ ๋ณด ํ์ธ>
ํ์๋ ๋ผ์ธ์์ ์คํจ ๋ฐ์E
๋ผ์ธ์์ ์คํจ ์ด์ ์ค๋ช
ValueError
๊ฐ ๋ฐ์ํ ๊ฒ์ผ๋ก ์์ํ์ง๋ง ๋ฐ์ํ์ง ์์๋น ๋ฅธ ํผ๋๋ฐฑ์ ์ํ ์คํ ์ ๋ต:
# ๊ฐ๋ฐ ์ค: ๋น ๋ฅธ ์ ๋ ํ
์คํธ๋ง
pytest -m "not slow and not integration" --ff
# ์ปค๋ฐ ์ : ๋ณ๊ฒฝ๋ ํ์ผ ๊ด๋ จ ํ
์คํธ
pytest --lf # ๋ง์ง๋ง ์คํจ ํ
์คํธ๋ง
pytest --ff # ์คํจํ๋ ํ
์คํธ ์ฐ์ ์คํ
# CI/๋ฐฐํฌ ์ : ์ ์ฒด ํ
์คํธ + ์ปค๋ฒ๋ฆฌ์ง
pytest --cov=app --cov-fail-under=80
๋๋ฒ๊น ์ ์ํ ์ต์ :
# ์ฒซ ์คํจ์์ ์ค๋จํ๊ณ ๋๋ฒ๊ฑฐ ์ง์
pytest --pdb -x
# ๋ ์์ธํ ์ถ๋ ฅ
pytest -vv
# ๊ฒฝ๊ณ ๋ฉ์์ง ์จ๊ธฐ๊ธฐ
pytest --disable-warnings
# ํน์ ๋ก๊ทธ ๋ ๋ฒจ ์ค์
pytest --log-cli-level=DEBUG
pytest.ini ์ค์ ์์:
[tool:pytest]
minversion = 6.0
addopts = -ra -q --strict-markers --strict-config
testpaths = tests
markers =
slow: ์คํ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ํ
์คํธ
integration: ์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ๊ฒฐํฉ๋ ํ
์คํธ
external: ์ธ๋ถ ์๋น์ค์ ์์กดํ๋ ํ
์คํธ
unit: ๋จ์ผ ํจ์/ํด๋์ค๋ง ํ
์คํธํ๋ ์ ๋ ํ
์คํธ
์คํ ์๋๋ฆฌ์ค๋ณ ๋ช ๋ น์ด ์ ๋ฆฌ:
# ๐ ๊ฐ๋ฐ ์ค (๋น ๋ฅธ ํผ๋๋ฐฑ)
pytest -m "unit" -x
# ๐ ํน์ ๊ธฐ๋ฅ ๊ฐ๋ฐ/๋๋ฒ๊น
pytest tests/test_feature.py -v -s
# ๐ PR/์ปค๋ฐ ์ ์ ๊ฒ
pytest -m "not external" --cov=app
# ๐ฏ CI/CD ์ ์ฒด ๊ฒ์ฆ
pytest --cov=app --cov-report=xml --junitxml=test-results.xml
# ๐ ์คํจ ์์ธ ๋ถ์
pytest --lf --pdb -v
์ด์ ํ ์คํธ ์คํ์ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ์ผ๋, ๊ตฌ์ฒด์ ์ธ ํ ์คํธ ์ ํ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
def normalize_whitespace(s: str) -> str:
import re
return re.sub(r"\s+", " ", s).strip()
def test_normalize_whitespace():
assert normalize_whitespace(" Hello World ") == "Hello World"
๋ฌด์์ ๊ฒ์ฆํ๋?
์ฌ๋ฌ ๊ฐ์ ๊ณต๋ฐฑ/๊ฐํ/ํญ
โ ๋จ์ผ ๊ณต๋ฐฑ
โ, โ์๋ค ๊ณต๋ฐฑ ์ ๊ฑฐ
โํจ์ค ์กฐ๊ฑด
" Hello World "
๊ฐ ์ ํํ "Hello World"
๋ก ๋ณํ๋๋ฉด ํต๊ณผํฉ๋๋ค.์คํจ ์์
strip()
์ ๋๋ฝํ๋ฉด ๊ฒฐ๊ณผ๊ฐ "Hello World"
์ฒ๋ผ ์๋ค ๊ณต๋ฐฑ์ด ๋จ์ ์คํจํฉ๋๋ค.\s+
๋์ " "
๋ง ์นํํ๋ฉด ํญ/๊ฐํ(\n
, \t
)์ด ํ ์นธ์ผ๋ก ํฉ์ณ์ง์ง ์์ ์คํจํฉ๋๋ค.์ ํ์ํ๊ฐ?
ํ์ฅ ์์ด๋์ด
\u00A0
)๋ ์ผ์ด์คํํ์ธ์.import pytest
def clip(x, lo=0, hi=1):
return max(lo, min(x, hi))
@pytest.mark.parametrize("x,expected", [(-0.1,0.0),(0,0),(0.5,0.5),(1,1),(1.1,1)])
def test_clip(x, expected):
assert clip(x) == expected
x
๋ฅผ [lo, hi]
๋ฒ์๋ก ํด๋ฆฌํํ๋ ํจ์๊ฐ ๊ฒฝ๊ณ ํฌํจ/์ด๊ณผ ์ผ์ด์ค์์ ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๋์ง ํ์ธํฉ๋๋ค.lo
, ์ํ ์ด๊ณผ โ hi
, ๋ฒ์ ๋ด โ x
๊ทธ๋๋ก ๋ฐํํด์ผ ํฉ๋๋ค.min(x, hi)
โ min(hi, x)
๋ก ๋ฐ๋๋ฉด ๋์์ ๊ฐ์ง๋ง, ๋ฒ์ ๋น๊ต ๋ก์ง์ ์๋ชป ๋ฐ๊พธ๋ค >=
/>
์ค์ ์ ํน์ ๊ฒฝ๊ณ๊ฐ ํ์ด์ ธ ์คํจํฉ๋๋ค.lo > hi
์ธ ์๋ชป๋ ์ค์ ์
๋ ฅ ์ ์์ธ๋ฅผ ๋์ง๋๋ก ์คํ์ ๋ช
ํํ ํ๊ณ ์์ธ ํ
์คํธ๋ฅผ ์ถ๊ฐํ์ธ์.import pytest
def safe_div(a, b):
if b == 0:
raise ValueError("b must be nonzero")
return a / b
def test_safe_div_raises():
with pytest.raises(ValueError, match="nonzero"):
safe_div(1, 0)
safe_div(1, 0)
ํธ์ถ ์ ValueError
๊ฐ ๋ฐ์ํ๊ณ ๋ฉ์์ง์ "nonzero"
๊ฐ ํฌํจ๋์ด์ผ ํฉ๋๋ค.ZeroDivisionError
) โ ํ์
๋ถ์ผ์น๋ก ์คํจ.match
์ ๊ท์ ๋ฏธ์ผ์น๋ก ์คํจ.b != 0
) ํ
์คํธ๋ ํจ๊ป ๋๊ณ , ์ ๊ฒฝ๋ก ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ํ๋ณดํ์ธ์.import pytest, csv
@pytest.fixture
def sample_csv(tmp_path):
p = tmp_path / "data.csv"
p.write_text("x,y\n1,2\n3,4\n", encoding="utf-8")
return p
def sum_csv(path):
with open(path) as f:
return sum(int(r["y"]) for r in csv.DictReader(f))
def test_sum_csv(sample_csv):
assert sum_csv(sample_csv) == 6
๋ฌด์์ ๊ฒ์ฆํ๋?
ํจ์ค ์กฐ๊ฑด
y
์ปฌ๋ผ ํฉ์ด ์ ํํ 6
์ด์ด์ผ ํฉ๋๋ค.์คํจ ์์
"y"
โ"Y"
)๋ก KeyError
๋ฐ์.ValueError
.์ ํ์ํ๊ฐ?
tmp_path
๋๋ถ์ ์ ์ญ ํ์ผ ์ถฉ๋ ์์ด ํ
์คํธ๊ฐ ๊ฒฉ๋ฆฌ๋ฉ๋๋ค.ํ์ฅ ์์ด๋์ด
yield
ํฝ์ค์ฒ๋ก ๋ฆฌ์์ค ํด์ (์: DB ์ปค๋ฅ์
) ํฌํจ.๊ทธ๋ฅ ํจ์์ ๋ฌด์์ด ๋ค๋ฅธ๊ฐ?
def sample_csv(): ...
๋ก ํจ์๋ฅผ ๋ง๋ค์ด ์ง์ ํธ์ถํ ์๋ ์์ง๋ง, fixture๋ pytest๊ฐ ์๋ ํธ์ถ๊ณผ ์ฃผ์
์ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ํ
์คํธ ๋ณธ๋ฌธ์ด โ๊ฒ์ฆ ๋ก์งโ์๋ง ์ง์คํ ์ ์์ต๋๋ค. scope
, yield
๋ฅผ ์ด์ฉํด ์์ ์๋ช
๊ด๋ฆฌ์ ๊ณต์ ๊ฐ ๊ฐ๋ฅํด ๋ฌด๊ฑฐ์ด ๋ฆฌ์์ค(DB ์ฐ๊ฒฐ, ํ์ผ ํธ๋ค ๋ฑ)์๋ ์ ํฉํฉ๋๋ค.import pytest, time
@pytest.mark.slow
def test_slow_op():
time.sleep(2)
assert 1 + 1 == 2
@pytest.mark.parametrize("a,b,expected", [(1,2,3),(2,3,5)])
def test_add(a,b,expected):
assert a + b == expected
๋ฌด์์ ๊ฒ์ฆํ๋?
slow
๋ง์ปค: ๋๋ฆฐ ํ
์คํธ๋ฅผ ์๋ณ/์ ํ ์คํํ ์ ์๊ฒ ํ๊น
ํฉ๋๋ค. ๊ธฐ๋ฅ ์์ฒด ๊ฒ์ฆ์ assert
๊ฐ ๋ด๋น.ํจ์ค ์กฐ๊ฑด
test_slow_op
: ์ ๊น ๊ธฐ๋ค๋ฆฐ ๋ค 1+1==2
๊ฐ ์ฐธ์ด๋ฉด ํต๊ณผ. CI์์๋ -m "not slow"
๋ก ์ ์ธํด๋ ์ ์ฒด๊ฐ ์ฑ๊ณตํด์ผ ํฉ๋๋ค.test_add
: ๊ฐ ํํ (a,b,expected)
๋ง๋ค a+b==expected
๊ฐ ๋ชจ๋ ์ฐธ์ด๋ฉด ํต๊ณผ.์คํจ ์์
--strict-markers
์ฌ์ฉ ์ ๊ฒฝ๊ณ /์๋ฌ.์ ํ์ํ๊ฐ?
ํ์ฅ ์์ด๋์ด
ids=
๋ฅผ ์ง์ ํด ์ผ์ด์ค ์ด๋ฆ์ ์๋ฏธ ์๊ฒ ๋ถ์ด๋ฉด ์คํจ ๋ฆฌํฌํธ ๊ฐ๋
์ฑ์ด ์ข์์ง๋๋ค.from unittest.mock import patch
@patch("requests.get")
def test_fetch_users(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = [{"id":1,"name":"Alice"}]
import requests
resp = requests.get("http://fake")
assert resp.json()[0]["name"] == "Alice"
๋ฌด์์ ๊ฒ์ฆํ๋?
requests.get
์ ํจ์นํด ๊ฐ์ง ์๋ต ๊ฐ์ฒด๋ฅผ ์ฃผ์
ํ๊ณ , .json()
๋ฐํ๊ฐ์ ๊ฒ์ฆํฉ๋๋ค.ํจ์ค ์กฐ๊ฑด
requests.get
์ด ํธ์ถ๋๋ฉด, status_code==200
์ด๊ณ json()
์ด [{"id":1,"name":"Alice"}]
๋ฅผ ๋ฐํํด์ผ ํฉ๋๋ค."Alice"
๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฝ์ผ๋ฉด ํต๊ณผํฉ๋๋ค.์คํจ ์์
@patch("my.module.requests.get")
vs @patch("requests.get")
) โ ์ค์ ๋คํธ์ํฌ ํธ์ถ ์๋/ํ์์์.status_code
๋ฅผ ๊ฒ์ฌํ๋ค ๋๋ฝ๋์ด์ผ ํ ์๋ฌ ๊ฒฝ๋ก๊ฐ ํต๊ณผํ๊ฑฐ๋, ๋ฐ๋๋ก ์ฑ๊ณต ๊ฒฝ๋ก์์ ์์ธ ๋ฐ์.์ ํ์ํ๊ฐ?
status_code=500
์ค์ ํ with pytest.raises(...)
๋ก ์์ธ๋ฅผ ์๊ตฌํ์ธ์.pytest-mock
์ mocker
ํฝ์ค์ฒ๋ฅผ ์ฐ๋ฉด assert_called_once_with
๋ฑ ํธ์ถ ๊ฒ์ฆ์ด ๊ฐ๊ฒฐํฉ๋๋ค.# app/db.py
import sqlite3
def init_db(path=":memory:"):
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
return conn
def add_user(conn, name):
conn.execute("INSERT INTO users(name) VALUES (?)", (name,))
conn.commit()
def list_users(conn):
return [r[0] for r in conn.execute("SELECT name FROM users")]
# tests/test_integration_db.py
import pytest
from app import db
@pytest.fixture
def conn():
conn = db.init_db() # ๊ฒฉ๋ฆฌ๋ in-memory DB
yield conn
conn.close()
def test_add_and_list_users(conn):
# Arrange + Act (์กฐํฉ๋ ํ๋ฆ)
db.add_user(conn, "Alice")
db.add_user(conn, "Bob")
# Assert
assert db.list_users(conn) == ["Alice", "Bob"]
๋ฌด์์ ๊ฒ์ฆํ๋?
ํจ์ค ์กฐ๊ฑด
["Alice", "Bob"]
์ด ์กฐํ๋๋ค.์คํจ ์์
OperationalError
commit()
๋๋ฝ์ผ๋ก ๋น ๊ฒฐ๊ณผProgrammingError
์ ํ์ํ๊ฐ?
# app/api.py
from fastapi import FastAPI, Depends
import sqlite3
app = FastAPI()
def get_conn():
# ์ค์ ํ๋ก๋์
์์ ํ/ํ์ผDB ์ฐ๊ฒฐ์ ๋ฐํ
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE IF NOT EXISTS items(id INTEGER PRIMARY KEY, name TEXT)")
return conn
@app.post("/items")
def create_item(name: str, conn: sqlite3.Connection = Depends(get_conn)):
conn.execute("INSERT INTO items(name) VALUES (?)", (name,))
conn.commit()
return {"ok": True}
@app.get("/items")
def list_items(conn: sqlite3.Connection = Depends(get_conn)):
return [{"name": r[0]} for r in conn.execute("SELECT name FROM items")]
# tests/test_integration_api.py
from fastapi.testclient import TestClient
from app.api import app, get_conn
import sqlite3
def test_items_flow_using_overridden_dep():
# ํ
์คํธ์ฉ ๋จ์ผ in-memory DB๋ฅผ ์์กด์ฑ์ผ๋ก ์ฃผ์
test_conn = sqlite3.connect(":memory:")
test_conn.execute("CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT)")
app.dependency_overrides[get_conn] = lambda: test_conn
client = TestClient(app)
# Act
r1 = client.post("/items", params={"name": "apple"})
r2 = client.post("/items", params={"name": "banana"})
r3 = client.get("/items")
# Assert
assert r1.status_code == 200 and r2.status_code == 200
assert r3.json() == [{"name": "apple"}, {"name": "banana"}]
# ์ ๋ฆฌ
app.dependency_overrides.clear()
test_conn.close()
๋ฌด์์ ๊ฒ์ฆํ๋?
์คํจ ์์
์ ํ์ํ๊ฐ?
# app/etl.py
import csv, json
from pathlib import Path
def run_etl(inp_csv: Path, out_json: Path):
total = 0
with inp_csv.open() as f:
for row in csv.DictReader(f):
total += int(row["value"])
out_json.write_text(json.dumps({"total": total}, ensure_ascii=False), encoding="utf-8")
# tests/test_integration_etl.py
from pathlib import Path
from app.etl import run_etl
import json
def test_etl_end_to_end(tmp_path):
# Arrange: ์
๋ ฅ CSV์ ์ถ๋ ฅ ๊ฒฝ๋ก ์ค๋น
inp = tmp_path / "in.csv"
out = tmp_path / "out.json"
inp.write_text("value\n1\n2\n3\n", encoding="utf-8")
# Act
run_etl(inp, out)
# Assert: ์ฐ์ถ๋ฌผ ์กด์ฌ + ๋ด์ฉ ๊ฒ์ฆ
assert out.exists()
data = json.loads(out.read_text(encoding="utf-8"))
assert data == {"total": 6}
{"total": 6}
.value
โvalues
)๋ก KeyError
ValueError
# app/mlpipe.py
import numpy as np
from sklearn.linear_model import LinearRegression
def preprocess(xs):
# ์์ ์ ๊ฑฐ + ์ ๊ทํ(๊ฐ๋จ ์์)
arr = np.array([x for x in xs if x is not None and x >= 0], dtype=float)
if arr.size == 0:
return arr
return (arr - arr.min()) / (arr.max() - arr.min() + 1e-12)
def fit_predict(xs, ys, xs_new):
X = preprocess(xs).reshape(-1, 1)
y = np.array(ys, dtype=float)
model = LinearRegression().fit(X, y)
preds = model.predict(preprocess(xs_new).reshape(-1, 1))
return preds
# tests/test_integration_mlpipe.py
import numpy as np
from app.mlpipe import fit_predict
def test_fit_predict_end_to_end():
# Arrange
xs = [0, 1, 2, 3, None, -1] # None/-1๋ ์ ์ฒ๋ฆฌ์์ ์ ๊ฑฐ
ys = [0, 10, 20, 30] # ์ ํ ๊ด๊ณ
xs_new = [0, 1, 3]
# Act
preds = fit_predict(xs, ys, xs_new)
# Assert: ์ถ๋ ฅ ๊ธธ์ด/๋ฒ์/๋จ์กฐ์ฑ ๊ฐ์ "๊ณ์ฝ" ๊ฒ์ฆ
assert preds.shape == (3,)
assert np.all(np.isfinite(preds))
assert np.all(np.diff(preds) >= -1e-9) # ๋จ์กฐ ์ฆ๊ฐ(์์น ์ค์ฐจ ํ์ฉ)
# ๋๋ต์ ๊ฐ ๊ฒ์ฆ(์์ ๋์ผ ์๋)
assert preds[0] < preds[1] < preds[2]
ํจ์ค ์กฐ๊ฑด
NaN/inf
๊ฐ ์์ผ๋ฉฐ, ๋จ์กฐ ์ฆ๊ฐ๊ฐ ์ ์ง๋๋ค.์คํจ ์์
None
/์์ ํํฐ๋ง ๋ฏธ์๋ โ ํ์ต/์์ธก ์คํจ ๋๋ NaNValueError
์ ํ์ํ๊ฐ?
์์ ์ด์ ํ
- ํตํฉ ํ ์คํธ๋ ๋๋ฆด ์ ์์ โ ๋ง์ปค๋ฅผ ๋ถ์ฌ ์ ํ ์คํ:
@pytest.mark.integration
- ๊ณต์ฉ ๋ฆฌ์์ค(DBยท๋ธ๋ก์ปค ๋ฑ)๋ ํฝ์ค์ฒ ์ค์ฝํ(scope="session")๋ก ๋น์ฉ ์ต์ํ
- โ๋จ์(๋ง์ด) > ํตํฉ(์ ๋นํ) > E2E(์์)โ ํ ์คํธ ํผ๋ผ๋ฏธ๋ ์ ์ง๋ก ํผ๋๋ฐฑ ์๋ ํ๋ณด
pytest-benchmark
): ํน์ ํจ์ ์ฑ๋ฅ ์ถ์ pytest-asyncio
): ๋น๋๊ธฐ ํจ์ ๊ฒ์ฆ๊ฐ์์ ๋ ํนํ ํฌ์ธํธ๋ ChatGPT ํ์ฉ์ ๋๋ค.
pytest๋
์ด ์ธ ๊ฐ์ง๊ฐ ํฉ์ณ์ง๋ฉด์ โํ
์คํธ๊ฐ ๊ท์ฐฎ์ ๋ถ๊ฐ ์์
โ์ด ์๋๋ผ,
โ์ฝ๋ ํ์ง์ ๋์ด๊ณ ๊ฐ๋ฐ์ ๋น ๋ฅด๊ฒ ํ๋ ๋๊ตฌโ์์ ์ฒด๊ฐํ๊ฒ ๋ฉ๋๋ค.