Перейти к содержанию

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

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

pytest.fixture() позволяет параметризовать функции фикстур.

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

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

Note

В качестве альтернативы параметризации см. How to use subtests.

@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-9.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 ========================

Note

Параметры передаются в тесты «как есть» (никакого копирования не происходит).

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

Note

По умолчанию pytest экранирует любые не-ASCII символы, используемые в unicode-строках для параметризации, потому что это имеет несколько недостатков. Если же вы хотите использовать unicode-строки в параметризации и видеть их в терминале как есть (без экранирования), используйте эту опцию в конфигурационном файле:

toml

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true

ini

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true

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

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

Также можно использовать маркер parametrize на уровне класса или модуля (см. How to mark test functions with attributes), что приведёт к вызову нескольких функций с наборами аргументов, например:

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-9.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, который вызывается при сборе (collection) тестовой функции. Через переданный объект 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"))

Note

Хук pytest_generate_tests можно реализовать непосредственно в тестовом модуле или внутри тестового класса — в отличие от других хуков, pytest обнаружит его и там. Другие хуки должны находиться в conftest.py или плагине. См. Writing hook functions.

Если теперь передать два значения 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 for (stringinput)
1 skipped in 0.12s

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

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

Дополнительные примеры см. в more parametrization examples.