HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📝
GitHub Action을 활용한 자동 크롤러 웹 페이지 만들기
/
📋
2. pytest 사용해보기
📋

2. pytest 사용해보기

 
저번 강의에서는 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 ================================================================
수동으로 만든 테스트와 다르게 오류 리포트가 자세하게 표시되는 것을 확인할 수 있습니다. 오류가 발생한 매개변수도 목록으로 표시해 정확하게 분석할 수 있습니다.