Team work

[TDD] 테스트 주도 개발 - pytest

taeeyeong 2024. 10. 1. 12:05

TDD - Test Driven Development.

 

TDD를 처음 경험한 건 대학원을 졸업하고 취직한 첫 회사였다.

10명이 넘는 인원이 하나의 소프트웨어 개발에 참여하고 있었다. 제대로 된 개별 경험도 없는 나에게는 이렇게 많은 인원이 팀웍으로 개발하는 소프트웨어는 버겁게 느껴졌다. 무수히 많은 모듈들 중 하나의 모듈 안에 함수 살짝 고치는 것도 머리털 빠지게 스트레스를 받았던 거 같다. 그런데 거기에 더해 그냥 개발 하는게 아닌 TDD를 하라는거다. (당시에 TDD를 처음 들어봤다.ㅎㅎ) 그냥 스크립트를 이해하기도 버거운데 모듈별로 따라오는 테스트 코드까지 보려 하니 스트레스를 정말 많이 받았던 기억이 있다. 당시 pytest로 test code를 짰기에 fixutre, mocking 등등 여러 설명을 들었지만 그 당시에 나에겐 너무나 버거웠다. 

그렇게 한달, 두달이 지나고 조금씩 테스트 주도 개발의 장점들이 보이기 시작했다.

한 줄 코드 추가가 이 거대한 소프트웨어에 어떤 영향을 미칠지 두려웠지만 모든 test를 돌려보고 수천개의 case에서 걸리는 게 발견했을 때 '아하' 모먼트가 있었다. 이걸 내가 놓쳤었구나를 깨닫는 모먼트랄까. 그리고 모든 오류를 수정하고 다시 test를 돌렸을 때 걸리는 거 하나 없이 녹색 점들이 주르륵 찍히는 쾌감은 이루 말할 수 없었다. 

나는 그래서 무엇보다 TDD는 팀웍으로 작업하는 큰 소프트웨어일 때 빛을 발한다고 생각한다. 

 

pytest 기본 구조

테스트 파일 및 함수 네이밍 규칙

파일 이름은 test_*.py, 또는 *_test.py 규칙을 따르고,

테스트 함수 이름은 test_* 규칙을 따른다.

pytest는 위의 네임들을 통해 테스트를 자동으로 발견하고 실행한다.

 

# math_function.py

def add(a, b):
	return a + b

 

# test_math_function.py

from math_function import add

def test_add_positive_numbers():
	assert add(2, 3) == 5
    
def test_add_negative_numbers():
	assert add(-1, -1) == -2
    
def test_add_zero():
	assert add(0, 0) == 0

 

pytest 실행

모든 테스트 실행 

터미널에서 프로젝트의 루트 디렉토리로 이동한 후, 다음 명령어를 실행한다. 

pytest

 

특정 파일의 테스트 실행

특정 테스트 파일만 실행하고 싶을 때는 파일 이름을 지정한다.

pytest test_math_functions.py

 

상세한 출력 보기

테스트 실행 시 상세한 출력을 원한다면 -v (verbose) 옵션을 사용한다. 

pytest -v

 

실행결과

============================= test session starts =============================
platform win32 -- Python 3.9.1, pytest-7.1.1, pluggy-1.0.0
rootdir: C:\Projects\pytest_example
collected 3 items

test_math_functions.py::test_add_positive_numbers PASSED                [ 33%]
test_math_functions.py::test_add_negative_numbers PASSED                [ 66%]
test_math_functions.py::test_add_zero PASSED                            [100%]

============================== 3 passed in 0.03s ===============================

 

pytest 유용한 기능들

예외 테스트 

특정 코드가 예외를 발생시키는지 테스트할 수 있다. (pytest는 pytest.raises를 제공한다)

import pytest

def divide(a, b):
	return a / b
    
def test_divide_by_zero():
	with pytest.raises(ZeroDivisionError):
    	divide(10, 0)

 

파라미터화된 테스트

@pytest.mark.parameterize 데코레이터를 통해 같은 테스트를 다양한 입력 값으로 반복해서 실행할 수 있다. (이후에 더 자세히 설명)

import pytest
from math_function import add

