Как параметризировать приспособления и тестовые функции

pytest позволяет параметризовать тесты на нескольких уровнях:

  • @pytest.mark.parametrize позволяет определить несколько наборов аргументов и приспособлений в тестовой функции или классе.

  • pytest_generate_tests позволяет определить пользовательские схемы параметризации или расширения.

@pytest.mark.parametrize: параметризация тестовых функций

Встроенный декоратор pytest.mark.parametrize позволяет параметризовать аргументы тестовой функции. Вот типичный пример тестовой функции, которая реализует проверку того, что определенный вход приводит к ожидаемому выходу:

# content of test_expectation.py
import pytest


@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expected

Здесь декоратор @parametrize определяет три различных кортежа (test_input,expected), чтобы функция test_eval выполнялась три раза, используя их по очереди:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_expectation.py ..F                                              [100%]

================================= FAILURES =================================
____________________________ test_eval[6*9-42] _____________________________

test_input = '6*9', expected = 42

    @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
    def test_eval(test_input, expected):
>       assert eval(test_input) == expected
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

test_expectation.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54...
======================= 1 failed, 2 passed in 0.12s ========================

Примечание

Значения параметров передаются в тесты как есть (без копирования).

Например, если вы передаете список или dict в качестве значения параметра, а код тестового примера мутирует его, мутации будут отражены в последующих вызовах тестового примера.

Примечание

pytest по умолчанию экранирует любые неаскрипционные символы, используемые в строках unicode для параметризации, поскольку это имеет ряд недостатков. Однако если вы хотите использовать юникодовые строки в параметризации и видеть их в терминале как есть (без эскейпа), используйте эту опцию в pytest.ini:

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

Однако помните, что это может вызвать нежелательные побочные эффекты и даже ошибки в зависимости от используемой ОС и установленных плагинов, поэтому используйте это на свой страх и риск.

Как и было задумано в этом примере, только одна пара входных/выходных значений не справляется с простой тестовой функцией. И как обычно в случае с аргументами тестовой функции, вы можете увидеть значения input и output в трассировке.

Обратите внимание, что вы также можете использовать маркер parametrize на классе или модуле (см. Как пометить тестовые функции атрибутами), который будет вызывать, например, несколько функций с наборами аргументов:

import pytest


@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

Чтобы параметризовать все тесты в модуле, можно присвоить глобальной переменной pytestmark:

import pytest

pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])


class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

Также можно пометить отдельные экземпляры теста внутри параметризации, например, с помощью встроенной функции mark.xfail:

# content of test_expectation.py
import pytest


@pytest.mark.parametrize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

Давайте запустим это:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_expectation.py ..x                                              [100%]

======================= 2 passed, 1 xfailed in 0.12s =======================

Один набор параметров, который ранее вызывал сбой, теперь отображается как тест «xfailed» (ожидается сбой).

В случае, если значения, предоставленные в parametrize, приводят к пустому списку - например, если они динамически генерируются какой-то функцией - поведение pytest определяется опцией empty_parameter_set_mark.

Чтобы получить все комбинации из нескольких параметризованных аргументов, можно складывать декораторы parametrize:

import pytest


@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass

Это запустит тест с аргументами, установленными в x=0/y=2, x=1/y=2, x=0/y=3 и x=1/y=3, исчерпывая параметры в порядке декораторов.

Базовый пример pytest_generate_tests

Иногда вы можете захотеть реализовать свою собственную схему параметризации или реализовать некоторый динамизм для определения параметров или области действия приспособления. Для этого вы можете использовать хук pytest_generate_tests, который вызывается при сборе тестовой функции. Через переданный объект metafunc вы можете проверить запрашивающий тестовый контекст и, самое главное, вы можете вызвать metafunc.parametrize(), чтобы вызвать параметризацию.

Например, допустим, мы хотим запустить тест, принимающий строковые входные данные, которые мы хотим задать с помощью новой опции командной строки pytest. Давайте сначала напишем простой тест, принимающий аргумент функции-фиксатора stringinput:

# content of test_strings.py


def test_valid_string(stringinput):
    assert stringinput.isalpha()

Теперь мы добавляем файл conftest.py, содержащий добавление опции командной строки и параметризацию нашей тестовой функции:

# content of conftest.py


def pytest_addoption(parser):
    parser.addoption(
        "--stringinput",
        action="append",
        default=[],
        help="list of stringinputs to pass to test functions",
    )


def pytest_generate_tests(metafunc):
    if "stringinput" in metafunc.fixturenames:
        metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))

Если теперь мы передадим два значения stringinput, наш тест будет выполняться дважды:

$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
..                                                                   [100%]
2 passed in 0.12s

Давайте также выполним тест со строковым вводом, который приведет к неудаче:

$ pytest -q --stringinput="!" test_strings.py
F                                                                    [100%]
================================= FAILURES =================================
___________________________ test_valid_string[!] ___________________________

stringinput = '!'

    def test_valid_string(stringinput):
>       assert stringinput.isalpha()
E       AssertionError: assert False
E        +  where False = <built-in method isalpha of str object at 0xdeadbeef0001>()
E        +    where <built-in method isalpha of str object at 0xdeadbeef0001> = '!'.isalpha

test_strings.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False
1 failed in 0.12s

Как и ожидалось, наша тестовая функция не работает.

Если вы не укажете stringinput, он будет пропущен, поскольку metafunc.parametrize() будет вызван с пустым списком параметров:

$ pytest -q -rs test_strings.py
s                                                                    [100%]
========================= short test summary info ==========================
SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:2
1 skipped in 0.12s

Обратите внимание, что при многократном вызове metafunc.parametrize с различными наборами параметров, все имена параметров в этих наборах не могут быть продублированы, иначе будет выдана ошибка.

Больше примеров

Другие примеры вы можете посмотреть на more parametrization examples.

Вернуться на верх