Параметризация Pytest

Завершённый

Параметризуемая функция в pytest может первоначально показаться сложной, но ее цель проста, как только вы понимаете проблему, которая она решает. По сути, параметризация позволяет эффективно запускать одну и ту же функцию тестирования с различными входными данными, что упрощает выполнение подробных и разнообразных утверждений с меньшим количеством кода.

При вызове параметризации первый аргумент — это строка, содержащая одно или несколько имен аргументов, например "test\_input_". Второй аргумент содержит список значений аргументов, например ["27", "6+9", "0", "O"]. Последние четыре аргумента имеют значения по умолчанию и являются необязательными.

Вы можете найти справочник по API pytest на английском языке для parametrize здесь: pytest.Metafunc.parametrize.

Когда следует использовать параметризацию

Два распространенных сценария, в которых может потребоваться использовать параметризацию :

  • При тестировании циклов for
  • Если несколько тестов утверждают одно и то же поведение

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

Циклы

Ниже приведен пример тестовой функции с циклом for:

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

Этот тест проблематичен, так как при сбое может привести к нескольким проблемам, в том числе:

  • Неоднозначные отчеты о тестах: Тестовый отчет не указывает, произошел ли сбой только одного элемента или несколько сбоев.
  • Одно тестовое представление: Все элементы рассматриваются как один тест, который скрывает производительность отдельных элементов.
  • Неопределенные исправления: Если сбой исправлен, нет способа узнать, разрешаются ли все проблемы без повторного запуска всего теста.

Давайте модифицируем тест, чтобы особенно включить два элемента, которые, как ожидается, не пройдут тестирование.

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

При выполнении теста отображается только один сбой, даже если в этом списке есть два недопустимых элемента:

$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x103fa1df0>()
E    +    where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Это отличный сценарий использования параметризации. Прежде чем мы узнаем, как обновить тест, давайте рассмотрим другую распространенную ситуацию, которая не включает for циклы.

Тесты, которые утверждают то же поведение

Группа тестов, делающих то же утверждение, является также хорошим кандидатом для параметризации . Если предыдущий тест был перезаписан с одним тестом для каждого элемента, это позволит улучшить отчеты о сбоях, но это будет повторяться:

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 как библиотеку, а затем использовать ее в качестве декоратора в функции. Ниже приведен обновленный тест:

import pytest

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

Перед выполнением тестов давайте рассмотрим изменения.

Декоратор pytest.mark.parametrize() определяет два аргумента. Первый аргумент — это строка с именем "item". Эта строка используется в качестве именованного аргумента для тестовой функции, которая отображается в следующей строке определения функции теста. Второй аргумент — это список тестовых значений.

Подробные отчеты об ошибках

За кулисами pytest рассматривает каждый элемент в этом списке как отдельный тест. Это означает, что успешные и неудачные тесты сообщаются отдельно. Давайте посмотрим, что происходит при выполнении теста с pytest:

$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

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

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45e30>()
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45df0>()
E    +    where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== 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 сообщает о пяти тестах в общей сложности: три успешных и два неуспешных. Ошибки передаются отдельно, в том числе сведения о неудачных входных данных.

$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

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

Использование подробного флага выходных данных

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

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

В результате выполнения тестов создаётся минимальный вывод:

$ pytest test_items.py 
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

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

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

При повышении детализации показываются значения, которые pytest использует для каждого теста при использовании параметризации.

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
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", но можно включить несколько имен аргументов в строку, указывающую первый аргумент, разделенный запятыми.

Один из вариантов использования нескольких имен аргументов заключается в том, чтобы передать набор ожидаемых значений для проверки на входное значение. Во втором аргументе каждый элемент в наборе должен иметь количество значений, равное количеству входных имен. Например, если ваши входные имена "test\_input, expected\_value", второй аргумент может выглядеть примерно так: [("3+5", 8), ("3*4", 12)]

Этот тест проверяет, имеет ли объект атрибут с помощью функции Python hasattr(). Он возвращает булевское значение в зависимости от того, обладает ли объект соответствующим атрибутом.

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

Так как hasattr() требуется два аргумента, мы можем использовать параметризацию следующим образом:

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

Параметризующий декоратор по-прежнему использует одну строку для первого аргумента, но с двумя именами аргументов, разделенными запятой, которая становится аргументами для функции тестирования. В этом случае это item и attribute.

Далее приведен список двух пар элементов. Каждая из этих пар представляет собой item и attribute, предназначенные для проверки.

Если pytest не может создать строковое представление передаваемых объектов, он создает его. Это можно увидеть при выполнении теста:

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
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 ===============================