Pytest 매개 변수화

완료됨

데코레이터를 @pytest.mark.parametrize() 사용하면 하나의 테스트 함수가 서로 다른 입력 값으로 여러 번 실행할 수 있습니다. 어설션 논리가 동일하지만 예제가 다를 때 사용합니다. 매개 변수화는 반복을 줄일 수 있으며 pytest가 각 입력을 자체 테스트 항목으로 보고할 수 있습니다.

데코레이터는 보통 두 개의 필수 인수를 필요로 합니다.

  • argnames: 테스트 함수에 전달할 인수의 이름을 가진 쉼표로 구분된 문자열 또는 문자열 목록 또는 튜플입니다. 예를 들어 "item", , "test_input, expected"["test_input", "expected"]("test_input", "expected")가 있습니다.
  • argvalues: 해당 인수에 사용할 값 또는 매개 변수 집합의 반복 가능입니다. 하나의 인수 이름을 사용하면 해당 값이 튜플인 경우에도 각 항목이 하나의 테스트 값으로 전달됩니다. 인수 이름이 여러 개인 경우 각 항목은 각 인수 이름에 대해 일반적으로 튜플, 목록 또는 pytest.param(...) 호출로 하나의 값을 제공해야 합니다.

선택적 ids 인수를 사용하면 pytest가 생성된 각 테스트 항목에 레이블을 지정하는 데 사용하는 테스트 ID를 사용자 지정할 수 있습니다. 고급 옵션(예: indirectscope및)은 매개 변수가 있는 비품 또는 고가의 리소스와 같은 경우에 유용합니다. pytest의 매개 변수 분석 방법@pytest.mark.parametrize 참조에서 해당 옵션에 대해 읽을 수 있습니다.

Note

Pytest는 매개 변수 값을 as-is테스트에 전달합니다. 복사하지 않습니다. 테스트가 목록 또는 사전과 같은 변경 가능한 매개 변수 값을 변경하면 동일한 개체를 수신하는 나중에 생성된 사례에서 변경이 표시될 수 있습니다.

매개 변수를 사용하는 경우

사용하고자 할 경우의 @pytest.mark.parametrize()의 두 가지 일반적인 시나리오는 다음과 같습니다.

  • 테스트가 입력을 반복하고 동일한 어설션을 반복하는 경우
  • 여러 테스트 함수가 서로 다른 입력으로 동일한 동작을 어설션하는 경우

각 예제를 먼저 @pytest.mark.parametrize()없이 검토한 후, 이를 사용하여 테스트를 더 쉽게 유지 관리하고 진단할 수 있는 방법을 보여 드리겠습니다.

loops의 경우

루프가 있는 테스트 함수의 예는 다음과 같습니다.for

def test_string_is_digit():
    items = ["1", "10", "33"]
    for item in items:
        assert item.isdigit()

한 입력이 실패하면 이 패턴을 진단하기 어려울 수 있습니다.

  • 불완전한 피드백: 첫 번째 실패한 어설션은 루프를 중지하므로 이후 값은 해당 실행에서 평가되지 않습니다.
  • 단일 테스트 결과: Pytest는 입력당 하나의 테스트 사례가 아닌 하나의 수집된 테스트를 보고합니다.
  • 반복된 다시 실행: 첫 번째 오류를 수정하면 테스트를 다시 실행한 후에만 다른 오류가 표시될 수 있습니다.

어설션에 실패하는 두 개의 항목을 포함하도록 테스트를 수정해 보겠습니다.

def test_string_is_digit():
    items = ["No", "1", "10", "33", "Yes"]
    for item in items:
        assert item.isdigit()

목록에 두 개의 잘못된 항목이 있더라도 테스트를 실행하면 첫 번째 실패만 표시됩니다. 명령 예제는 Python을 python로 사용할 수 있을 때 python -m pytest를 사용합니다. 셸이 macOS 또는 Linux에서 python3 사용하거나 Windows py 사용하는 경우 해당 명령을 대체합니다.

python -m pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________

    def test_string_is_digit():
        items = ["No", "1", "10", "33", "Yes"]
        for item in items:
>           assert item.isdigit()
E           AssertionError: assert False
E            +  where False = <built-in method isdigit of str object at 0x...>()
E            +    where <built-in method isdigit of str object at 0x...> = 'No'.isdigit

test_items.py:4: AssertionError
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Pytest의 어설션 검토 로그에는 바운드 str.isdigit 메서드와 오류를 트리거한 문자열 값이 표시됩니다. 세션 헤더, 플랫폼 세부 정보, 메모리 주소 및 실행 시간은 다를 수 있으므로, 예제에서는 ...0x... 태그로 이들을 생략하거나 줄입니다. 관련 세부 정보는 루프가 첫 번째 오류('No')에서 중지되고 평가되지 않는다는 것입니다 'Yes'.

