Dev/SW Engineering

[TDD] Python Testing Framework 1 - Pytest, unittest

HJChung 2021. 5. 21. 05:41

1.Installation and Getting Started (unittest / pytest)

1) Unittest

unittest는 python에 내장되어있어 따로 설치하지 않아도 되는 표준 라이브러리 입니다. 그래서 바로 import해서 사용할 수 있습니다.

import unittest

2) Pytest

pytest는 설치를 하고, import하여 사용합니다.

$ pip install pytest

import pytest


2. Naming Conventions (unittest / pytest)

1) Unittest

        • 파일명: test로 시작
          • Unittest에서는 지정된 파일은 반드시 모듈로 import 가능해야 합니다. 
        • 메소드명: test로 시작
          • 단위 테스트의 기본 구성 블록인 TestCase를 base class로 하는 클래스를 작성하고, 테스트를 수행하는 로직을 메서드로 추가해주어 테스트 케이스를 생성합니다. 이 때 특정한 테스트 코드를 수행하도록  test로 시작하는 이름의 테스트 메서드를 작성합니다.  
        • 실행 명령: 실행하는 방법은 2가지인데, 해당 코드를 추가하고 python으로 직접 실행하여 줄 수도 있으며conventions에 맞게 파일명을 다 맞게 적어주었다면 2번 명령어로 test탐색을 통해 unittest가 일괄적으로 진행 되게 할 수도 있습니다. 또는 파일명을 저 규칙대로 정하지 않더라도 Discovery 를 통해 경로지정이 가능합니다. Discovery 관련 옵션들은 python -h unittest 명령을 통해 확인할 수 있습니다.
          • $ python [해당 테스트 파일 명]
          • $ python -m unittest [option] [해당 테스트 파일 명]
            • -m 옵션을 python 실행 시 주면 뒤에 나오는 모듈을 임포트 한 후 스크립트를 실행합니다.unittest 모듈은 프로젝트의 최상위 디렉토리에서 명령행을 사용하여 모듈, 클래스,  테스트 메서드의 테스트들을 실행할 수 있습니다.
            • $ python -m unittest test_module1 test_module2
            • $ python -m unittest test_module.TestClass
            • $ python -m unittest test_module.TestClass.test_method나열
            • 예를들어 ex1_test.py라면 $ python -m unittest discover -s /python-test/tests -p “*_test.py” 로 해주면 됩니다. 

2) Pytest

Pytest에서는 아래와 같은 규칙으로 작성만해주면 pytest라는 명령어로 테스트를 탐색하여 테스트를 진행합니다.

  • 파일명: pytest will run all files of the form test_*.py or *_test.py in the current directory and its subdirectories.
  • 함수명: test_로 시작 (pytest discovers all tests following its Conventions for Python test discovery, so it finds both test_ prefixed functions.)
  • Class명: Test로 시작 (prefix your class with Test otherwise the class will be skipped.)
  • 실행명령:
    • 1)  $ pytest
    • 2)  $ pytest [해당 테스트 파일 명]

3. An Example of a Simple Test (unittest / pytest)

다음은 아주 간단한 테스트 케이스를 작성하고 실행해 본 것입니다. 

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

1) Unittest

unittest의 경우 unittest.TestCase를 상속받아서 class 형태로 작성해야하며

from tests.unittest1.ex1 import add

import unittest

class Add_testing(unittest.TestCase):
  def test1(self):
    self.assertEqual(add(1, 2), 3)
  def test_str(self):
    self.assertEqual(add("a", "b"), "ab")

  

if __name__ == '__main__':
  unittest.main()

2) pytest

pytest는 convention을 지켜서 함수형으로 작성할 수도 있고, unittest와 달리 TestCase와 같이 뭘 상속받지 않아도 됩니다. 

from tests.pytest1.myapp.ex1 import add

def test_add_num():
    assert add(1, 2) == 3

def test_add_str():
    assert add("a", "b") == "ab"

class TestSample:
    def test_add_num(self):
        assert add(1, 2) == 3
    def test_add_str(self):
        assert add("a", "b") == "ab"

 

