저번 강의에서는
assert
문을 이용해 함수를 만들어 직접 테스트를 실행해보았습니다. 이번 강의에서는 pytest
프레임워크를 이용해 단위 테스트를 만들어 보겠습니다.pytest는 단위 테스트 프레임워크를 넘어서 테스트 실행을 도와주는 테스트 러너(Test Runner)입니다. 먼저 왜 pytest와 같은 테스트 러너를 사용해야 하는지 알아봅시다. 기존에는 테스트 함수를 작성하면 수동으로 함수를 실행해야만 했습니다. 또한, 성공 및 실패 여부를 직접
print()
문을 통해 관리해야 하며 일부 테스트만 실행하려면 함수를 개별적으로 실행해야 해 관리가 어려워집니다. 또한, 에러도 Python의 기본 tracestack만 제공해 오류가 발생한 상황을 자세히 살펴보려면 무조건 코드를 확인해봐야 합니다.테스트 러너는 개별적인 유닛 테스트를 모아서 실행할 때 도와주며, 실행 소요 시간 및 커버리지와 같은 다양한 통계를 제공합니다.
Python을 위한 단위 테스트 프레임워크는
pytest
와 unittest
가 있습니다. 두 프레임워크의 가장 큰 차이점은 테스트를 작성하는 형식입니다. pytest는 이전 강의에서 만든 방식과 매우 유사하게 작동합니다. 함수와 assert
문을 사용해 테스트를 구성할 수 있습니다. 또한, 보일러플레이트(boilerplate—초기에 작성하는 코드 템플릿) 코드의 양이 작기 때문에 빠른 시간에 테스트를 작성할 수 있습니다. unittest
는 Java의 Junit
프레임워크에서 영감을 많이 받았기 때문에 클래스(Class) 방식으로 테스트를 작성해야 하기 때문에 보일러플레이트가 길어지며 assert
문 대신 unittest의 함수를 호출해야 합니다. pytest는 웹 프레임워크 Flask와 HTTP 요청 라이브러리 Requests 등에서 사용되며 unittest는 웹 프레임워크 Django 등에서 사용됩니다.pytest를 사용하기 위해 로컬 환경에서 코드를 작성해 봅시다. 컴퓨터에서 IDE 및 코드 에디터를 실행해 주세요. 저는 VSCode를 사용해 진행해보겠습니다.
먼저 프로젝트 파일을 저장할 폴더를 만들어 보겠습니다. 상단 메뉴에서 File -> Open (열기)를 선택한 후, 새 폴더를 만들어 주세요. 저는
pytest-demo
폴더를 만들고 열어보겠습니다.이제 새로운
calculator.py
파일을 생성한 후 테스트를 위한 간단한 계산기를 만들어 보겠습니다.def add(a, b): return a + b def subtract(a, b): return a - b def multiply(a, b): return a * b def divide(a, b): return a / b
calculator.py
아래에 코드를 추가로 작성해 계산기를 위한 테스트를 작성해 봅시다.def test_add_two_int(): assert add(3, 5) == 8 assert add(1, 1) == 2 assert add(-52, 5) == -47 def test_subtract_two_int(): assert subtract(7, 5) == 2 assert subtract(2, 1) == 1 assert subtract(1, 99) == -98 def test_multiply_two_int(): assert multiply(5, 7) == 35 assert multiply(4, 3) == 12 assert multiply(-6, 8) == -48 def test_divide_two_int(): assert divide(5, 1) == 5 assert divide(1024, 5) == 204.8 assert divide(36, -3) == -12
pytest는 pip에서 설치해야 합니다. 터미널을 실행한 후, 가상 환경을 만들어 pytest를 설치해 봅시다. Conda 사용자는
conda install pytest
로도 설치할 수 있습니다.$ python3 -m venv test $ source test/bin/activate $ pip install pytest
pytest calculator.py
명령어를 입력해 테스트를 실행시켜 봅시다.$ pytest calculator.py ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: /Users/user/Desktop collected 4 items calculator.py .... [100%] ============================== 4 passed in 0.03s ===============================
모든 테스트가 통과하는 모습을 확인할 수 있습니다. 파일 이름 이후에 표시되는 초록색 점의 갯수는 통과한 테스트의 수이며, 오류가 발생하면 빨간색
F
(fail)가 표시됩니다. 그러나 지금은 통과한 테스트는 표시가 생략됩니다. 더 많은 정보를 확인하기 위해 Verbose 파라미터(-v
)를 추가할 수 있습니다.pytest -v calculator.py
를 입력하고 테스트를 다시 실행해 봅시다.$ pytest -v calculator.py ================================== test session starts =================================== platform darwin -- Python 3.8.2, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /Users/user/venv/test/bin/python3 cachedir: .pytest_cache rootdir: /Users/user/Desktop collected 4 items calculator.py::test_add_two_int PASSED [ 25%] calculator.py::test_subtract_two_int PASSED [ 50%] calculator.py::test_multiply_two_int PASSED [ 75%] calculator.py::test_divide_two_int PASSED [100%] =================================== 4 passed in 0.01s ====================================
이제 자세한 테스트 이름과 통과 여부를 확인할 수 있습니다.
지금은 코드와 테스트가 하나의 파일에 있는데, 정리하기 위해 리팩토링해 코드와 테스트를 분리해보도록 하겠습니다. pytest의 공식 문서에서는 두 가지의 방법 중 하나를 추천합니다. 여기서는 첫번째 방법인 별도의
tests
폴더를 만들어 정리해보겠습니다.calculator
라는 상위 폴더를 생성해 주세요. 그리고 calculator.py
를 폴더 내로 옮겨 주세요. 그리고, 폴더에 빈 __init__.py
를 생성해 주세요. __init__.py
는 Python이 폴더를 패키지 (package)로 처리하기 위해 생성해 줍니다.tests
폴더를 만든 후, __init__.py
와 test_calculator.py
를 생성해 주세요. 지금까지 잘 따라 오셨다면 이 구조와 동일해야 합니다.|-- calculator | |-- tests | | |-- __init__.py | | |-- test_calculator.py | |-- __init__.py | |-- calculator.py
calculator.py
의 테스트 함수를 복사한 후 지우고 저장합니다. 그리고 test_calculator.py
로 돌아와서 붙여넣습니다.test_calculator.py
에서 calculator.py
의 함수를 사용할 수 있도록 모듈을 불러오기 위해 첫 줄에 from calculator.calculator import add, subtract, multiply, divide
를 추가합니다.이번에는 터미널에서
pytest
만 입력 후 실행해봅니다. pytest가 자동으로 테스트를 인식한 후 실행한 모습을 확인할 수 있습니다. pytest는 디렉토리와 모든 하위 디렉토리의 test
로 시작하거나 끝나는 모든 파일을 찾아 test_
로 시작하는 함수를 실행합니다. pytest의 테스트 탐색 과정은 공식 문서에서 확인할 수 있습니다.코드를 더 최적화할 수 있을까요? 지금은 테스트 함수 내에서 같은 함수를 다른 인자로 테스트하기 위에 세번 작성하였습니다.
pytest
의 데코레이터 (Decorator)를 사용해서 코드를 더 줄여봅시다. Python에서의 데코레이터는 함수를 감싸는 함수라고 생각해도 괜찮습니다. 이를 통해 함수를 직접 수정하지 않아도 기능을 추가할 수 있습니다.pytest는 같은 함수를 다른 인자로 여러번 호출할 때 최적화할 수 있는 매개변수화 데코레이터를 제공합니다. 이를 통해 다양한 데이터로 하나의 함수를 간단히 테스트할 수 있습니다. 테스트 함수의 선언문 위에
@pytest.mark.parametrize('변수명', [(데이터1), (데이터2)])
형식으로 추가해서 매개변수화가 가능합니다.직접 코드를 작성해보면 이해하기가 쉽습니다. 먼저
test_calculator.py
파일의 첫 번째 줄에 import pytest
를 추가해줍니다. 그리고 test_add_two_int()
함수 선언문 바로 위에 @pytest.mark.parametrize('a,b,expected', [(3, 5, 8)])
를 작성합니다.이제 함수에
a, b,와 expected
라는 인자를 전달해 봅시다. 첫번째 assert 문의 숫자를 a, b와 expected로 대치한 후 나머지 assert문은 삭제합니다.import pytestfrom calculator.calculator import add, subtract, multiply, divide @pytest.mark.parametrize('a,b,expected', [(3, 5, 8)]) def test_add_two_int(a, b, expected): assert add(a, b) == expected def test_subtract_two_int(): ...
다시
pytest
를 실행하면 오류 없이 동작하는 것을 확인할 수 있습니다. @pytest.mark.parametrize()
에 전달하는 인수를 자세히 알아봅시다. 먼저, 문자열로 함수에 전달될 매개변수명을 순서대로 지정해 줍니다. 여기서는 테스트 함수가 a
, b
와 expected
를 전달받기 때문에 'a,b,expected'
를 전달합니다. 두번째 인수는 튜플이 담긴 리스트입니다. 튜플은 첫 번째 인자의 매개변수 순서대로 지정해야 하며, 리스트 내의 모든 튜플을 루프로 돌아가면서 코드를 테스트합니다. 리스트에 튜플을 두개 더 추가해 테스트 항목을 늘려 봅시다.@pytest.mark.parametrize('a,b,expected', [(3, 5, 8), (1, 1, 2), (-52, 5, -47)])
이제 남은 세개의 함수에도 매개변수화를 적용해 봅시다.
import pytestfrom calculator.calculator import add, subtract, multiply, divide @pytest.mark.parametrize('a,b,expected', [(3, 5, 8), (1, 1, 2), (-52, 5, -47)]) def test_add_two_int(a, b, expected): assert add(a, b) == expected @pytest.mark.parametrize('a,b,expected', [(7, 5, 2), (2, 1, 1), (1, 99, -98)]) def test_subtract_two_int(a, b, expected): assert subtract(a, b) == expected @pytest.mark.parametrize('a,b,expected', [(5, 7, 35), (4, 3, 12), (-6, 8, -48)]) def test_multiply_two_int(a, b, expected): assert multiply(a, b) == expected @pytest.mark.parametrize('a,b,expected', [(5, 1, 5), (1024, 5, 204.8), (36, -3, -12)]) def test_divide_two_int(a, b, expected): assert divide(a, b) == expected
오류가 나면 어떤 식으로 표시되는지 확인해 봅시다. 일부러 데이터의 일부를 오류가 발생하게 변경해 보겠습니다.
@pytest.mark.parametrize('a,b,expected', [(3, 5, 10), (1, 1, 2), (-52, 5, -47)])
3 + 5 는 10이 아니기 때문에 오류가 발생해야 합니다.
==================================================================== test session starts ==================================================================== platform darwin -- Python 3.7.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 rootdir: /Users/user/Documents collected 16 items tests/test_calculator.py F........... [ 75%] tests/test_calculator_1.py .... [100%] ========================================================================= FAILURES ========================================================================== _________________________________________________________________ test_add_two_int[3-5-10] __________________________________________________________________ a = 3, b = 5, expected = 10 @pytest.mark.parametrize('a,b,expected', [(3, 5, 10), (1, 1, 2), (-52, 5, -47)]) def test_add_two_int(a, b, expected): > assert add(a, b) == expected E assert 8 == 10 E + where 8 = add(3, 5) tests/test_calculator.py:7: AssertionError ================================================================== short test summary info ================================================================== FAILED tests/test_calculator.py::test_add_two_int[3-5-10] - assert 8 == 10 =============================================================== 1 failed, 15 passed in 0.09s ================================================================
수동으로 만든 테스트와 다르게 오류 리포트가 자세하게 표시되는 것을 확인할 수 있습니다. 오류가 발생한 매개변수도 목록으로 표시해 정확하게 분석할 수 있습니다.