๐Ÿงช ๋‚ด๊ฐ€ ๋ณด๋ ค๊ณ  ์ž‘์„ฑํ•œ pytest ๊ฐ€์ด๋“œ

Posted by Euisuk's Dev Log on August 26, 2025

๐Ÿงช ๋‚ด๊ฐ€ ๋ณด๋ ค๊ณ  ์ž‘์„ฑํ•œ pytest ๊ฐ€์ด๋“œ

์›๋ณธ ๊ฒŒ์‹œ๊ธ€: https://velog.io/@euisuk-chung/๋‚ด๊ฐ€-๋ณด๋ ค๊ณ -์ž‘์„ฑํ•œ-pytest-๊ฐ€์ด๋“œ

https://youtu.be/cHYq1MRoyI0

๋ณธ ํฌ์ŠคํŒ…์€ ์•„๋ž˜ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•ด์„œ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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 ์Šคํƒ€์ผ๋กœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•ด์ค˜โ€
      โ†’ ํ…Œ์ŠคํŠธ ์Šค์บํด๋“œ ์ž๋™ ์ƒ์„ฑ
    • ๋ˆ„๋ฝ๋œ ์ผ€์ด์Šค๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐํ™” ์•„์ด๋””์–ด ์ œ์•ˆ๋ฐ›๊ธฐ ์šฉ์ด

https://docs.pytest.org/en/stable/

https://pypi.org/project/pytest/


  1. ์™œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š”๊ฐ€?

์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์—์„œ ํ…Œ์ŠคํŠธ๋Š” โ€œ์ฝ”๋“œ๊ฐ€ ์•ฝ์†ํ•œ ์กฐ๊ฑด์„ ์ง€ํ‚ค๋Š”๊ฐ€?โ€๋ฅผ ์ž๋™์œผ๋กœ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋ฅผ ์ž˜ ์„ค๊ณ„ํ•˜๋ฉด ๋ฆฌํŒฉํ„ฐ๋ง์— ์ž์‹ ๊ฐ์„ ์–ป๊ณ , ๋ฒ„๊ทธ๋ฅผ ์ดˆ๊ธฐ์— ์žก์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ˜‘์—… ์‹œ ์‹ ๋ขฐ์„ฑ์ด ์˜ฌ๋ผ๊ฐ‘๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ณธ ์ฒ ํ•™์€ AAA(Arrangeโ€“Actโ€“Assert, 3A)์ž…๋‹ˆ๋‹ค.:

  1. Arrange: ์ค€๋น„ โ€“ ์ž…๋ ฅ, ํ™˜๊ฒฝ, ๋ฐ์ดํ„ฐ ์ค€๋น„
  2. Act: ์‹คํ–‰ โ€“ ํ•จ์ˆ˜๋‚˜ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
  3. Assert: ๊ฒ€์ฆ โ€“ ๊ธฐ๋Œ€ํ•œ ์ถœ๋ ฅ๊ณผ ๋น„๊ต

https://semaphore.io/blog/aaa-pattern-test-automation


  1. pytest๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

pytest๋Š” ํŒŒ์ด์ฌ์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

https://youtu.be/cHYq1MRoyI0

์ฃผ์š” ํŠน์ง•

  • Autodiscovery: test_*.py ๋˜๋Š” *_test.py ํ˜•์‹์˜ ํŒŒ์ผ, test_* ํ•จ์ˆ˜๋ช…์„ ์ž๋™์œผ๋กœ ํƒ์ง€
  • Rich Assertion Introspection: assert ์‹คํŒจ ์‹œ ์‹ค์ œ ๊ฐ’๊ณผ ๊ธฐ๋Œ€ ๊ฐ’์„ ์ง๊ด€์ ์œผ๋กœ ์ถœ๋ ฅ
  • Fixture ์‹œ์Šคํ…œ: ํ…Œ์ŠคํŠธ ์ค€๋น„/์ •๋ฆฌ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ๊ด€๋ฆฌ
  • ํŒŒ๋ผ๋ฏธํ„ฐํ™” ์ง€์›: ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์—ฌ๋Ÿฌ ์ผ€์ด์Šค๋กœ ๋ฐ˜๋ณต ์‹คํ–‰
  • ํ˜ธํ™˜์„ฑ: unittest, nose ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ๋„ ์‹คํ–‰ ๊ฐ€๋Šฅ
  • ํ™•์žฅ์„ฑ: Django, Flask, Pandas ๋“ฑ ๊ฐ์ข… ํ”Œ๋Ÿฌ๊ทธ์ธ ํ™œ์šฉ ๊ฐ€๋Šฅ