unittest와의 또 다른 차이점은 unittest에서 assertEqual 같은 테스트용 메소드를 사용했던 것과 달리, 파이썬의 assert 문을 사용하여 대부분의 테스트를 할 수 있습니다. unittest의 테스트용 메소드는  unittest assertMethod List에서 확인할 수 있습니다.

 

4. Asserting Expected Exceptions (unittest / pytest)

def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be less than 0")

Raise Error를 작성하였다면 이 역시 해당 상황에서 기대대로 실행되는지 테스트를 하는 것이 필요합니다. 이 때 unittest에서는 assertRaise, pytest에서는 pytest.raises를 사용할 수 있습니다. 

unittest에서 컨텍스트 관리자로 사용되면, 

컨텍스트 관리자는 잡은 예외 객체를 exception attribute에 저장하는데, 이를 예외에 대해서 추가적인 검사를 수행하려는 경우에 위의 예시 코드처럼 사용할 수 있습니다. 

1) Unittest

# REFERENCE: https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises

from tests.unittest2.ex2 import validate_age

import unittest

class Age_testing(unittest.TestCase):
  def test_validate_age_valid_age(self):
    # validate_age(-10) #     raise ValueError("Age cannot be less than 0 ValueError: Age cannot be less than 0
    validate_age(10)

  def test_validate_age_invalid_age(self):
    with self.assertRaises(ValueError):
        validate_age(-1)

  def test_validate_age_invalid_age2(self):
    with self.assertRaises(ValueError) as exc_info:
        validate_age(-1)
    the_exception = exc_info.exception
    self.assertEqual(str(the_exception), "Age cannot be less than 0")
    

if __name__ == '__main__':
  unittest.main()

2) Pytest

import pytest

from tests.pytest2.ex2 import validate_age

def test_validate_age_valid_age():
    validate_age(10)

def test_validate_age_invalid_age():
    with pytest.raises(ValueError, match="Age cannot be less than 0"):
        validate_age(-1)

 

5. Skipping Tests (unittest / pytest)

제목 그대로 test를 skip할 수 있는 방법입니다. 

1) Unittest

2) Pytest

pytest에서 skip할 때 사용한 mark는 skip하는 역할외에도 테스트에 대한 metadata를 지정해주는 역할을 합니다. 

예를들어 pytest-django에서 테스트할 때 DB에 접근이 필요한 경우 @pytest.mark.django_db 이런식으로 적어줍니다.

6. Test Fixture

테스트가 많을 때 각각의 테스트를 위한 사전 설정이 반복되어야 하는 경우가 있습니다.  

이 때 Test Fixture가 필요합니다. Fitxture는 여러 개의 테스트를 수행 할 때 필요한 준비도구와 재료및 그에 관련된 정리 동작에 해당합니다

1) Unittest

반복되는 사전 설정을 setUp()을 사용하여 사전 설정 코드를 밖으로 분리할 수 있습니다.  

그리고 테스트 메서드가 실행되고 나서 정리를 위해 tearDown()가 사용됩니다.  

정리하면 각각의 테스트 메서드가 실행되기 전에 항상 setUp() 메서드가 실행되어 setUp내용을 생성하고, 테스트 실행후 tearDown() 메서드가 실행됩니다.

테스트마다 setUp 해주고 tearDown 했다가 다시 setUp 하는 것 보다 어쩔 때는 맨 처음에 setUp되고, 그 다음에 test가 모두 끝난 후에 tearDown 하도록 하는 것도 필요할 것입니다.

그 때 사용할 수 있는 것이 setUpClass, tearDownClass입니다. 

실행순서

2) Pytest

Pytest에서는 pytest.fixture 데코레이터를 이용해서 구현해 줄 수 있습니다.

 

pytest의 fixture에는 4가지 scope가 있고, 각각의 scope마다 한 번씩 실행됩니다. 

  • Session: pytest를 한 번 실행할 때마다 한 번
  • Module: 테스트 스크립트의 모듈마다 한 번
  • Class: 테스트 클래스마다 한 번
  • Function: 테스트 케이스마다 한 번