@pytest.mark.parameterize("a, b, expected", [
	(2, 3, 5),
    (-1, -1, -2),
    (0, 0, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
	assert add(a, b) == expected

 

실행 결과

test_math_functions.py::test_add[2-3-5] PASSED                     [ 25%]
test_math_functions.py::test_add[-1--1--2] PASSED                 [ 50%]
test_math_functions.py::test_add[0-0-0] PASSED                     [ 75%]
test_math_functions.py::test_add[100-200-300] PASSED               [100%]

 

Fixture 

테스트 환경을 설정하고 리소스를 관리하는 데 사용할 수 있다. 

 

pytest 설정 및 구성

설정 파일 (pytest.ini, tox.ini, setup.cfg)

설정파일을 통해 pytest의 동작을 커스터마이징할 수 있다. 이때 사용하는 가장 일반적인 파일은 pytest.ini이다. 

# pytest.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
    tests
    integration_tests
python_files = test_*.py *_test.py

 

위 설정파일에 대해 설명하면

- minversion : 최소 pytest 버전이다

- addopts: 추가 옵션 (-ra: 실패한 테스트 요약, -q: 간결한 출력)

- testpaths: 테스트 파일이 위치한 디렉토리

- python_files: 테스트 파일 네이밍 규칙

 

플러그인

pytest에는 다양한 플러그인들이 있다. 예를 들어, 테스트 결과를 html 형식으로 받아보고 싶다면 pytest-html 플러그인을 사용해 받아볼 수 있다. 

pip install pytest-html
pytest --html=report.html

- 테스트 실행 후 report.html 파일에 테스트 결과를 html 형식으로 저장한다. 

 

유용한 명령어 및 옵션

특정 테스트 함수 실행

특정 테스트 함수만 실행하고 싶을 때는 :: 구문을 사용한다. 

pytest test_math_function.py::test_add_positive_numbers

실패한 테스트만 재실행

이전에 실패한 테스트만 재실행하려면 --lf (last failed) 옵션을 사용한다. 

pytest --lf

마커 

pytest는 특정 마커를 추가해서 그룹화하거나 조건부로 실행할 수 있다. 예를 들어,

import pytest

@pytest.mark.slow
def test_long_running():
	import time
    time.sleep(5)
    assert True

 

특정 테스트에 마커를 추가하고, 그 마커만 테스트를 진행할 수 있다. 

pytest -m slow

또한 pytest.ini 설정파일에 마커를 등록하여 경고를 방지할 수 있다.

# pytest.ini
[pytest]
markers =
    slow: 긴 실행 시간을 가진 테스트

 

또 다른 예로는 테스트 스킵이 있다. 

import pytest

@pytest.mark.skip(reason="아직 구현되지 않음")
def test_not_implemented():
	assert False

 

실행결과

test_skip.py::test_not_implemented SKIPPED                     [100%]

============================== 1 skipped in 0.01s ===============================

 

import pytest
import sys

@pytest.mark.skipif(sys.platform == "win32", reason="윈도우에서 실행되지 않음")
def test_non_windows():
	assert True

 

실행결과

test_conditional_skip.py::test_non_windows SKIPPED                [100%]

============================== 1 skipped in 0.01s ===============================

 

 

커버리지 측정

코드 커버리지를 특정할 수도 있는데 이땐 pytest-cov 플러그인으로 가능하다.

pip install pytest-cov

 

pytset --cov=math_function tests/

- -- cov=math_function : math_function 모듈의 커버리지를 특정한다

- tests/ : 테스트 파일이 있는 디렉토리

 

 

 

 

코드 작성 -> 테스트 함수 작성 -> pytest 실행 -> 테스트 결과 확인 -> 코드 수정 및 리펙토링 -> 다시 코드 작성....

이러한 반복을 통해 코드의 안정성을 유지시키며 새로운 기능을 추가하거나, 기존 기능을 변경할 수 있다. 

pytest는 처음 익힐 땐 진입장벽이 있지만 한번 익숙해지면 간결하고 직관적인 문법, 유용한 기능들과 다양한 플러그인들이 있어 테스트를 효율적으로 작성하고 실행할 수 있게 할 것이다.