https://youtu.be/cHYq1MRoyI0


  1. ๊ฐœ๋ฐœํ™˜๊ฒฝ๊ณผ ์‹ค์šฉ ํฌ์ธํŠธ

  • IDE ํ†ตํ•ฉ: Pycharm, VSCode์—์„œ ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜ ์˜†์— ์‹คํ–‰ ๋ฒ„ํŠผ ํ‘œ์‹œ โ†’ CLI ์—†์ด๋„ ํด๋ฆญ ์‹คํ–‰
  • ๋ณด๊ณ ์„œ ๊ฐ€๋…์„ฑ: ์‹คํŒจ ์‹œ ์–ด๋А ๊ฐ’์ด ๋‹ฌ๋ž๋Š”์ง€ ์ž์„ธํžˆ ์ถœ๋ ฅ โ†’ assertEqual ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋ณด๋‹ค ์ง๊ด€์ 
  • ๋งˆ์ปค(Markers): @pytest.mark.slow, skip, xfail๋กœ ์‹คํ–‰/์ œ์™ธ ์ œ์–ด ๋ฐ ๋ฆฌํฌํŠธ์— ํƒœ๊ทธ ํ‘œ์‹œ
  • AI ๋ณด์กฐ: ChatGPT ๋“ฑ์„ ํ™œ์šฉํ•ด ํ…Œ์ŠคํŠธ ์Šค์บํด๋“œ ์ž๋™ ์ƒ์„ฑ โ†’ ๋ˆ„๋ฝ๋œ ์ผ€์ด์Šค ์ ๊ฒ€์— ์œ ์šฉ

  1. ํ•ต์‹ฌ ํ…Œ์ŠคํŠธ ์œ ํ˜•๊ณผ ์˜ˆ์ œ

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์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์‹คํŒจ ์˜ˆ์‹œ

    • ์ธ์ฝ”๋”ฉ/๊ฐœํ–‰ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜๋กœ ํŒŒ์‹ฑ ์‹คํŒจ.
    • ํ—ค๋” ์˜คํƒ€("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}.
  • ์‹คํŒจ ์˜ˆ์‹œ
    • ํ—ค๋” ์˜คํƒ€(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(์†Œ์ˆ˜)โ€ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ ์œ ์ง€๋กœ ํ”ผ๋“œ๋ฐฑ ์†๋„ ํ™•๋ณด

  1. ๊ณ ๊ธ‰ ํŒจํ„ด

  • Property-based Testing (hypothesis): ๋ฌด์ž‘์œ„ ์ž…๋ ฅ์œผ๋กœ ํ•ญ์ƒ ์„ฑ์งˆ์ด ์œ ์ง€๋˜๋Š”์ง€ ํ™•์ธ
  • Regression Test: ๊ณผ๊ฑฐ ๋ฒ„๊ทธ ์ผ€์ด์Šค๋ฅผ ๊ณ ์ • ํ…Œ์ŠคํŠธ๋กœ ๋‚จ๊ฒจ ์žฌ๋ฐœ ๋ฐฉ์ง€
  • Performance Test (pytest-benchmark): ํŠน์ • ํ•จ์ˆ˜ ์„ฑ๋Šฅ ์ถ”์ 
  • Async Test (pytest-asyncio): ๋น„๋™๊ธฐ ํ•จ์ˆ˜ ๊ฒ€์ฆ

  1. AI๋ฅผ ํ™œ์šฉํ•œ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

๊ฐ•์˜์˜ ๋…ํŠนํ•œ ํฌ์ธํŠธ๋Š” ChatGPT ํ™œ์šฉ์ž…๋‹ˆ๋‹ค.

  • ํ•จ์ˆ˜/ํด๋ž˜์Šค๋ฅผ ๋ถ™์—ฌ๋„ฃ๊ณ  โ†’ โ€œpytest fixture/parameterize/raises๋ฅผ ํ™œ์šฉํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•ด์ค˜โ€ ์š”์ฒญ
  • AI๊ฐ€ ๊ณจ๊ฒฉ์„ ์ž๋™ ์ƒ์„ฑ, ๋ˆ„๋ฝ๋œ ์ผ€์ด์Šค๊นŒ์ง€ ์ œ์•ˆ
  • ๊ฐœ๋ฐœ์ž๋Š” ์ด๋ฅผ ๊ฒ€์ฆยท๋ณด์™„ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํšจ์œจ์„ ํฌ๊ฒŒ ๋†’์ผ ์ˆ˜ ์žˆ์Œ

  1. ์ •๋ฆฌ

pytest๋Š”

  1. ์ง๊ด€์  ๋ฌธ๋ฒ• (assert) + ํ’๋ถ€ํ•œ ๋ฆฌํฌํŠธ
  2. Fixture/Marker/Parametrize๋กœ ๊ฐ•๋ ฅํ•œ ํ™•์žฅ์„ฑ
  3. IDE ํ†ตํ•ฉ, ํ˜ธํ™˜์„ฑ, AI ๋ณด์กฐ

์ด ์„ธ ๊ฐ€์ง€๊ฐ€ ํ•ฉ์ณ์ง€๋ฉด์„œ โ€œํ…Œ์ŠคํŠธ๊ฐ€ ๊ท€์ฐฎ์€ ๋ถ€๊ฐ€ ์ž‘์—…โ€์ด ์•„๋‹ˆ๋ผ,
โ€œ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๋†’์ด๊ณ  ๊ฐœ๋ฐœ์„ ๋น ๋ฅด๊ฒŒ ํ•˜๋Š” ๋„๊ตฌโ€์ž„์„ ์ฒด๊ฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.



-->