๐งช ๋ด๊ฐ ๋ณด๋ ค๊ณ ์์ฑํ pytest ๊ฐ์ด๋
์๋ณธ ๊ฒ์๊ธ: https://velog.io/@euisuk-chung/๋ด๊ฐ-๋ณด๋ ค๊ณ -์์ฑํ-pytest-๊ฐ์ด๋
๋ณธ ํฌ์คํ ์ ์๋ ์๋ฃ๋ค์ ์ฐธ๊ณ ํด์ ์์ฑ๋์์ต๋๋ค.
TL;DR ๐
- 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)์ ๋๋ค.:
- Arrange: ์ค๋น โ ์ ๋ ฅ, ํ๊ฒฝ, ๋ฐ์ดํฐ ์ค๋น
- Act: ์คํ โ ํจ์๋ ๋ฉ์๋ ํธ์ถ
- Assert: ๊ฒ์ฆ โ ๊ธฐ๋ํ ์ถ๋ ฅ๊ณผ ๋น๊ต
-
pytest๋ ๋ฌด์์ธ๊ฐ?
pytest๋ ํ์ด์ฌ์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ํ ์คํธ ํ๋ ์์ํฌ์ ๋๋ค.
์ฃผ์ ํน์ง
- Autodiscovery:
test_*.py
๋๋*_test.py
ํ์์ ํ์ผ,test_*
ํจ์๋ช ์ ์๋์ผ๋ก ํ์ง - Rich Assertion Introspection:
assert
์คํจ ์ ์ค์ ๊ฐ๊ณผ ๊ธฐ๋ ๊ฐ์ ์ง๊ด์ ์ผ๋ก ์ถ๋ ฅ - Fixture ์์คํ : ํ ์คํธ ์ค๋น/์ ๋ฆฌ๋ฅผ ์ ์ฐํ๊ฒ ๊ด๋ฆฌ
- ํ๋ผ๋ฏธํฐํ ์ง์: ํ๋์ ํ ์คํธ๋ฅผ ์ฌ๋ฌ ์ผ์ด์ค๋ก ๋ฐ๋ณต ์คํ
- ํธํ์ฑ: unittest, nose ๊ธฐ๋ฐ ํ ์คํธ๋ ์คํ ๊ฐ๋ฅ
- ํ์ฅ์ฑ: Django, Flask, Pandas ๋ฑ ๊ฐ์ข ํ๋ฌ๊ทธ์ธ ํ์ฉ ๊ฐ๋ฅ
-
๊ฐ๋ฐํ๊ฒฝ๊ณผ ์ค์ฉ ํฌ์ธํธ
- IDE ํตํฉ: Pycharm, VSCode์์ ํ ์คํธ ํจ์ ์์ ์คํ ๋ฒํผ ํ์ โ CLI ์์ด๋ ํด๋ฆญ ์คํ
- ๋ณด๊ณ ์ ๊ฐ๋
์ฑ: ์คํจ ์ ์ด๋ ๊ฐ์ด ๋ฌ๋๋์ง ์์ธํ ์ถ๋ ฅ โ
assertEqual
๊ฐ์ ๋ฉ์๋๋ณด๋ค ์ง๊ด์ - ๋ง์ปค(Markers):
@pytest.mark.slow
,skip
,xfail
๋ก ์คํ/์ ์ธ ์ ์ด ๋ฐ ๋ฆฌํฌํธ์ ํ๊ทธ ํ์ - AI ๋ณด์กฐ: ChatGPT ๋ฑ์ ํ์ฉํด ํ ์คํธ ์ค์บํด๋ ์๋ ์์ฑ โ ๋๋ฝ๋ ์ผ์ด์ค ์ ๊ฒ์ ์ ์ฉ
-
ํต์ฌ ํ ์คํธ ์ ํ๊ณผ ์์
4.0 ํ ์คํธ ์คํ ๋ฐ ํด์ ๊ฐ์ด๋
๊ทธ๋์ pytest๋ ์ด๋ป๊ฒ ์คํํ๋๊ฐ!?
4.0.1 ๊ธฐ๋ณธ ์คํ ๋ช ๋ น์ด
1
2
# ํ๋ก์ ํธ ์ ์ฒด ํ
์คํธ ์คํ
pytest
1
2
# ํน์ ํ์ผ์ ํ
์คํธ๋ง ์คํ
pytest tests/test_unit.py
1
2
# ํน์ ํจ์๋ง ์คํ
pytest tests/test_unit.py::test_normalize_whitespace
1
2
# ์์ธํ ์ถ๋ ฅ์ผ๋ก ์คํ (๊ฐ ํ
์คํธ ์ด๋ฆ๊ณผ ๊ฒฐ๊ณผ ํ์)
pytest -v
1
2
# ์คํจ ์ ์ฆ์ ์ค๋จ
pytest -x
1
2
# print๋ฌธ ์ถ๋ ฅ ๋ณด๊ธฐ (๋๋ฒ๊น
์ฉ)
pytest -s
4.0.2 ๋ง์ปค๋ฅผ ํ์ฉํ ์ ํ์ ์คํ
1
2
# ๋๋ฆฐ ํ
์คํธ ์ ์ธํ๊ณ ๋น ๋ฅธ ํ
์คํธ๋ง
pytest -m "not slow"
1
2
# ํตํฉ ํ
์คํธ๋ง ์คํ
pytest -m integration
1
2
# ์ธ๋ถ ์์กด์ฑ์ด ์๋ ํ
์คํธ๋ง
pytest -m "not external"
1
2
# ์ฌ๋ฌ ์กฐ๊ฑด ์กฐํฉ
pytest -m "not slow and not integration"
4.0.3 ์ปค๋ฒ๋ฆฌ์ง ์ธก์
1
2
3
4
5
6
7
8
# ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง์ ํจ๊ป ์คํ
pytest --cov=app
# HTML ๋ฆฌํฌํธ ์์ฑ
pytest --cov=app --cov-report=html
# ์ต์ ์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ ์ค์ (80% ๋ฏธ๋ง์ ์คํจ)
pytest --cov=app --cov-fail-under=80
4.0.4 ์คํ ๊ฒฐ๊ณผ ํด์
โ ์ฑ๊ณต์ ์ธ ์คํ ์์:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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์ด ๋ง์ ์๋ฃ
โ ์คํจ๊ฐ ์๋ ๊ฒฝ์ฐ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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
๊ฐ ๋ฐ์ํ ๊ฒ์ผ๋ก ์์ํ์ง๋ง ๋ฐ์ํ์ง ์์
4.0.5 ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ ์ต์ ํ
๋น ๋ฅธ ํผ๋๋ฐฑ์ ์ํ ์คํ ์ ๋ต:
1
2
3
4
5
6
7
8
9
# ๊ฐ๋ฐ ์ค: ๋น ๋ฅธ ์ ๋ ํ
์คํธ๋ง
pytest -m "not slow and not integration" --ff
# ์ปค๋ฐ ์ : ๋ณ๊ฒฝ๋ ํ์ผ ๊ด๋ จ ํ
์คํธ
pytest --lf # ๋ง์ง๋ง ์คํจ ํ
์คํธ๋ง
pytest --ff # ์คํจํ๋ ํ
์คํธ ์ฐ์ ์คํ
# CI/๋ฐฐํฌ ์ : ์ ์ฒด ํ
์คํธ + ์ปค๋ฒ๋ฆฌ์ง
pytest --cov=app --cov-fail-under=80
๋๋ฒ๊น ์ ์ํ ์ต์ :
1
2
3
4
5
6
7
8
9
10
11
# ์ฒซ ์คํจ์์ ์ค๋จํ๊ณ ๋๋ฒ๊ฑฐ ์ง์
pytest --pdb -x
# ๋ ์์ธํ ์ถ๋ ฅ
pytest -vv
# ๊ฒฝ๊ณ ๋ฉ์์ง ์จ๊ธฐ๊ธฐ
pytest --disable-warnings
# ํน์ ๋ก๊ทธ ๋ ๋ฒจ ์ค์
pytest --log-cli-level=DEBUG
4.0.6 ํ ์คํธ ๊ตฌ์ฑ ํ์ผ ์ค์
pytest.ini ์ค์ ์์:
1
2
3
4
5
6
7
8
9
[tool:pytest]
minversion = 6.0
addopts = -ra -q --strict-markers --strict-config
testpaths = tests
markers =
slow: ์คํ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ํ
์คํธ
integration: ์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ๊ฒฐํฉ๋ ํ
์คํธ
external: ์ธ๋ถ ์๋น์ค์ ์์กดํ๋ ํ
์คํธ
unit: ๋จ์ผ ํจ์/ํด๋์ค๋ง ํ
์คํธํ๋ ์ ๋ ํ
์คํธ
์คํ ์๋๋ฆฌ์ค๋ณ ๋ช ๋ น์ด ์ ๋ฆฌ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ๐ ๊ฐ๋ฐ ์ค (๋น ๋ฅธ ํผ๋๋ฐฑ)
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
์ด์ ํ ์คํธ ์คํ์ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ์ผ๋, ๊ตฌ์ฒด์ ์ธ ํ ์คํธ ์ ํ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
4.1 ์ ๋ ํ ์คํธ (Unit Test)
1
2
3
4
5
6
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"
-
๋ฌด์์ ๊ฒ์ฆํ๋?
- ๊ณต๋ฐฑ ์ ๊ทํ ํจ์๊ฐ
- โ
์ฌ๋ฌ ๊ฐ์ ๊ณต๋ฐฑ/๊ฐํ/ํญ
โ๋จ์ผ ๊ณต๋ฐฑ
โ, โ์๋ค ๊ณต๋ฐฑ ์ ๊ฑฐ
โ
- โ
- ์ด๋ผ๋ ๊ณ์ฝ(contract)์ ์งํค๋์ง ํ์ธํฉ๋๋ค.
- ๊ณต๋ฐฑ ์ ๊ทํ ํจ์๊ฐ
-
ํจ์ค ์กฐ๊ฑด
- ์
๋ ฅ
" Hello World "
๊ฐ ์ ํํ"Hello World"
๋ก ๋ณํ๋๋ฉด ํต๊ณผํฉ๋๋ค.
- ์
๋ ฅ
-
์คํจ ์์
strip()
์ ๋๋ฝํ๋ฉด ๊ฒฐ๊ณผ๊ฐ"Hello World"
์ฒ๋ผ ์๋ค ๊ณต๋ฐฑ์ด ๋จ์ ์คํจํฉ๋๋ค.\s+
๋์" "
๋ง ์นํํ๋ฉด ํญ/๊ฐํ(\n
,\t
)์ด ํ ์นธ์ผ๋ก ํฉ์ณ์ง์ง ์์ ์คํจํฉ๋๋ค.
-
์ ํ์ํ๊ฐ?
- ์ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ์์ ๋ฌธ์์ด ๊ท์น์ด ๊นจ์ง๋ฉด ํ์ ํ ํฐํ/์ ๊ท์ ๋งค์นญ์ด ํ๋ค๋ฆฝ๋๋ค. ์ด ํ ์คํธ๋ ์์ ๋ก์ง์ ์ ํ์ฑ์ ๊ณ ์ ํฉ๋๋ค.
-
ํ์ฅ ์์ด๋์ด
- ํ๋ผ๋ฏธํฐํ๋ก ํญ/๊ฐํ/๋ค๊ตญ์ด ๊ณต๋ฐฑ(์:
\u00A0
)๋ ์ผ์ด์คํํ์ธ์.
- ํ๋ผ๋ฏธํฐํ๋ก ํญ/๊ฐํ/๋ค๊ตญ์ด ๊ณต๋ฐฑ(์:
4.2 ๊ฒฝ๊ณ๊ฐ ํ ์คํธ (Boundary)
1
2
3
4
5
6
7
8
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
์ธ ์๋ชป๋ ์ค์ ์ ๋ ฅ ์ ์์ธ๋ฅผ ๋์ง๋๋ก ์คํ์ ๋ช ํํ ํ๊ณ ์์ธ ํ ์คํธ๋ฅผ ์ถ๊ฐํ์ธ์.
4.3 ์์ธ ์ฒ๋ฆฌ
1
2
3
4
5
6
7
8
9
10
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)
- ๋ฌด์์ ๊ฒ์ฆํ๋?
- ์๋ชป๋ ์ ๋ ฅ(0์ผ๋ก ๋๋)์์ ๋ช ์์ ์ธ ์์ธ ํ์ ยท๋ฉ์์ง๋ฅผ ๋ฐ์์ํค๋์ง ํ์ธํฉ๋๋ค. ์คํจ๋ ์คํ์ ๋๋ค.
- ํจ์ค ์กฐ๊ฑด
safe_div(1, 0)
ํธ์ถ ์ValueError
๊ฐ ๋ฐ์ํ๊ณ ๋ฉ์์ง์"nonzero"
๊ฐ ํฌํจ๋์ด์ผ ํฉ๋๋ค.
- ์คํจ ์์
- ์์ธ๋ฅผ ๋์ง์ง ์์ โ ZeroDivisionError๊ฐ ๋์ค์ ๋ฐ์ํ๊ฑฐ๋ ํ ์คํธ๊ฐ ๊ณ์ ์งํ๋์ด ์คํจ.
- ๋ค๋ฅธ ํ์
์ ์์ธ๋ฅผ ๋์ง(
ZeroDivisionError
) โ ํ์ ๋ถ์ผ์น๋ก ์คํจ. - ๋ฉ์์ง๊ฐ ๋ฐ๋ โ
match
์ ๊ท์ ๋ฏธ์ผ์น๋ก ์คํจ.
- ์ ํ์ํ๊ฐ?
- ์๋ฌ ํธ๋ค๋ง ๊ท์ฝ์ ๋ฌธ์ํ/๊ณ ์ ํฉ๋๋ค. API ์ฌ์ฉ์(๋๋ฃ ์ฝ๋)์๊ฒ ์์ธก ๊ฐ๋ฅํ ์คํจ ์๊ทธ๋์ ์ ๊ณตํฉ๋๋ค.
- ํ์ฅ ์์ด๋์ด
- ์์ธ ๊ฒฝ๋ก ์ธ์ ์ ์ ๊ฒฝ๋ก(
b != 0
) ํ ์คํธ๋ ํจ๊ป ๋๊ณ , ์ ๊ฒฝ๋ก ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ํ๋ณดํ์ธ์.
- ์์ธ ๊ฒฝ๋ก ์ธ์ ์ ์ ๊ฒฝ๋ก(
4.4 ํฝ์ค์ฒ (Fixture)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
-
๋ฌด์์ ๊ฒ์ฆํ๋?
- ํ์ผ I/O์ ์์กดํ๋ ํจ์๊ฐ ์ฃผ์ด์ง CSV ์คํค๋ง/๋ด์ฉ์์ ์ฌ๋ฐ๋ฅธ ํฉ๊ณ๋ฅผ ๊ณ์ฐํ๋์ง ํ์ธํฉ๋๋ค.
-
ํจ์ค ์กฐ๊ฑด
- ํฝ์ค์ฒ๊ฐ ๋ง๋ ์์ CSV์์
y
์ปฌ๋ผ ํฉ์ด ์ ํํ6
์ด์ด์ผ ํฉ๋๋ค.
- ํฝ์ค์ฒ๊ฐ ๋ง๋ ์์ CSV์์
-
์คํจ ์์
- ์ธ์ฝ๋ฉ/๊ฐํ ์ฒ๋ฆฌ ์ค๋ฅ๋ก ํ์ฑ ์คํจ.
- ํค๋ ์คํ(
"y"
โ"Y"
)๋กKeyError
๋ฐ์. - ์ ์ ๋ณํ ์คํจ(๊ณต๋ฐฑ/NA)๋ก
ValueError
.
-
์ ํ์ํ๊ฐ?
- ํฝ์ค์ฒ๋ ์ค๋น/์ ๋ฆฌ ๋ก์ง์ ์ฌ์ฌ์ฉ๊ณผ ๋
๋ฆฝ ์คํ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
tmp_path
๋๋ถ์ ์ ์ญ ํ์ผ ์ถฉ๋ ์์ด ํ ์คํธ๊ฐ ๊ฒฉ๋ฆฌ๋ฉ๋๋ค.
- ํฝ์ค์ฒ๋ ์ค๋น/์ ๋ฆฌ ๋ก์ง์ ์ฌ์ฌ์ฉ๊ณผ ๋
๋ฆฝ ์คํ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
-
ํ์ฅ ์์ด๋์ด
yield
ํฝ์ค์ฒ๋ก ๋ฆฌ์์ค ํด์ (์: DB ์ปค๋ฅ์ ) ํฌํจ.- ๊ฒฐ์ธก/์๋ชป๋ ํ์ด ์๋ CSV๋ฅผ ์ถ๊ฐ ์ผ์ด์ค๋ก ๋ง๋ค์ด ๊ฐ๊ฑด์ฑ ํ ์คํธ๋ฅผ ํ์ฅํ์ธ์.
-
๊ทธ๋ฅ ํจ์์ ๋ฌด์์ด ๋ค๋ฅธ๊ฐ?
- ๋จ์ํ
def sample_csv(): ...
๋ก ํจ์๋ฅผ ๋ง๋ค์ด ์ง์ ํธ์ถํ ์๋ ์์ง๋ง, fixture๋ pytest๊ฐ ์๋ ํธ์ถ๊ณผ ์ฃผ์ ์ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ํ ์คํธ ๋ณธ๋ฌธ์ด โ๊ฒ์ฆ ๋ก์งโ์๋ง ์ง์คํ ์ ์์ต๋๋ค. - ๋ํ
scope
,yield
๋ฅผ ์ด์ฉํด ์์ ์๋ช ๊ด๋ฆฌ์ ๊ณต์ ๊ฐ ๊ฐ๋ฅํด ๋ฌด๊ฑฐ์ด ๋ฆฌ์์ค(DB ์ฐ๊ฒฐ, ํ์ผ ํธ๋ค ๋ฑ)์๋ ์ ํฉํฉ๋๋ค.
- ๋จ์ํ
4.5 ๋ง์ปค์ ํ๋ผ๋ฏธํฐํ
1
2
3
4
5
6
7
8
9
10
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=
๋ฅผ ์ง์ ํด ์ผ์ด์ค ์ด๋ฆ์ ์๋ฏธ ์๊ฒ ๋ถ์ด๋ฉด ์คํจ ๋ฆฌํฌํธ ๊ฐ๋ ์ฑ์ด ์ข์์ง๋๋ค.
4.6 ๋ชจํน (Mocking)
1
2
3
4
5
6
7
8
9
10
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"
-
๋ฌด์์ ๊ฒ์ฆํ๋?
- ์ธ๋ถ API ํธ์ถ์ ์์กดํ๋ ์ฝ๋๊ฐ, ๋คํธ์ํฌ์ ์ค์ ๋ก ์ ์ํ์ง ์๊ณ ๋ ์์๋ ์๋ต ํํ๋ฅผ ์ฒ๋ฆฌํ ์ ์๋์ง ํ์ธํฉ๋๋ค.
- ์ฌ๊ธฐ์๋
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
๋ฅผ ๊ฒ์ฌํ๋ค ๋๋ฝ๋์ด์ผ ํ ์๋ฌ ๊ฒฝ๋ก๊ฐ ํต๊ณผํ๊ฑฐ๋, ๋ฐ๋๋ก ์ฑ๊ณต ๊ฒฝ๋ก์์ ์์ธ ๋ฐ์.
- ํจ์น ๋์ ๊ฒฝ๋ก ์ค๊ธฐ (
-
์ ํ์ํ๊ฐ?
- ์ธ๋ถ ์์คํ (API/DB/ํ์ผ์์คํ /์๊ฐ)์ ๋๋ฆฌ๊ณ ๋ถ์์ ํฉ๋๋ค. ๋ชจํน์ผ๋ก ์์ ๋ก์ง๋ง ๊ฒฉ๋ฆฌํด ๋น ๋ฅด๊ณ ๊ฒฐ์ ์ ์ธ ํ ์คํธ๋ฅผ ๋ง๋ญ๋๋ค.
-
ํ์ฅ ์์ด๋์ด
- ์คํจ ๊ฒฝ๋ก ํ
์คํธ:
status_code=500
์ค์ ํwith pytest.raises(...)
๋ก ์์ธ๋ฅผ ์๊ตฌํ์ธ์. pytest-mock
์mocker
ํฝ์ค์ฒ๋ฅผ ์ฐ๋ฉดassert_called_once_with
๋ฑ ํธ์ถ ๊ฒ์ฆ์ด ๊ฐ๊ฒฐํฉ๋๋ค.
- ์คํจ ๊ฒฝ๋ก ํ
์คํธ:
4.7 ํตํฉ ํ ์คํธ: In-Memory DB + ์๋น์ค ๊ณ์ธต
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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")]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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"]
-
๋ฌด์์ ๊ฒ์ฆํ๋?
- DB ์ด๊ธฐํ โ INSERT โ SELECT๊น์ง ๋ชจ๋ ๊ฐ ์ฐ๊ฒฐ๋ ์๋๋ฆฌ์ค๊ฐ ์ผ๊ด๋๊ฒ ๋์ํ๋์ง.
-
ํจ์ค ์กฐ๊ฑด
- ์ถ๊ฐํ ์์๋๋ก
["Alice", "Bob"]
์ด ์กฐํ๋๋ค.
- ์ถ๊ฐํ ์์๋๋ก
-
์คํจ ์์
- ํ
์ด๋ธ ์คํค๋ง ๋ถ์ผ์น๋ก
OperationalError
commit()
๋๋ฝ์ผ๋ก ๋น ๊ฒฐ๊ณผ- ์ปค๋ฅ์
๊ณต์ /ํด์ ์ค์๋ก
ProgrammingError
- ํ
์ด๋ธ ์คํค๋ง ๋ถ์ผ์น๋ก
-
์ ํ์ํ๊ฐ?
- ๋จ์ ํ ์คํธ๋ ํจ์ ํ๋๋ง ๋ณธ๋ค.
- ํตํฉ ํ ์คํธ๋ ์ํ ๋ณํ๊ฐ ์ด์ด์ง๋ ํ๋ฆ์ ๊ฒ์ฆํด โ๋ถ์์ ๋ ๊นจ์ง๋โ ๋ฒ๊ทธ๋ฅผ ์ก๋๋ค.
4.8 ํตํฉ ํ ์คํธ: FastAPI + DB ์์กด์ฑ ์ฃผ์
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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")]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 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()
-
๋ฌด์์ ๊ฒ์ฆํ๋?
- HTTP ๊ณ์ธต(๋ผ์ฐํ /์ง๋ ฌํ) + DB ๊ณ์ธต(์ฝ์ /์กฐํ)์ด ํ๋์ ํ๋ก์ฐ๋ก ๊ฒฐํฉ๋์ด ์ ์ ๋์ํ๋์ง.
-
- ํจ์ค ์กฐ๊ฑด
- POST 2ํ๊ฐ 200์ ๋ฐํํ๊ณ , GET์ด ์ฝ์ ๋ ์์ดํ ๋ชฉ๋ก์ ๊ทธ๋๋ก ๋ฐํํ๋ค.
-
์คํจ ์์
- ์์กด์ฑ ์ค๋ฒ๋ผ์ด๋ ๋๋ฝ โ ๊ฐ ์์ฒญ๋ง๋ค ๋ค๋ฅธ ์ DB๋ก ์ฐ๊ฒฐ๋์ด ๋น ๊ฒฐ๊ณผ
- ํธ๋์ญ์ /์ปค๋ฐ ๋๋ฝ โ ์กฐํ ์ ๋น ๋ชฉ๋ก
- ๋ผ์ฐํ ํ๋ผ๋ฏธํฐ ์ด๋ฆ/ํ์ ๋ถ์ผ์น โ 422/500
-
์ ํ์ํ๊ฐ?
- ์ค์ ํ๊ฒฝ์ ๊ฐ๊น์ด APIโDB ๊ฒฐํฉ ๋์์ ๋น ๋ฅด๊ฒ ๊ฒ์ฆํ๋ฉด์๋ ๋คํธ์ํฌ/์คDB ์์ด ๊ฒฉ๋ฆฌ ๊ฐ๋ฅํ ์ฌํ์ฑ์ ํ๋ณดํ๋ค.
4.9 ํตํฉ ํ ์คํธ: ํ์ผ I/O + ํ์ + ์ง๊ณ(ETL ๋ฏธ๋ ํ์ดํ๋ผ์ธ)
1
2
3
4
5
6
7
8
9
10
# 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")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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}
- ๋ฌด์์ ๊ฒ์ฆํ๋?
- ํ์ผ ์์คํ โ CSV ํ์ฑ โ ์ง๊ณ โ JSON ์ฐ์ถ๊น์ง ๋๋จ ๊ฐ ๋ฐ์ดํฐ ํ๋ฆ์ด ๊นจ๋ํ๊ฒ ์ด์ด์ง๋์ง.
- ํจ์ค ์กฐ๊ฑด
- ์ถ๋ ฅ ํ์ผ์ด ์์ฑ๋๊ณ JSON ๋ด์ฉ์ด
{"total": 6}
.
- ์ถ๋ ฅ ํ์ผ์ด ์์ฑ๋๊ณ JSON ๋ด์ฉ์ด
- ์คํจ ์์
- ํค๋ ์คํ(
value
โvalues
)๋กKeyError
- ๋น์ ์ ๊ฐ(๊ณต๋ฐฑ/NA)๋ก
ValueError
- ๊ฒฝ๋ก/์ธ์ฝ๋ฉ ๋ฌธ์ ๋ก ํ์ผ ์ฝ๊ธฐ/์ฐ๊ธฐ ์คํจ
- ํค๋ ์คํ(
- ์ ํ์ํ๊ฐ?
- ํ์ค ์ ๋ฌด์ ๋ง์ ์ฝ๋๋ I/O + ๋ณํ + ์ฐ์ถ๋ฌผ๋ก ์ด๋ฃจ์ด์ง๋ค.
- ํตํฉ ํ ์คํธ๊ฐ ์ฐ์ถ๋ฌผ์ ์กด์ฌ/ํ์/๋ด์ฉ์ ๋ณด์ฆํด ํ๊ท๋ฅผ ๋ง๋๋ค.
4.10 ํตํฉ ํ ์คํธ: ์ ์ฒ๋ฆฌ โ ๋ชจ๋ธ ํ์ต/์์ธก(ML ๋ฏธ๋ ํ์ดํ๋ผ์ธ)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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]
- ๋ฌด์์ ๊ฒ์ฆํ๋?
- ์ ์ฒ๋ฆฌ โ ํ์ต โ ์์ธก๊น์ง ML ํ์ดํ๋ผ์ธ์ ๊ณ์ฝ(invariants): ๋ชจ์, ์ ํ์ฑ, ๋จ์กฐ์ฑ ๋ฑ.
-
ํจ์ค ์กฐ๊ฑด
- ์์ธก ๋ฐฐ์ด ๊ธธ์ด๊ฐ ์
๋ ฅ ๊ธธ์ด์ ๋ง๊ณ ,
NaN/inf
๊ฐ ์์ผ๋ฉฐ, ๋จ์กฐ ์ฆ๊ฐ๊ฐ ์ ์ง๋๋ค. - ๋ฐ์ดํฐ ๋ ธ์ด์ฆยท์ค์ผ์ผ๋ง ์ฐจ์ด์๋ ์ฑ์ง ๊ธฐ๋ฐ(assertions on properties)์ด ํต๊ณผํ๋ค.
- ์์ธก ๋ฐฐ์ด ๊ธธ์ด๊ฐ ์
๋ ฅ ๊ธธ์ด์ ๋ง๊ณ ,
-
์คํจ ์์
- ์ ์ฒ๋ฆฌ์์
None
/์์ ํํฐ๋ง ๋ฏธ์๋ โ ํ์ต/์์ธก ์คํจ ๋๋ NaN - ๋ฆฌ์ํ/๋ฆฌ์์ดํ ์ค๋ฅ๋ก
ValueError
- ๋ชจ๋ธ์ด ๊ณผ์ ํฉ/ํ์ต ์คํจ๋ก ๋จ์กฐ์ฑ ๋ถ๊ดด
- ์ ์ฒ๋ฆฌ์์
-
์ ํ์ํ๊ฐ?
- ์์น ํ ์คํธ๋ ์ ๋ต ์ซ์๊ฐ ๊ณ ์ ๋๊ธฐ ์ด๋ ต๋ค.
- ๋์ ํํยท๋ฒ์ยท์ฑ์ง์ ํตํฉ ํ ์คํธ๋ก ๊ณ ์ ํ๋ฉด ๋ณ๊ฒฝ์ ๊ฐํ๋ฉด์ ํ๊ท๋ฅผ ์ก๋๋ค.
์์ ์ด์ ํ
- ํตํฉ ํ ์คํธ๋ ๋๋ฆด ์ ์์ โ ๋ง์ปค๋ฅผ ๋ถ์ฌ ์ ํ ์คํ:
@pytest.mark.integration
- ๊ณต์ฉ ๋ฆฌ์์ค(DBยท๋ธ๋ก์ปค ๋ฑ)๋ ํฝ์ค์ฒ ์ค์ฝํ(scope=โsessionโ)๋ก ๋น์ฉ ์ต์ํ
- โ๋จ์(๋ง์ด) > ํตํฉ(์ ๋นํ) > E2E(์์)โ ํ ์คํธ ํผ๋ผ๋ฏธ๋ ์ ์ง๋ก ํผ๋๋ฐฑ ์๋ ํ๋ณด
-
๊ณ ๊ธ ํจํด
- Property-based Testing (hypothesis): ๋ฌด์์ ์ ๋ ฅ์ผ๋ก ํญ์ ์ฑ์ง์ด ์ ์ง๋๋์ง ํ์ธ
- Regression Test: ๊ณผ๊ฑฐ ๋ฒ๊ทธ ์ผ์ด์ค๋ฅผ ๊ณ ์ ํ ์คํธ๋ก ๋จ๊ฒจ ์ฌ๋ฐ ๋ฐฉ์ง
- Performance Test (
pytest-benchmark
): ํน์ ํจ์ ์ฑ๋ฅ ์ถ์ - Async Test (
pytest-asyncio
): ๋น๋๊ธฐ ํจ์ ๊ฒ์ฆ
-
AI๋ฅผ ํ์ฉํ ํ ์คํธ ์๋ํ
๊ฐ์์ ๋ ํนํ ํฌ์ธํธ๋ ChatGPT ํ์ฉ์ ๋๋ค.
- ํจ์/ํด๋์ค๋ฅผ ๋ถ์ฌ๋ฃ๊ณ โ โpytest fixture/parameterize/raises๋ฅผ ํ์ฉํด ํ ์คํธ ์ฝ๋ ์์ฑํด์คโ ์์ฒญ
- AI๊ฐ ๊ณจ๊ฒฉ์ ์๋ ์์ฑ, ๋๋ฝ๋ ์ผ์ด์ค๊น์ง ์ ์
- ๊ฐ๋ฐ์๋ ์ด๋ฅผ ๊ฒ์ฆยท๋ณด์ํ๋ ๋ฐฉ์์ผ๋ก ํจ์จ์ ํฌ๊ฒ ๋์ผ ์ ์์
-
์ ๋ฆฌ
pytest๋
- ์ง๊ด์ ๋ฌธ๋ฒ (assert) + ํ๋ถํ ๋ฆฌํฌํธ
- Fixture/Marker/Parametrize๋ก ๊ฐ๋ ฅํ ํ์ฅ์ฑ
- IDE ํตํฉ, ํธํ์ฑ, AI ๋ณด์กฐ
์ด ์ธ ๊ฐ์ง๊ฐ ํฉ์ณ์ง๋ฉด์ โํ
์คํธ๊ฐ ๊ท์ฐฎ์ ๋ถ๊ฐ ์์
โ์ด ์๋๋ผ,
โ์ฝ๋ ํ์ง์ ๋์ด๊ณ ๊ฐ๋ฐ์ ๋น ๋ฅด๊ฒ ํ๋ ๋๊ตฌโ์์ ์ฒด๊ฐํ๊ฒ ๋ฉ๋๋ค.