Сообщить об ошибке.

Хук pytest_generate_tests модуля pytest Python

Собственная схема передачи параметров в pytest

Содержание:

Базовый пример использования хука pytest_generate_tests().

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

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

# test_strings.py

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

Далее в файле конфигурации conftest.py, создадим 2 функции:

  • pytest_addoption(): - добавляет пользовательскую опцию командной строки --stringinput;
  • pytest_generate_tests(): - хук, который извлекает из опции командной строки --stringinput значения и передает их тестовой функции.
# conftest.py

def pytest_addoption(parser):
    """функция добавления опции командной строки"""
    parser.addoption(
        "--stringinput",
        action="append",
        default=[],
        help="список строковых данных для передачи в тестовые функции",
    )

def pytest_generate_tests(metafunc):
    """хук извлечения параметров CLI с последующей передачей их тесту"""
    # проверяем, есть ли тесты с аргументом `stringinput`
    if "stringinput" in metafunc.fixturenames:
        #  извлечения параметров CLI и передача 
        # их тестам, имеющим аргумент `stringinput`
        metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))

Теперь тест можно запустить командой CLI, которая принимает пользовательскую опцию --stringinput:

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

Если при запуске тестов НЕ УКАЗЫВАТЬ пользовательскую опцию --stringinput, то соответствующий тест (с аргументом stringinput) тоже будет пропущен, так как metafunc.parametrize() будет вызываться с пустым списком параметров.

Генерация комбинаций параметров для тестов в зависимости от опции CLI.

Допустим, что нужно выполнить тест с различными вычисляемыми параметрами и диапазон параметров должен определяться опцией командной строки. Напишем тест test_compute(), который сравнивает аргумент param1:

# test_compute.py

def test_compute(param1):
    assert param1 < 4

Теперь добавим в конфигурацию conftest.py следующий код:

# conftest.py
import pytest

def pytest_addoption(parser):
    """функция добавления опции командной строки"""
    parser.addoption("--all", action="store_true", help="выполнить все комбинации")

def pytest_generate_tests(metafunc):
    """хук извлечения параметров CLI и передачи тесту значений"""
    # проверяем, есть ли тесты с аргументом `param1`
    if "param1" in metafunc.fixturenames:
        if metafunc.config.getoption("all"):
            # если указана опция CLI `--all`
            end = 5
        else:
            # если нет
            end = 2
        # передача значений тестам, имеющим аргумент `param1`
        metafunc.parametrize("param1", range(end))

Добавленный код означает, что если при запуске теста не использовать пользовательскую опцию --all, то тест test_compute() запустится с param1=range(2), т.е. выполняться 2 теста. Если запустить тест с опцией --all, то выполнится 5 тестов, из которых последний выдаст ошибку.

Параметризация тестовых методов через конфигурацию класса.

Ниже приведен пример функции pytest_generate_tests(), которая реализующей схему параметризации тестовых методов через атрибут класса params. Что бы эта функция обслуживала все тестовые классы, ее можно поместить в файл конфигурации conftest.py.

# ./test_parametrize.py
import pytest

def pytest_generate_tests(metafunc):
    """Хук просматривает все тестовые классы модуля и ищет в них
    атрибут `params`, на основании которого передает значения
    аргументов соответствующим методам. Вызывается один 
    раз для каждого тестового метода"""
    funcarglist = metafunc.cls.params[metafunc.function.__name__]
    argnames = sorted(funcarglist[0])
    # передача значений аргументов тестам
    metafunc.parametrize(
        argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist]
    )

class TestClass:
    # словарь, определяющий несколько наборов аргументов 
    # для метода тестирования, указанных в качестве ключей
    params = {
        "test_equals": [dict(a=1, b=2), dict(a=3, b=3)],
        "test_zerodivision": [dict(a=1, b=0)],
    }

    def test_equals(self, a, b):
        assert a == b

    def test_zerodivision(self, a, b):
        with pytest.raises(ZeroDivisionError):
            a / b

Отсрочка настройки параметризованных ресурсов.

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

Пример, который реализует эту идею, используя хук pytest_generate_tests().

# test_backends.py
import pytest

def test_db_initialized(db):
    """фиктивный тест, использующий объект фикстуры `db`"""
    if db.__class__.__name__ == "DB2":
        pytest.fail("сбой в демонстрационных целях")

Затем добавим в файл конфигурации conftest.py, хук pytest_generate_tests() косвенно генерирует два вызова функции test_db_initialized() со значением аргумента db, а так же фикстуру как фабрику, которая создает объект базы данных для запуска конкретного теста:

# conftest.py
import pytest

