PyTest 사용을 위한 13가지 팁

단위 테스트는 소프트웨어 개발에서 정말 중요한 기술입니다. Nose 및 Unittest 와 같은 단위 테스트를 작성하고 실행하는 데 도움이 되는 훌륭한 Python 라이브러리가 있습니다 . 하지만 내가 가장 좋아하는 것은 PyTest 입니다.
나는 최근에 PyTest의 기능에 대해 더 깊이 배우기 위해 PyTest의 설명서를 자세히 읽었습니다.
다음은 내가 유용하다고 생각하고 내 자신의 테스트 워크플로에 통합하기 시작할 몇 가지 모호한 기능의 목록입니다. 이 목록에 당신이 아직 알지 못했던 새로운 것이 있기를 바랍니다…
이 게시물의 모든 코드 스니펫은 e4ds-snippets GitHub 리포지토리 에서 사용할 수 있습니다 .
테스트 작성을 위한 일반적인 팁
1. 좋은 단위 테스트를 작성하는 방법
자, 이것은 PyTest 라이브러리에만 국한되지 않지만 첫 번째 팁은 단위 테스트 구성에 대한 PyTest 문서 를 살펴보는 것입니다. 빠르게 읽을 가치가 있습니다.
좋은 테스트는 특정 예상 동작을 확인하고 다른 코드와 독립적으로 실행할 수 있어야 합니다. 즉, 테스트 내에는 테스트할 동작을 설정하고 실행하는 데 필요한 모든 코드가 있어야 합니다.
이렇게 4단계로 요약할 수 있습니다.
- 정렬 — 테스트에 필요한 설정을 수행합니다. 예: 정의 입력
- Act — 테스트하려는 기능을 실행합니다.
- Assert — 함수의 출력이 예상대로인지 확인
- 정리 — (선택 사항) 테스트에서 생성된 모든 아티팩트를 정리합니다. 예: 출력 파일.
# example function
def sum_list(my_list):
return sum(my_list)
# example test case
def test_sum_list():
# arrange
test_list = [1, 2, 3]
# act
answer = sum_list(test_list)
# Assert
assert answer == 6
https://docs.pytest.org/en/7.1.x/explanation/anatomy.html
2. 테스트 예외
일반적으로 테스트에 대해 가장 먼저 생각하는 것은 함수가 성공적으로 실행될 때 예상되는 출력입니다.
그러나 예외가 발생할 때 함수의 동작을 확인하는 것도 중요합니다. 특히 어떤 유형의 입력이 특정 예외를 발생시켜야 하는지 알고 있는 경우.
컨텍스트 관리자 를 사용하여 예외를 테스트 할 수 있습니다 .pytest.raises
예를 들어:
import pytest
def divide(a, b):
"""Divide to numbers"""
return a/b
def test_zero_division():
with pytest.raises(ZeroDivisionError):
divide(1,0)
def test_type_error():
with pytest.raises(TypeError):
divide("abc",10)
3. 테스트 로깅/인쇄
PyTest를 사용하면 코드에서 인쇄 및 로깅 문을 테스트할 수 있습니다.
두 개의 내장된 PyTest 픽스처, capsys 및 caplog 가 있으며, 함수에 의해 터미널에 인쇄된 정보를 추적하는 데 사용할 수 있습니다.
테스트 인쇄 출력
def printing_func(name):
print(f"Hello {name}")
def test_printing_func(capsys):
printing_func(name="John")
# use the capsys fixture to record terminal output
output = capsys.readouterr()
assert output.out == "Hello John\n"
테스트 로깅 출력
import logging
def logging_func():
logging.info("Running important function")
# some more code...
logging.info("Function completed")
def test_logging_func(caplog):
# use the caplog fixture to record logging records
caplog.set_level(logging.INFO)
logging_func()
records = caplog.records
# first message
assert records[0].levelname == 'INFO'
assert records[0].message == "Running important function"
# second message
assert records[1].levelname == 'INFO'
assert records[1].message == "Function completed"
4. 플로트 테스트
float와 관련된 산술 연산 은 Python에서 문제를 일으킬 수 있습니다 .
예를 들어, 이 간단한 함수는 이상한 오류를 일으킵니다.
def subtract_floats(a,b):
return a - b
def test_substract_floats():
assert subtract_floats(1.2, 1.0) == 0.2
함수의 논리에는 아무런 문제가 없으며 이 테스트 사례에 실패해서는 안 됩니다.
테스트에서 부동 소수점 반올림 오류를 방지하려면 approx 함수 를 사용할 수 있습니다.
import pytest
def test_substract_floats():
assert subtract_floats(1.2, 1.0) == pytest.approx(0.2)
approx
numpy 배열에도 함수를 적용할 수 있습니다 . 이는 배열과 데이터 프레임을 비교할 때 유용합니다.
예를 들어:
import pytest
import numpy as np
np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6]))
시간을 절약할 수 있는 팁⏳
5. 특정 테스트만 실행하여 시간 절약
테스트 실행은 워크플로에 도움이 되어야 하며 장애물이 되어서는 안 됩니다. 오래 실행되는 테스트 스위트는 속도를 늦추고 정기적으로 테스트를 실행하지 못하게 할 수 있습니다.
일반적으로 변경할 때마다 전체 테스트 스위트를 실행할 필요가 없습니다. 특히 코드베이스의 작은 부분에서만 작업하는 경우에는 더욱 그렇습니다.
따라서 작업 중인 코드와 관련된 테스트의 하위 집합을 실행할 수 있으면 편리할 수 있습니다.
PyTest에는 실행할 테스트를 선택할 수 있는 몇 가지 옵션이 있습니다.
-k
플래그 사용
-k
주어진 하위 문자열과 일치하는 테스트만 실행하기 위해 PyTest를 실행할 때 플래그 를 사용할 수 있습니다 .
예를 들어 다음과 같은 테스트가 있었다면:
def test_preprocess_categorical_columns():
...
def test_preprocess_numerical_columns():
...
def test_preprocess_text():
...
def test_train_model():
...
# run first test only
pytest -k categorical
# run first three tests only
pytest -k preprocess
# run first two tests only
pytest -k "preprocess and not text"
단일 테스트 파일에서 테스트 실행
테스트가 여러 파일로 분할된 경우 PyTest를 실행할 때 파일 이름을 명시적으로 전달하여 단일 파일에서 테스트를 실행할 수 있습니다.
# only run tests defined in 'tests/test_file1.py' file
pytest tests/test_file1.py
pytest '마커'를 사용하여 특정 테스트를 표시할 수도 있습니다. 이는 -m
플래그로 제외할 수 있는 '느린' 테스트를 표시하는 데 유용할 수 있습니다.
예를 들어.
import time
import pytest
def my_slow_func():
# some long running code...
time.sleep(5)
return True
@pytest.mark.slow
def test_my_slow_func():
assert my_slow_func()
데코레이터를 사용한 후 플래그 @pytest.mark.slow
를 사용하여 매번 이 테스트 실행을 제외할 수 있습니다 .-m
# exclude running tests marked as slow
pytest -m "not slow"
import sys
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function():
...
6. 실패한 테스트만 재실행
전체 테스트 스위트를 실행하면 적은 수의 테스트가 실패했음을 알 수 있습니다.
문제를 디버깅하고 코드를 업데이트하면 전체 테스트 스위트를 다시 실행하는 대신 --lf
플래그를 사용하여 마지막 실행에서 실패한 테스트만 실행할 수 있습니다.
전체 테스트 스위트를 다시 실행하기 전에 업데이트된 코드가 이러한 테스트를 통과하는지 확인할 수 있습니다.
# only run tests which failed on last run
pytest --lf
# run all tests but run failed tests first
pytest --ff
코드 작성을 줄여주는 팁
7. 매개변수화 테스트
특정 함수에 대한 여러 입력을 테스트하려는 경우 사람들이 테스트 함수 내에서 여러 assert 문을 작성하는 것이 일반적입니다. 예를 들어:
def remove_special_characters(input_string):
return re.sub(r"[^A-Za-z0-9]+", "", input_string)
def test_remove_special_characters():
assert remove_special_characters("hi*?.") == "hi"
assert remove_special_characters("f*()oo") == "foo"
assert remove_special_characters("1234bar") == "bar"
assert remove_special_characters("") == ""
import pytest
@pytest.mark.parametrize(
"input_string,expected",
[
("hi*?.", "hi"),
("f*()oo", "foo"),
("1234bar", "1234bar"),
("", ""),
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
8. 독스트링에서 테스트 실행
또 다른 멋진 트릭은 독스트링에서 직접 테스트를 정의하고 실행하는 것입니다.
다음과 같이 문서 문자열에서 테스트 사례를 정의할 수 있습니다.
def add(a, b):
"""Add two numbers
>>> add(2,2)
4
"""
return a + b
pytest --doctest-modules
나는 이것이 입력 및 출력으로 '단순한' 데이터 구조를 가진 함수에 대해 잘 작동한다는 것을 알았습니다. 오히려 테스트 스위트에 더 많은 코드를 추가하는 완전한 테스트를 작성하는 것보다.
https://docs.pytest.org/en/7.1.x/how-to/doctest.html#how-to-run-doctests
9. 붙박이 pytest 비품
PyTest에는 매우 유용할 수 있는 여러 내장 고정물이 포함되어 있습니다.
팁 번호 3(capsys 및 caplog)에서 이러한 고정 장치 몇 가지를 간략하게 다루었지만 전체 목록은 여기에서 찾을 수 있습니다.https://docs.pytest.org/en/stable/reference/fixtures.html#built-in-fixtures
이러한 픽스처는 테스트 함수에 인수로 추가하기만 하면 테스트에서 액세스할 수 있습니다.
내 생각에 가장 유용한 내장형 고정 장치 중 두 가지는 request
고정 장치와 tmp_path_factory
고정 장치입니다.
여기 에서 매개 변수화된 테스트에서request
고정 장치를 사용하기 위해 고정 장치 사용에 대한 내 기사를 확인할 수 있습니다 .
픽스처 는 tmp_path_factory
테스트 실행을 위한 임시 디렉토리를 만드는 데 사용할 수 있습니다. 예를 들어 특정 디렉토리에 파일을 저장해야 하는 기능을 테스트하는 경우입니다.
https://docs.pytest.org/en/stable/reference/fixtures.html#built-in-fixtures
https://docs.pytest.org/en/7.1.x/how-to/tmp_path.html#the-tmp-path-factory-fixture
디버깅에 도움이 되는 팁
10. 테스트의 상세도를 높입니다.
PyTest의 기본 출력은 매우 작을 수 있습니다. 테스트가 실패하는 경우 터미널 출력에 제공되는 정보의 양을 늘리는 것이 도움이 될 수 있습니다.
자세한 정보 표시 플래그를 사용하여 추가할 수 있습니다.-vv
# increase the amount of information provided by PyTest in the terminal output
pytest -vv
11. 테스트 기간 표시
테스트 도구 모음을 실행하는 데 시간이 오래 걸리는 경우 실행하는 데 가장 오래 걸리는 테스트를 이해하고 싶을 수 있습니다. 그런 다음 이러한 테스트를 시도하고 최적화하거나 위에서 설명한 것처럼 마커를 사용하여 테스트를 제외할 수 있습니다.
--durations
플래그 를 사용하여 실행하는 데 가장 오랜 시간이 걸리는 테스트를 찾을 수 있습니다 .
또한 전체 기간 보고서를 표시하려면 자세한 표시 플래그를 전달해야 합니다.
# show top 5 longest running tests
pytest --durations=5 -vv
12. 코드에 인쇄 문의 출력 표시
때때로 함수 디버깅을 돕기 위해 소스 코드에 인쇄 문이 있을 것입니다.
기본적으로 Pytest는 테스트가 통과하면 이러한 인쇄 문의 출력을 표시하지 않습니다.
-rP
플래그 를 사용하여 이 동작을 재정의할 수 있습니다 .
def my_function_with_print_statements():
print("foo")
print("bar")
return True
def test_my_function_with_print_statements():
assert my_function_with_print_statements()
# run tests but show all printed output of passing tests
pytest -rP
13. 매개변수화된 테스트에 ID 할당
매개변수화된 테스트를 실행할 때 발생할 수 있는 한 가지 잠재적인 문제는 모두 터미널 출력에 동일한 이름으로 표시된다는 것입니다. 기술적으로 다른 행동을 테스트하고 있지만.
매개변수화된 테스트에 ID를 추가하여 각 매개변수화된 테스트를 식별하는 데 도움이 되는 고유한 이름을 지정할 수 있습니다. 또한 테스트하려는 대상에 대해 명시할 수 있으므로 테스트의 가독성이 높아집니다.
테스트에 ID를 추가하기 위한 두 가지 옵션이 있습니다.
옵션 1: id
인수
팁 번호 7에서 매개변수화된 예제 재사용:
@pytest.mark.parametrize(
"input_string,expected",
[
("hi*?.", "hi"),
("f*()oo", "foo"),
("1234bar", "1234bar"),
("", ""),
],
ids=[
"remove_special_chars_from_end",
"remove_special_chars_from_middle",
"ignore_numbers",
"no_input",
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
또는 pytest.param
래퍼를 사용하여:
@pytest.mark.parametrize(
"input_string,expected",
[
pytest.param("hi*?.", "hi", id="remove_special_chars_from_end"),
pytest.param("f*()oo", "foo", id="remove_special_chars_from_middle"),
pytest.param("1234bar", "1234bar", id="ignore_numbers"),
pytest.param("", "", id="no_input"),
],
)
def test_remove_special_characters(input_string, expected):
assert remove_special_characters(input_string) == expected
https://docs.pytest.org/en/stable/example/parametrize.html#different-options-for-test-ids
결론
PyTest는 유용한 기능이 많은 훌륭한 테스트 프레임워크입니다. 문서는 일반적으로 매우 훌륭하며 더 많은 정보와 기타 훌륭한 기능을 찾아볼 것을 적극 권장합니다.
새로운 것을 배웠기를 바랍니다. PyTest 사용에 대한 다른 팁이 있는지 알고 싶습니다.
행복한 테스트!
이 기사는 원래 engineeringfordatascience.com 에 게시되었습니다.