Как параметризировать приспособления и тестовые функции¶
pytest позволяет параметризовать тесты на нескольких уровнях:
pytest.fixture()
позволяет parametrize fixture functions.
@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.