단위 테스트 알아보기
- 테스트 함수명은 자세하게 적는게 좋다.
테스트할 함수
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() 이름을 사용해야 합니다.
- 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
결과는 아래와 같다
참조 사이트
- 파이썬 자동 테스트를 위한 Pytest - https://kibua20.tistory.com/227