이는 @pytest.mark.parametrize()에 유용한 사례입니다. 테스트를 업데이트하기 전에 루프를 포함하지 for 않는 또 다른 일반적인 상황을 살펴보겠습니다.

동일한 동작을 어설션하는 테스트

동일한 어설션을 만드는 테스트 그룹도 적합한 후보 @pytest.mark.parametrize()입니다. 이전 테스트를 각 항목에 대해 하나의 테스트로 다시 작성한 경우 더 나은 실패 보고를 허용하지만 반복적입니다.

def test_is_digit_1():
    assert "1".isdigit()

def test_is_digit_10():
    assert "10".isdigit()

def test_is_digit_33():
    assert "33".isdigit()

이러한 테스트는 오류가 단일 입력과 연결될 수 있기 때문에 더 세분화됩니다. 몇 가지 유사한 테스트가 있는 것은 드문 일이지만, 이 패턴은 프로덕션 테스트 도구 모음에서 오류를 정확하게 보고하려고 하는 경우에 일반적입니다.

그러나 반복 테스트에는 다음과 같은 문제가 발생합니다.

  • 코드는 반복적이므로 유지 관리 부담이 발생합니다.
  • 유사한 함수는 일관되지 않게 쉽게 업데이트할 수 있습니다.
  • 새 입력을 추가하려면 다른 테스트 본문을 복사해야 하므로 중요한 사례를 간과할 수 있습니다.

매개 변수화는 동일한 테스트 본문을 복사하지 않고 별도의 테스트 항목의 보고 이점을 유지합니다.

매개 변수를 사용하는 방법

이제 몇 가지 사용 사례를 알았으니, 실패한 항목을 포함한 for 루프를 사용한 테스트를 업데이트해 보겠습니다.

가져온 pytest을 테스트 함수 바로 위에 @pytest.mark.parametrize()을 적용합니다.

import pytest

@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
    assert item.isdigit()

테스트를 실행하기 전에 변경 내용을 살펴보겠습니다.

이 예제에서 데코레이터는 두 개의 인수를 받습니다. 첫 번째 인수인 "item"pytest가 테스트 함수에 전달하는 인수의 이름을 지정합니다. 이름은 .의 item매개 변수와 test_string_is_digit(item) 일치해야 합니다. 두 번째 인수는 pytest가 생성된 테스트 사례에 사용하는 값 목록입니다.

상세한 오류 보고

백그라운드에서 pytest는 매개 변수 목록의 각 값에 대해 하나의 테스트 항목을 수집합니다. 즉, 통과 및 실패 사례는 별도로 보고됩니다. 테스트를 실행할 때 어떤 일이 발생하는지 살펴보겠습니다.

python -m pytest test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py F...F                                                      [100%]

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________

item = 'No'

    @pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
    def test_string_is_digit(item):
>       assert item.isdigit()
E       AssertionError: assert False
E        +  where False = <built-in method isdigit of str object at 0x...>()
E        +    where <built-in method isdigit of str object at 0x...> = 'No'.isdigit

test_items.py:5: AssertionError
__________________________ test_string_is_digit[Yes] ___________________________

item = 'Yes'

    @pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
    def test_string_is_digit(item):
>       assert item.isdigit()
E       AssertionError: assert False
E        +  where False = <built-in method isdigit of str object at 0x...>()
E        +    where <built-in method isdigit of str object at 0x...> = 'Yes'.isdigit

test_items.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================

테스트 보고서에는 몇 가지 주목할 만한 항목이 있습니다. 먼저 pytest는 단일 테스트 함수에서 수집된 항목 5개(통과 3개 및 실패 2개)를 보고합니다. 각 오류를 발생시킨 입력 값을 포함하여 오류가 별도로 보고됩니다. 헤더 test_string_is_digit[No] 에 매개 변수 집합이 표시되고 매개 변수 로컬 item = 'No' 이 원본 뷰 위에 표시됩니다. 바운드 메서드 조사 줄(메모리 주소가 0x...으로 표시됨)은 어설션 실패를 유발한 값을 확인합니다.

___________________________ test_string_is_digit[No] ___________________________

item = 'No'
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

보고되는 위치가 너무 많아 실패를 일으킨 값을 놓치기 어렵습니다.

자세한 정보 표시 출력 플래그 사용

테스트가 통과하면 명령줄 보고는 기본적으로 최소화됩니다. 업데이트를 통해 오류 수정을 완료한 후 테스트가 다음과 같이 보일 것입니다 (동일한 test_items.py를 계속 편집하여 import pytest가 이미 존재함).

