Profile picture

[Python] pytest 실습

JaehyoJJAng2023년 04월 05일

Getting Started

- 프로젝트 폴더 생성

mkdir pytest-demo

- calculator.py 파일 생성한 후 테스트를 위한 간단한 계산기를 Class로 작성

class Calculator:
    def add(a:int,b:int) -> int:
        return a + b
    
    def subtract(a:int,b:int) -> int:
        return a - b
    
    def multiply(a:int,b:int) -> int:
        return a * b  

    def divide(a:int,b:int) -> int:
        return a / b

- calculator.py 아래에 코드를 추가로 작성해 계산기를 위한 테스트 작성해보자

# --------------------------------
# Test
# --------------------------------
class TestCalculator:
    @classmethod
    def setup_class(cls) -> None:
        cls.calculator : Calculator = Calculator()
    
    @classmethod
    def teardown_class(cls) -> None:
        del cls.calculator 
        
    def test_add_two_int(self) -> None:
        assert self.calculator.add(a=3,b=5) == 8
        assert self.calculator.add(a=1,b=1) == 2
        assert self.calculator.add(a=0,b=3) == 3
        assert self.calculator.add(a=-1,b=-2) == -3
    
    def test_substract_two_int(self) -> None:
        assert self.calculator.subtract(a=3,b=3) == 0
        assert self.calculator.subtract(a=3,b=2) == 1
        assert self.calculator.subtract(a=3,b=-4) == 7
    
    def test_multiply_two_int(self) -> None:
        assert self.calculator.multiply(a=5,b=5) == 25        
    
    def test_divide_two_int(self) -> None:
        assert self.calculator.divide(a=5,b=5) == 1.0

pytest 설치

python3 -m venv dev
source dev/bin/activate
pip install pytest

테스트 실행

pytest -v calculator.py

========================================== test session starts ==========================================
platform darwin -- Python 3.10.2, pytest-7.2.0, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
cachedir: .pytest_cache
metadata: {'Python': '3.10.2', 'Platform': 'macOS-13.2.1-arm64-arm-64bit', 'Packages': {'pytest': '7.2.0', 'pluggy': '1.0.0'}, 'Plugins': {'html': '3.2.0', 'Faker': '15.3.4', 'metadata': '2.0.4'}}
rootdir: /Users/jaehyolee/git/Python/pytest/pytest-demo
plugins: html-3.2.0, Faker-15.3.4, metadata-2.0.4
collected 4 items

calculator.py::TestCalculator::test_add_two_int PASSED                                            [ 25%]
calculator.py::TestCalculator::test_substract_two_int PASSED                                      [ 50%]
calculator.py::TestCalculator::test_multiply_two_int PASSED                                       [ 75%]
calculator.py::TestCalculator::test_divide_two_int PASSED                                         [100%]

=========================================== 4 passed in 0.04s ===========================================

코드와 테스트 분리하기

현재 위에서는 calculator.py 파일 안에

계산기 코드와 계산기 코드를 테스트하는 코드가 같이 존재한다.

테스트 코드와 계산기 코드를 분리해 조금 더 가독성 있게 만들어보자.


코드 분리

pytest 공식 문서 에서는 두 가지의 방법 중 하나를 추천한다.

해당 포스팅에서는 첫번째 방법인 별도의 tests 폴더를 만들어 정리해보자.


calculator 라는 상위 폴더를 생성하고 calculator.py 파일을 calculaotr 폴더로 옮기자.

mv calculator.py ./calculator

그리고 calculator 폴더에 빈 __init__.py 파일을 생성하자

  • __init__.py 파일은 python이 폴더를 패키지 로 처리하기 위해 생성하는 것임

pytest를 한번 실행해보자

pytest -v tests/test_calculator.py

========================================== test session starts ==========================================
platform darwin -- Python 3.10.2, pytest-7.2.0, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
cachedir: .pytest_cache
metadata: {'Python': '3.10.2', 'Platform': 'macOS-13.2.1-arm64-arm-64bit', 'Packages': {'pytest': '7.2.0', 'pluggy': '1.0.0'}, 'Plugins': {'html': '3.2.0', 'Faker': '15.3.4', 'metadata': '2.0.4'}}
rootdir: /Users/jaehyolee/git/Python/pytest/pytest-demo/calculator
plugins: html-3.2.0, Faker-15.3.4, metadata-2.0.4
collected 4 items

tests/test_calculator.py::TestCalculator::test_add_two_int PASSED                                 [ 25%]
tests/test_calculator.py::TestCalculator::test_substract_two_int PASSED                           [ 50%]
tests/test_calculator.py::TestCalculator::test_multiply_two_int PASSED                            [ 75%]
tests/test_calculator.py::TestCalculator::test_divide_two_int PASSED                              [100%]

=========================================== 4 passed in 0.04s ===========================================

오류 없이 동작하고 있는 것을 확인할 수 있었다.

pytest 데코레이터

@pytest.mark.parametrize()에 전달하는 인수를 자세히 봐보자.

  • @pytest.mark.parametrize('변수명', [(데이터1), (데이터2)]) 형식으로 추가해서 매개변수화가 가능하다.

아래 코드를 봐보자

@pytest.mark.parametrize('a,b,expected',[(3,5,8)])
def test_add_two_int(self,a,b,expected) -> None:
    assert self.calculator.add(a=a,b=b) == expected

위 코드로 변경 후 실행해도 위와 같이 똑같은 결과가 나온다

