본문 바로가기
SW 개발/Python

Python 자동 테스트를 위한 Pytest 사용법과 예제

by Kibua20 2021. 12. 12.

Python에서 Unittest의 가장 많이 사용하는 모듈은 PytestUnittest입니다. unittest는 python 기본 모듈로 설치되고 JUnit와 같은 형식으로  테스트 코드를 간단하게 작성할 수 있고,  Pytest는 unittest를 포함하여 다양한 형태의 texture 함수를 지원하고, 매우 다양한 옵션을 지원합니다.  또는 Pytest unittest 뿐 아니라 JUnit 등 다양한 테스트 framework을 호출할 수 있습니다.  Test case를 쉽게 작성하고자 하는 경우에는 unittest를 사용하고, 고도화된 Test를 만들고자 하는 경우 pytest를 추천드립니다.  

 

Pytest 설치

Python unnittest는 기본 모듈로 별도 설치가 필요 없지만, pytest는 pip를 통해서 모듈을 별도를 설치해야 합니다.  pytest를 설치 후 pytest --help 명령어를 확인하면 상세한 옵션을 확인할 수 있고, pytest [file or directory] 명령어를 통해서 Test case를 실행할 수 있습니다.  pytest-html 모듈을 동작에 필요한 필수 모듈은 아니지만 html 형식의 report 생성을 위해서 필요한 모듈입니다. Html report가 필요 없는 경우  pytest-html를 설치할 필요는 없습니다.  

 

$ pip3 install pytest pytest-html
$ pytest --help

usage: pytest [options] [file_or_dir] [file_or_dir] [...]

Pytest 설치

 



Test code 작성

pytest는 다양한 Test case 코드를 만들 수 있지만, 가장 많이 쓰이는 JUnit과 unittest 방식 Fixture 함수를 사용할 예정입니다.  Test code는 함수형으로 만들 수도 있고, Class형식으로도 작성 가능합니다. 

 

Function으로 Test  case 구현

함수를 사용하는 테스트 코드는 다음과 같습니다.

  1. 코드 상단에 pytest 모듈을 import 
  2. texture 함수를 구현합니다.  각각의 test case 함수를 실행 "전"에 setup_function(function)가 pytest에 의해서 호출되고, 실행 후에는 teardown_function(function)호출됩니다.  함수 인자로 전달되는  function은 각 test case의 함수 object를 전달합니다. 
  3. tecase 함수를 구현합니다. 테스트 함수는 각각 독립적으로  테스트할 수 있도록 구현해야 합니다. 테스트 함수의 이름은 test_OOO으로 시작해야 해고, assert()를 통해서  해당 test의 성공과 실패를 판단합니다.   

Function으로 Test  case 구현

 

위의 test case를 실행하면 다음과 같습니다.   test_function_01() 실행 전과 후로 setup_function()과 teardown_function() 이 pytest에 호출됩니다. test_function_02()는 test_function_01() 다음에 동일한 방식으로 호출됩니다. 

Test case 실행 결과

Class로 Test  case 구현

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() 이름을 사용해야 합니다. 
  4. Test case 함수 구현:   각 Test case는 별도로 test 가능하도록 독립적으로 구성해야 하고, test_OOO 이름 형식으로 함수 이름을 만들어야 합니다.  Test  case의 성공과 실패 여부는 assert() 함수를 사용할 수 있습니다.  

Class로 Test  case 구현

 

TestClassSample class에서 test_0001, test_0002, test_0003을 구현한 결과입니다. assert()  결과에 따라서 성공과 실패 결과를 출력하고 assert(False)인 경우 상세 내용을 출력합니다. 

Class로 Test  case 실행 결과

 



Test  조건문 판단

unittest는 별도의   assert 구문을 사용하지만, Pytest는  python 기본 내장 함수인  assert()를 사용할 수 있습니다. 

 

Test case Skip

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

@pytest.mark.skip(reason="Skip reasson")
def test_0002(self):
    logging.info(sys._getframe(0).f_code.co_name)
    assert (True)

 

Pytest Loggin 기능

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 logging 옵션
pytest logging - pytest.ini

 

Pytest의 결과를 html report 생성 하기

pytest의 가장 큰 장점 중에 하나는 각종 plugin을 직접 구현하거나 추가할 수 있습니다.  Html report 기능도 plug-in을 통해서 쉽게 적용할 수 있습니다.   pytest-html을 설치하고 Pytest 명령어로  pytest --html=report/report.html  옵션을 추가하면 생성합니다. 

 

pip3 install pytest-html 

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

 

Html reprot 결과는 다음과 같습니다.  

html으로 report생성하기

 



html-report를 customization 하기 위해서는 conftest.py파일을 생성하고  hook 함수를 구현하여 변경할 수 있습니다. 

def pytest_html_report_title(report):
@pytest.mark.optionalhook
def pytest_html_results_summary(prefix, summary, postfix):
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
from datetime import datetime
from py.xml import html
import pytest
import sys
import logging

def pytest_html_report_title(report):
    ''' modifying the title  of html report'''
    report.title = "My PyTest Title"
    pass

@pytest.mark.optionalhook
def pytest_html_results_summary(prefix, summary, postfix):
    ''' modifying the summary in pytest environment'''
    # prefix.extend([html.h3("Adding prefix message")])
    # summary.extend([html.h3("Adding summary message")])
    # postfix.extend([html.h3("Adding postfix message")])
    pass    

