Profile picture

[Python] pytest를 이용한 단위 테스트

JaehyoJJAng2023년 04월 05일

단위 테스트 알아보기

  • 테스트 함수명은 자세하게 적는게 좋다.

테스트할 함수

def add(a:int,b:int) -> int:
    return a + b

테스트 코드 #1

def test_add_int_and_int() -> None:
    assert add(3,5) == 8

테스트 코드 #2

def test_add_float_and_int() -> None:
    assert add(5,8.0002) == 13.0002

테스트 코드 #3

def test_add_zero_and_negative_int() -> None:
    assert add(0,-3) == -3

pytest 패키지

Python에서 Unittest의 가장 많이 사용하는 모듈은 Pytest와 Unittest임

unittest는 python 기본 모듈로 설치되고 JUnit와 같은 형식으로 테스트 코드를 간단하게 작성할 수 있고,

Pytest는 unittest를 포함하여 다양한 형태의 texture 함수를 지원하고, 매우 다양한 옵션을 지원함.

또는 Pytest unittest 뿐 아니라 JUnit 등 다양한 테스트 framework을 호출할 수 있다고 한다.

Test case를 쉽게 작성하고자 하는 경우에는 unittest를 사용하고, 고도화된 Test를 만들고자 하는 경우 pytest를 사용하자.

  • pytest 명령어는 이름이 test_로 시작하는 모든 파일을 로드하고 test_로 시작하는 모든 기능을 실행함
  • pytest --help
  • Usage : pytest [options] [file_or_dir] [file_or_dir] [..]

1. pytest 설치하기

pip install pytest

2. 간단한 테스트 코드 작성

def test_true() -> bool:
    assert True

3. pytest 실행하기

pytest -v test_true.py

4. 테스트 결과

========================================== 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
rootdir: /Users/jaehyolee/git/github-action
plugins: Faker-15.3.4
collected 1 item

test_true.py::test_true PASSED                                                                    [100%]

=========================================== warnings summary ============================================
test_true.py::test_true
  /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/_pytest/python.py:199: PytestReturnNotNoneWarning: Expected None, but test_true.py::test_true returned True, which will be an error in a future version of pytest.  Did you mean to use `assert` instead of `return`?
    warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
===================================== 1 passed, 1 warning in 0.08s ======================================

테스트 코드 작성

pytest는 다양한 Test case 코드를 만들 수 있지만, 가장 많이 쓰이는 JUnit과 unittest 방식 Fixture 함수를 사용해보자.

Test code는 함수형으로도 만들 수 있고, Class 형식으로도 작성 가능하다


Function Test

1. 코드 상단에 pytest 모듈을 import

2. texture 함수를 구현. 각각의 test case 함수를 실행 "전"에 setup_function(function)가 pytest에 의해서 호출되고, 실행 후에는 teardown_function(function) 호출된다. 함수 인자로 전달되는 function은 각 test case의 함수 object를 전달

3. tecase 함수를 구현. 테스트 함수는 각각 독립적으로 테스트할 수 있도록 구현해야 한다. 테스트 함수의 이름은 test_OOO으로 시작해야 하고, assert()를 통해서 해당 test의 성공과 실패를 판단

import pytest,sys,logging
import os 

# ---------------------------------------------
# Function
# ---------------------------------------------
def remove_blank(text: str) -> str:
    return text.replace(' ','')

def remove_new_line(text: str) -> str:
    return text.replace('\n','')

def add_semicolon(text: str) -> str:
    return text + ';'

# ---------------------------------------------
# Function Test
# ---------------------------------------------
def setup_function(function) -> None:
    with open ('blank.txt','w') as fp:
        fp.write('he llo')
    
    with open('new_line.txt','w') as fp:
        fp.write('he\nllo')
    
    with open('add_semi.txt','w') as fp:
        fp.write('hello')

def teardown_function(function) -> None:
    os.remove('blank.txt')    
    os.remove('new_line.txt')
    os.remove('add_semi.txt')

def test_remove_blank() -> None:
    with open('blank.txt','r') as f:
        text = f.read()    
    assert ' ' not in remove_blank(text=text)

def test_remove_new_line() -> None:
    with open('new_line.txt','r') as f:
        text = f.read()
    
    assert '\n' not in remove_new_line(text=text)

def test_add_semicolon() -> None:
    with open('add_semi.txt','r') as f:
        text = f.read()
    
    assert add_semicolon(text=text) == 'hello;'

실행 결과는 아래와 같다

pytest -v test_true.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
rootdir: /Users/jaehyolee/git/github-action
plugins: Faker-15.3.4
collected 3 items

test_true.py::test_remove_blank PASSED                                                            [ 33%]
test_true.py::test_remove_new_line PASSED                                                         [ 66%]
test_true.py::test_add_semicolon PASSED                                                           [100%]

=========================================== 3 passed in 0.04s ===========================================

Class Test