========================================== test session starts ==========================================
platform darwin -- Python 3.10.2, pytest-7.2.0, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
cachedir: .pytest_cache
metadata: {'Python': '3.10.2', 'Platform': 'macOS-13.2.1-arm64-arm-64bit', 'Packages': {'pytest': '7.2.0', 'pluggy': '1.0.0'}, 'Plugins': {'html': '3.2.0', 'Faker': '15.3.4', 'metadata': '2.0.4'}}
rootdir: /Users/jaehyolee/git/Python/pytest/pytest-demo/calculator
plugins: html-3.2.0, Faker-15.3.4, metadata-2.0.4
collected 4 items

tests/test_calculator.py::TestCalculator::test_add_two_int[3-5-8] PASSED                          [ 25%]
tests/test_calculator.py::TestCalculator::test_substract_two_int PASSED                           [ 50%]
tests/test_calculator.py::TestCalculator::test_multiply_two_int PASSED                            [ 75%]
tests/test_calculator.py::TestCalculator::test_divide_two_int PASSED                              [100%]

=========================================== 4 passed in 0.04s ===========================================

자 이제 그러면 위 데코레이터를 적용하여 코드를 리팩토링 해보자

from calculator.calculator import Calculator
import pytest

class TestCalculator:
    @classmethod
    def setup_class(cls) -> None:
        cls.calculator : Calculator = Calculator()
    
    @classmethod
    def teardown_class(cls) -> None:
        del cls.calculator 
    
    @pytest.mark.parametrize('a,b,expected',[(3,5,8)])
    def test_add_two_int(self,a,b,expected) -> None:
        assert self.calculator.add(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,0)])
    def test_substract_two_int(self,a,b,expected) -> None:
        assert self.calculator.subtract(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,25)])
    def test_multiply_two_int(self,a,b,expected) -> None:
        assert self.calculator.multiply(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,1.0)])
    def test_divide_two_int(self,a,b,expected) -> None:
        assert self.calculator.divide(a=a,b=b) == expected

다시 pytest를 실행하면 오류 없이 동작하는 것을 확인할 수 있습니다.

@pytest.mark.parametrize()에 전달하는 인수를 자세히 알아봅시다.

먼저, 문자열로 함수에 전달될 매개변수명을 순서대로 지정해 줍니다. 여기서는 테스트 함수가 a, bexpected를 전달받기 때문에 'a,b,expected'를 전달합니다. 두번째 인수는 튜플이 담긴 리스트입니다. 튜플은 첫 번째 인자의 매개변수 순서대로 지정해야 하며, 리스트 내의 모든 튜플을 루프로 돌아가면서 코드를 테스트합니다. 리스트에 튜플을 두개 더 추가해 테스트 항목을 늘려 봅시다.

@pytest.mark.parametrize('a,b,expected', [(3, 5, 8), (1, 1, 2), (-52, 5, -47)])


그러면 위 내용을 적용하여 테스트 항목을 더 늘려보자.

from calculator.calculator import Calculator
import pytest

class TestCalculator:
    @classmethod
    def setup_class(cls) -> None:
        cls.calculator : Calculator = Calculator()
    
    @classmethod
    def teardown_class(cls) -> None:
        del cls.calculator 
    
    @pytest.mark.parametrize('a,b,expected',[(3,5,8),(1,2,3)])
    def test_add_two_int(self,a,b,expected) -> None:
        assert self.calculator.add(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,0),(10,5,5)])
    def test_substract_two_int(self,a,b,expected) -> None:
        assert self.calculator.subtract(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,25),(4,5,20)])
    def test_multiply_two_int(self,a,b,expected) -> None:
        assert self.calculator.multiply(a=a,b=b) == expected
    
    @pytest.mark.parametrize('a,b,expected',[(5,5,1.0),(4,2,2)])
    def test_divide_two_int(self,a,b,expected) -> None:
        assert self.calculator.divide(a=a,b=b) == expected

실행해보면 아래와 같이 테스트 항목 결과가 늘어난 것을 볼 수 있다.

========================================== test session starts ==========================================
platform darwin -- Python 3.10.2, pytest-7.2.0, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
cachedir: .pytest_cache
metadata: {'Python': '3.10.2', 'Platform': 'macOS-13.2.1-arm64-arm-64bit', 'Packages': {'pytest': '7.2.0', 'pluggy': '1.0.0'}, 'Plugins': {'html': '3.2.0', 'Faker': '15.3.4', 'metadata': '2.0.4'}}
rootdir: /Users/jaehyolee/git/Python/pytest/pytest-demo/calculator
plugins: html-3.2.0, Faker-15.3.4, metadata-2.0.4
collected 8 items

tests/test_calculator.py::TestCalculator::test_add_two_int[3-5-8] PASSED                          [ 12%]
tests/test_calculator.py::TestCalculator::test_add_two_int[1-2-3] PASSED                          [ 25%]
tests/test_calculator.py::TestCalculator::test_substract_two_int[5-5-0] PASSED                    [ 37%]
tests/test_calculator.py::TestCalculator::test_substract_two_int[10-5-5] PASSED                   [ 50%]
tests/test_calculator.py::TestCalculator::test_multiply_two_int[5-5-25] PASSED                    [ 62%]
tests/test_calculator.py::TestCalculator::test_multiply_two_int[4-5-20] PASSED                    [ 75%]
tests/test_calculator.py::TestCalculator::test_divide_two_int[5-5-1.0] PASSED                     [ 87%]
tests/test_calculator.py::TestCalculator::test_divide_two_int[4-2-2] PASSED                       [100%]

=========================================== 8 passed in 0.05s ===========================================

최종 파일 트리

tree .

.
└── calculator
    ├── __init__.py
    ├── calculator.py
    └── tests
        ├── __init__.py
        └── test_calculator.py

Loading script...