def pytest_generate_tests(metafunc):
    if "db" in metafunc.fixturenames:
        metafunc.parametrize("db", ["d1", "d2"], indirect=True)

class DB1:
    "one database object"

class DB2:
    "alternative database object"

@pytest.fixture
def db(request):
    if request.param == "d1":
        return DB1()
    elif request.param == "d2":
        return DB2()
    else:
        raise ValueError("неверная внутренняя конфигурация теста")

Запустим тест командой $ pytest -q test_backends.py. Первый вызов с db == "DB1" прошел, в то время как второй с db == "DB2" завершился с ошибкой. Фикстура db создавала каждую из баз данных на этапе настройки, в то время как хук pytest_generate_tests() генерировал два соответствующих вызова test_db_initialized() на этапе сборки.

Объект Metafunc.

Объект Metafunc, передается хуку pytest_generate_tests().

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

Атрибуты и методы объекта Metafunc.

Metafunc.definition:

Свойство Metafunc.definition обеспечивает доступ к базовому _pytest.python.FunctionDefinition.

Metafunc.config:

Свойство Metafunc.config обеспечивает доступ к конфигурации pytest.Config для тестового сеанса.

Metafunc.module:

Свойство Metafunc.module представляет собой объект модуля, в котором определена тестовая функция.

Metafunc.function:

Свойство Metafunc.function представляет собой базовую тестовую функцию Python.

Metafunc.fixturenames:

Свойство Metafunc.fixturenames множество set имен фикстур, которые запрашивает тестовая функция.

Metafunc.cls:

Свойство Metafunc.cls представляет собой объект класса, в котором определена тестовая функция или None.

Metafunc.parametrize(argnames, argvalues, indirect=False, ids=None, scope=None, *, _param_mark=None):

Метод Metafunc.parametrize() добавляет новые вызовы в базовую тестовую функцию, используя список значений аргументов для заданных имен аргументов. Параметризация выполняется на этапе сбора тестов. Если нужно настроить дорогостоящие ресурсы, посмотрите, как это сделать косвенно, а не во время сбора тестов.

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

unparametrized:         t
parametrize ["x", "y"]: t[x], t[y]
parametrize [1, 2]:     t[x-1], t[x-2], t[y-1], t[y-2]

Принимаемые аргументы:

  • argnames: строка, разделенная запятыми, обозначающая одно или несколько имен аргументов, или список/кортеж строк аргументов;
  • argvalues: список значений аргументов определяет, как часто вызывается тест с различными значениями аргументов.

    Если было указано только одно имя аргумента, то argvalues - это список значений. Если было указано N имен аргументов, то значения аргументов должны представлять собой список из N кортежей, где каждый элемент кортежа задает значение для соответствующего имени аргумента.

  • indirect: список имен аргументов (подмножество аргументов) или логическое значение. Если True, то список содержит все имена из arnames. Каждое значение аргумента, соответствующее имени аргумента в этом списке, будет передано как request.param в соответствующую фикстуру имени аргумента, чтобы она могла выполнять более дорогостоящие настройки на этапе настройки теста, а не во время сбора данных тестов.

  • ids: последовательность (или генератор) идентификаторов для значений аргументов или вызываемый объект для возврата части идентификатора для каждого значения аргумента.

Если ids последовательность или генератор, такой как itertools.count(), то возвращаемые идентификаторы должны иметь тип str, int, float, bool или None. Они сопоставляются с соответствующим индексом в argvalues. None означает использование автоматически сгенерированного идентификатора.

Если ids вызываемый объект, то он будет вызываться для каждой записи в argvalues, а возвращаемое значение используется как часть автоматически сгенерированного идентификатора для всего набора (где части соединены тире '-'). Это полезно для предоставления более конкретных идентификаторов для определенных элементов, например даты.

Возврат None будет использовать автоматически сгенерированный идентификатор. Если идентификаторы не указаны, то они будут сгенерированы автоматически из значений аргументов.

  • scope: если указано, то обозначает область действия параметров. Область видимости используется для группировки тестов по экземплярам параметров. Аргумент scope также переопределит любую область, определенную функцией приспособления, позволяя установить динамическую область, используя тестовый контекст или конфигурацию.

Функция parser.addoption(*opts, **attrs).

Функция parser.addoption() регистрирует параметр командной строки.

Аргумент *opts - имена опций, могут быть короткими или длинными опциями.

Аргумент **attrs - те же атрибуты, которые принимает функция parser.add_argument() модуля argparse.

После синтаксического анализа командной строки, параметры доступны в объекте конфигурации pytest через config.option.NAME, где NAME обычно задается путем передачи атрибута dest, например parser.addoption('--long', dest='NAME', ...).