class로 Test code를 구현하는 방법은 이전에 설명한 unittest와 거의 동일합니다. Class 생성과 소멸 시 생성되는 fixture함수와 method 단위의 fixture 함수를 구현할 수 있고, Class 이름은 반드시 "Test"로 시작해야 합니다.

1. Testcase를 구현한 class 선언: class 이름은 Test로 시작해야 합니다.

2. Class Level fixture함수 정의 : @classmethod decorator를 사용하고, 함수 인자로 cls를 전달받아서 class 변수를 생성하거나 접근할 수 있습니다. class level fixture는 class 생성과 소멸 시 1회만 호출되고, setup_class(), teardown_class() 함수명을 사용해야 합니다.

3. Method level texture 함수 구현: 각 test case 함수 실행 실행 전후로 실행하는 함수입니다. setup_method()와 teardown_method() 이름을 사용해야 합니다.

  1. Test case 함수 구현: 각 Test case는 별도로 test 가능하도록 독립적으로 구성해야 하고, test_OOO 이름 형식으로 함수 이름을 만들어야 합니다. Test case의 성공과 실패 여부는 assert() 함수를 사용할 수 있습니다.
import pytest,sys,logging
import os 

class Remove:        
    def remove_blank(self,text: str) -> str:
        return text.replace(' ','')

    def remove_new_line(self,text: str) -> str:
        return text.replace('\n','')

class Add:
    def add_semicolon(self,text: str) -> str:
        return text + ';'

# ---------------------------------------------
# Class
# ---------------------------------------------
class TestClassSample:
    @classmethod
    def setup_class(cls) -> None:
        cls.remove : Remove = Remove()
        cls.add : Add = Add()
    
    @classmethod
    def teardown_class(cls) -> None:
        del cls.remove
        del cls.add
    
    def setup_method(self,method) -> None:
        with open ('blank.txt','w') as fp:
            fp.write('he llo')
        
        with open('new_line.txt','w') as fp:
            fp.write('he\nllo')
        
        with open('add_semi.txt','w') as fp:
            fp.write('hello')        
        logging.info(sys._getframe(0).f_code.co_name)
    
    def teardown_method(self,method) -> None:
        os.remove('blank.txt')    
        os.remove('new_line.txt')
        os.remove('add_semi.txt')
        logging.info(sys._getframe(0).f_code.co_name)
    
    def test_remove_blank(self) -> None:
        with open('blank.txt','r') as f:
            text = f.read()        
        assert ' ' not in self.remove.remove_blank(text=text)

    def test_remove_new_line(self) -> None:
        with open('new_line.txt','r') as f:
            text = f.read()        
        assert '\n' not in self.remove.remove_new_line(text=text)
    
    def test_add_semicolon(self) -> None:
        with open('add_semi.txt','r') as f:
            text = f.read()        
        assert '\n' not in self.add.add_semicolon(text=text)

실행 결과는 아래와 같다

========================================== 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
rootdir: /Users/jaehyolee/git/github-action
plugins: Faker-15.3.4
collected 3 items

test_true.py::TestClassSample::test_remove_blank PASSED                                           [ 33%]
test_true.py::TestClassSample::test_remove_new_line PASSED                                        [ 66%]
test_true.py::TestClassSample::test_add_semicolon PASSED                                          [100%]

=========================================== 3 passed in 0.04s ===========================================

Test case Skip

특정 test case를 skip 하기 위해서는 @pytest.mark.skip() decorator를 사용할 수 있습니다. skip 조건을 추가해 특정 test를 skip 할 수도 있읍니다 ..

@pytest.mark.skip(reason="그냥 스킵하고싶으니까")
def test_sample(self) -> None:
    logging.info(sys._getframe(0).f_code.co_name)
    assert (True)

Pytest Logging

pytest로 test case를 구동하면 기본 값으로 print()의 출력을 확인이 안 됩니다.

Test case를 구현한 함수나 class에서 import logging을 하고 log level에 따라서 logging을 할 수 있습니다.

Log 출력에 대한 설정은 pytest 실행 시 logging 옵션을 사용할 수동 수 있으며 pytest.ini 파일에 log 옵션을 추가할 수 있습니다.

import logging

logging.info(sys._getframe(0).f_code.co_name)

pytest.ini

[pytest]
log_cli=true
log_cli_level=DEBUG
log_cli_date_format=%Y-%m-%d %H:%M:%S
log_cli_format=%(levelname)-8s %(asctime)s %(name)s::%(filename)s:%(funcName)s:%(lineno)d: %(messages)s

pytest html report

pytest의 가장 큰 장점 중에 하나는 각종 plugin을 직접 구현하거나 추가할 수 있습니다.

Html report 기능도 plug-in을 통해서 쉽게 적용할 수 있습니다.

pytest-html을 설치하고 Pytest 명령어로 pytest --html=report/report.html 옵션을 추가하면 생성합니다.

pip install pytest-html

pytest --html=report/report.html sample_pytest.py

HTML report 결과는 아래와 같다

image



참조 사이트


Loading script...