def pytest_configure(config):
    ''' modifying the table pytest environment'''
    # print(sys._getframe(0).f_code.co_name)
    # # getting user name
    # from pwd import getpwuid
    # from os import getuid
    # username = getpwuid(getuid())[0]

    # # getting python version
    # from platform import python_version
    # py_version = python_version()
    # # overwriting old parameters with  new parameters
    # config._metadata =  {
    #     "user_name": username,
    #     "python_version": py_version,
    #     "date": "오늘"
    # }
    pass
    

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    pytest_html = item.config.pluginmanager.getplugin('html')
    outcome = yield
    report = outcome.get_result()
    extra = getattr(report, 'extra', [])
    if report.when == 'call':
        # always add url to report
        # extra.append(pytest_html.extras.url('./assets/image.png'))
        #extra.append(pytest_html.extras.text(item.name))

        # extra.append(pytest_html.extras.text('some string', name='Different title'))
        xfail = hasattr(report, 'wasxfail')
        if (report.skipped and xfail) or (report.failed and not xfail):
            # only add additional html on failure
            extra.append(pytest_html.extras.html('<div>Additional HTML</div>'))
        report.extra = extra

 

__main__함수에서 test case 실행 및 My plug in 작성

터미널에서 pytest를 실행하는 방법도 있지만,  py  소스 코드 내 __main__ 함수에서 실행하는 하는 방법은 pytest.main() 호출하는 것입니다. args에  pytest 명령어 인자를 넣고, plugin  변수에 My Plugin을 설정할 수 있습니다.  plugin 작성 방법은 링크를 확인해주세요.

 

class MyPlugin:
    def pytest_sessionfinish(self):
        pass

if __name__ == "__main__":
    # args_str = '--html=report/report.html --self-contained-html '+ __file__
    # args_str = ' --capture=tee-sys '+ __file__
    args_str = '--html=report/report.html ' + __file__

     args = args_str.split(" ")
     pytest.main(args, plugins=[MyPlugin()])
 

Sample code 

앞에서  설명한 내용을 모두 정리한 코드는 다음과 같습니다.  pytest.init 파일과 conftest.py파일도 GitHub에 반영하였습니다. GitHub에 전체 파일을 확인해주세요. 

 

import pytest, sys, logging 
from time import sleep

# ------------------------------------------------------------------------------------------
# Function Test
# ------------------------------------------------------------------------------------------
def setup_function(function):
    """ setup any state tied to the execution of the given function.
    Invoked for every test function in the module.
    """
    logging.info(sys._getframe(0).f_code.co_name)

def teardown_function(function):
    """ teardown any state that was previously setup with a setup_function
    call.
    """
    logging.info(sys._getframe(0).f_code.co_name)

def test_function_01():
    """ Test Function"""
    logging.info(sys._getframe(0).f_code.co_name)
    assert (True)

def test_function_02():
    """ Test Function"""
    logging.info(sys._getframe(0).f_code.co_name)
    assert (True)


# ------------------------------------------------------------------------------------------
# Class Test
# ------------------------------------------------------------------------------------------
class TestClassSample():
    @classmethod
    def setup_class(cls):
        """ setup any state specific to the execution of the given class (which usually contains tests)."""
        logging.info(sys._getframe(0).f_code.co_name)
        cls.name= 'test'
        cls.members = [1, 2, 3, 4]

    @classmethod
    def teardown_class(cls):
        """ teardown any state that was previously setup with a call to setup_class."""
        logging.error(sys._getframe(0).f_code.co_name)
        pass


    def setup_method(self, method):
        """ setup any state tied to the execution of the given method in a class.  
        setup_method is invoked for every test method of a class.
        """
        logging.info(sys._getframe(0).f_code.co_name)

    def teardown_method(self, method):
        """ teardown any state that was previously setup with a setup_method call.
        """
        logging.info(sys._getframe(0).f_code.co_name)

    def test_0001(self):
        logging.info(sys._getframe(0).f_code.co_name)
        sleep(1)
        assert (True)

    @pytest.mark.skip(reason="Skip reasson")
    def test_0002(self):
        logging.info(sys._getframe(0).f_code.co_name)
        sleep(1)
        assert (True)

    def test_0003(self):
        logging.info(sys._getframe(0).f_code.co_name)
        sleep(1)
        assert (False)

class MyPlugin:
    def pytest_sessionfinish(self):
        pass

if __name__ == "__main__":
    # args_str = '--html=report/report.html --self-contained-html '+ __file__
    # args_str = ' --capture=tee-sys '+ __file__
    args_str = '--html=report/report.html ' + __file__

    args = args_str.split(" ")

    pytest.main(args, plugins=[MyPlugin()])
    # pytest.main(args)

 

관련 글

[SW 개발/Python] - Python 단위 테스트(Unit Test)를 위한 unittest 사용법과 예제

[SW 개발/Python] - Python Decorator를 이용한 함수 실행 시간 측정 방법 (Sample code)

[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Jupyter Notebook의 업그레이드: Jupyter Lab 설치 및 extension 사용법

[SW 개발/Python] - Python 여러 버전 설치 방법 (3.x and 3.y 동시 설치)

[개발환경/Web Server] - Web Server 성능 및 Load 측정 Tool: Apache AB (Apache Http Server Benchmarking Tool)

[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Python Dash를 활용한 Web App 구현 및 시계열 데이터 Visualization (Sample code)

[SW 개발/Python] - Python 가상환경(Virtual Environment) 만들기 위한 virtualenv 명령어 및 실행 예제

[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Python Dash를 활용한 Web App 구현 및 시계열 데이터 Visualization (Sample code)

[SW 개발/Python] - Python: JSON 개념과 json 모듈 사용법

 




댓글