@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
    assert item.isdigit()

테스트를 실행하면 최소 출력이 생성됩니다.

python -m pytest test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py .....                                                      [100%]

============================== 5 passed in 0.01s ===============================

세부 정보를 늘리면 생성된 각 테스트 사례와 생성된 매개 변수 ID가 표시됩니다.

python -m pytest -v test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py::test_string_is_digit[0] PASSED                            [ 20%]
test_items.py::test_string_is_digit[1] PASSED                            [ 40%]
test_items.py::test_string_is_digit[10] PASSED                           [ 60%]
test_items.py::test_string_is_digit[33] PASSED                           [ 80%]
test_items.py::test_string_is_digit[9] PASSED                            [100%]

============================== 5 passed in 0.01s ===============================

여러 인수 이름을 사용하는 방법

지금까지 본 예제에서는 하나의 인수 이름을 "item"사용합니다. 첫 번째 인수에 여러 인수 이름을 포함할 수도 있습니다. 문자열을 사용하는 경우 이름을 쉼표로 구분합니다. 이름을 목록이나 튜플(예: ["item", "attribute"] 또는 ("item", "attribute").)으로 전달할 수도 있습니다.

여러 인수 이름에 대한 한 가지 사용 사례는 입력 값과 이에 대해 테스트할 예상 값을 전달하는 것입니다. 두 번째 인수에서 각 매개 변수 집합에는 각 인수 이름에 대해 하나의 값이 필요합니다. 예를 들어 인수 이름이 "test_input, expected_value"면 인수 값이 될 [("3+5", 8), ("3*4", 12)]수 있습니다.

다음 예제에서는 개체에 명명된 특성이 있는지 여부에 따라 부울 값을 반환하는 Python hasattr() 함수를 사용합니다.

>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False

각 테스트 사례에는 개체와 특성 이름이 필요하므로 다음과 같은 방식으로 사용할 @pytest.mark.parametrize() 수 있습니다. 동일한 test_items.py 파일에서 계속 진행하므로 import pytest 이미 있습니다.

@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

데코레이터는 @pytest.mark.parametrize() 여전히 첫 번째 인수에 단일 문자열을 사용하지만, 이제 해당 문자열에는 쉼표로 구분된 두 개의 인수 이름이 포함됩니다. 이름은 테스트 함수의 인수가 됩니다. 이 경우 다음과 같습니다 itemattribute. 이름 목록 또는 튜플은 동일한 방식으로 작동합니다.

두 번째 인수는 두 개의 매개 변수 집합이 있는 목록입니다. 각 매개 변수 집합에는 하나의 item 값과 하나의 attribute 값이 포함됩니다. 매개 변수 집합에 인수 이름에 대한 값이 너무 적거나 너무 많은 경우 테스트 사례를 실행하기 전에 pytest에서 컬렉션 오류가 발생합니다.

Pytest는 각 매개 변수 집합에 대한 테스트 ID를 빌드합니다. 숫자, 문자열, 불리언, None과 같은 단순한 값의 경우 pytest는 해당 값의 일반적인 문자열 표현을 사용합니다. 다른 개체의 경우, pytest는 인수 이름과 매개 변수 집합의 0부터 시작하는 인덱스를 기반으로 한 이름을 사용합니다. 이 출력에서 빈 문자열은 빈 ID 부분을 발생하므로 두 ID 사이의 대시 구분 기호는 계속 표시됩니다 [-format]. 간단한 생성된 ID가 없으므로 pytest는 list()item1로 대체합니다.

python -m pytest -v test_items.py::test_attributes
============================= test session starts ==============================
...
collected 2 items

test_items.py::test_attributes[-format] PASSED                           [ 50%]
test_items.py::test_attributes[item1-append] PASSED                      [100%]

============================== 2 passed in 0.01s ===============================

생성된 ID를 읽기 어려운 경우 선택적 ids 인수를 전달하거나 매개 변수 집합을 pytest.param(..., id="name") 래핑하여 더 명확한 이름을 제공할 수 있습니다. 다음은 그 예입니다.

import pytest


@pytest.mark.parametrize(
    "item, attribute",
    [
        pytest.param("", "format", id="empty-string-format"),
        (list(), "append"),
    ],
)
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

이 호출은 pytest.param("", "format", id="empty-string-format") 해당 매개 변수 집합에 대한 전체 ID를 제공하고 자동 생성된 -format ID를 대체합니다. 생성된 테스트는 자세한 출력에서 test_attributes[empty-string-format]test_attributes[item1-append]로 명명됩니다. 명확한 ID는 자세한 출력 및 오류 메시지를 더 쉽게 확인할 수 있도록 만듭니다.