Параметры фикстуры и pytest_generate_tests

Pytest - потрясающая среда тестирования для Python. В этой статье я сосредоточусь на том, как параметризация приспособлений преобразуется в параметризацию теста в Pytest.

В Pytest есть две приятные особенности: параметризация и приспособления. Они служат совершенно разным целям, но вы можете использовать приспособления для параметризации.

Что такое приспособление?

Прибор - это функция, которая автоматически вызывается Pytest, когда имя аргумента (аргумент тестовой функции или другого прибора) совпадает с именем прибора. Другими словами:

@pytest.fixture
def fixture1():
   return "foo"
def test_foo(fixture1):
    assert fixture1 == "foo"

В этом примере fixture1 вызывается в момент выполнения test_foo. Возвращаемое значение fixture1 передается в test_foo как аргумент с именем fixture1. У приборов есть много-много нюансов (например, у них есть область видимости, они могут использовать yield вместо return, чтобы иметь некоторый код очистки и т. Д., И т. Д.), Но в этой главе я сосредоточусь на одной мощной функции, возможном params аргументе pytest.fixture декоратор. Используется для параметризации.

Что такое параметризация?

Грубо говоря, параметризация - это процесс варьирования (изменения) одного или нескольких коэффициентов в математическом уравнении. В контексте тестирования параметризация - это процесс запуска одного и того же теста с разными значениями из подготовленного набора. Каждая комбинация теста и данных считается новым тестовым случаем.

Самая простая форма параметризации:

@pytest.mark.parametrize("number", [1, 2, 3, 0, 42])
def test_foo(number):
    assert number > 0 

В этом случае мы получаем пять тестов: Параметр number может иметь значение 1, 2, 3, 0 или 42. Каждый из этих тестов может завершиться ошибкой независимо друг от друга (если в этом примере тест со значением 0 не прошел и четыре другие проходит). Этот подход намного удобнее для отладки и разработки по сравнению с простым циклом с утверждением в нем. Цикл не сможет запустить тест для 42 после сбоя теста на 0, но параметризация позволяет нам видеть результаты для всех случаев, даже если они происходят после неудачного тестового примера.

Это была легкая часть, о которой все знают. Теперь мы собираемся обсудить, что такое «параметризация» с точки зрения Pytest; когда это происходит и как это можно сделать по параметрам приспособления. Наконец, мы рассмотрим общий метод создания произвольной алгоритмической параметризации.

Время сбора и время тестирования

Pytest имеет специальный этап выполнения, называемый «время сбора» (название аналогично «времени выполнения» и «времени компиляции»). На этом этапе Pytest обнаруживает тестовые файлы и тестовые функции в этих файлах и, что наиболее важно для этой статьи, выполняет динамическую генерацию тестов (параметризация - один из способов генерации тестов). На этом этапе также создаются фикстуры, но декораторы (такие как @pytest.fixture) выполняются во время импорта модуля.

По истечении времени сбора Pytest начинает следующий этап, называемый «временем тестирования», когда вызываются функции настройки, вызываются фикстуры и выполняются тестовые функции (которые были обнаружены / сгенерированы во время сбора). Точный порядок выполнения фикстур / тестовых функций довольно сложен, так как разные фикстуры могут выполняться на уровне модуля (один раз перед каждой тестовой функцией), на функциональном уровне (перед каждым тестом) и т. Д. То же самое относится и к разборке. код.

Ключевой вывод из этого заключается в том, что ни фикстура, ни тест никогда не вызываются во время сбора, и нет способа генерировать тесты (включая параметризацию) во время тестирования. Некоторые из этих ограничений являются естественными (например, декораторы выполняются во время импорта, функции выполняются намного позже), некоторые активно применяются самим Pytest (например, если кто-то пытается вызвать какой-либо прибор во время сбора, Pytest прерывается с определенным сообщением: Fixtures are not meant to be called directly ).

Если вы пришли к этой статье, чтобы найти способ добавить больше тестов за один раз, ответ - «это невозможно».

Параметры приспособления

Светильники могут иметь собственные параметры.

Обратите внимание, «параметр» в этом контексте абсолютно отличается от «аргумента функции».

Эти параметры можно передать в виде списка аргументу params @pytest.fixture() decorator (см. Примеры ниже).

Эти параметры должны повторяться. Они могут быть генераторами, списками, кортежами, наборами и т. Д. Pytest использует такие итерации и преобразует их в список. Для таких итераций нет ленивого вычисления; все итерации будут завершены до времени тестирования.

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

Чтобы использовать эти параметры, прибор должен использовать специальный прибор с именем «request». Он предоставляет специальному (встроенному) приспособлению некоторую информацию о функциях, с которыми он имеет дело. request также содержит request.param, который содержит один элемент из params. Фиксация вызывается столько раз, сколько элементов в итерации params argument, а тестовая функция вызывается со значениями фикстур такое же количество раз. (в основном, прибор вызывается len(iterable) раз с каждым следующим элементом итерации в request.param).

Параметризация с pytest_generate_tests

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

Во время сбора Pytest ищет и вызывает (если обнаруживается) специальную функцию в каждом модуле с именем pytest_generate_tests. Эта функция - не приспособление, а просто обычная функция.

Он получает аргумент metafunc, который сам по себе является не приспособлением, а особым объектом. Часть имени функции генерировать немного вводит в заблуждение, поскольку не позволяет генерировать новый код. У него есть единственная возможность выполнять индивидуальную параметризацию (которая технически порождает новые тесты, но не в смысле нового кода).

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

def test_simple():
   assert 2+2 == 4

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

metafunc аргумент pytest_generate_tests предоставляет некоторую полезную информацию о тестовой функции:

  • возможность видеть все имена приборов, которые запрашивает функция
  • возможность видеть название функции
  • возможность видеть код функции (это менее полезно, чем вы можете себе представить, что вы собираетесь делать с func.__code__?)

Наконец, metafunc имеет функцию parametrize, которая позволяет предоставить несколько вариантов значений для приборов (например, параметризацию).

Я хочу подчеркнуть, что pytest_generate_tests не имеет никакого доступа к данным о времени тестирования, включая выходные значения любых других приборов или результаты каких-либо тестов.

Это было много тестов и никакого кода. А теперь займемся этим.

Примеры

Начнем с базового примера без каких-либо уловок:

def test_foobar():
    assert type("one") == type("two")

Теперь мы добавляем два прибора fixture1 и fixture2, каждый из которых возвращает одно значение. Это тот же результат, но более подробный. Мы делаем это для разработки дальнейших примеров.

import pytest
@pytest.fixture
def fixture1():
    return "one"
@pytest.fixture
def fixture2():
    return "two"
def test_foobar(fixture1, fixture2):
    assert type(fixture1) == type(fixture2)

(начиная со следующего примера я пропущу строку «import pytest», но она должна присутствовать во всех примерах ниже).

Теперь давайте добавим наши первые параметры в приборы:

@pytest.fixture(params=["one", "uno"])
def fixture1(request):
    return request.param
@pytest.fixture(params=["two", "duo"])
def fixture2(request):
    return request.param
def test_foobar(fixture1, fixture2):
    assert type(fixture1) == type(fixture2)

Результат:

#OUTPUT 3
collected 4 items
test_3.py::test_foobar[one-two] PASSED  [ 25%]
test_3.py::test_foobar[one-duo] PASSED  [ 50%]
test_3.py::test_foobar[uno-two] PASSED  [ 75%]
test_3.py::test_foobar[uno-duo] PASSED  [100%]

Теперь протестированы все четыре комбинации, и этот код более краток, чем четыре отдельных теста. Только представьте, что эти приборы имеют по 5 параметров - это 25 тестовых примеров!

Теперь займемся этим с pytest_generate_tests:

def pytest_generate_tests(metafunc):
    if "fixture1" in metafunc.fixturenames:
        metafunc.parametrize("fixture1", ["one", "uno"])
    if "fixture2" in metafunc.fixturenames:
        metafunc.parametrize("fixture2", ["two", "duo"])
def test_foobar(fixture1, fixture2):
    assert type(fixture1) == type(fixture2)

Результат такой же, как и раньше. В этом примере вы можете видеть, что мы параметризуем функцию дважды: для fixture1 и для fixture2.

В следующем примере я использую озорные способности к самоанализу:

def pytest_generate_tests(metafunc):
    if "fixture1" in metafunc.fixturenames:
        metafunc.parametrize("fixture1", [
            metafunc.function.__name__,
            metafunc.module.__name__,
        ])
    if "fixture2" in metafunc.fixturenames:
        metafunc.parametrize("fixture2", dir(metafunc.module))
def test_foobar(fixture1, fixture2):
    assert type(fixture1) == type(fixture2)

Результат выглядит как анатомический атлас:

collected 26 items
test_5.py::test_foobar[test_foobar-@py_builtins] PASSED
test_5.py::test_foobar[test_foobar-@pytest_ar] PASSED
test_5.py::test_foobar[test_foobar-__builtins__] PASSED
test_5.py::test_foobar[test_foobar-__cached__] PASSED
test_5.py::test_foobar[test_foobar-__doc__] PASSED
test_5.py::test_foobar[test_foobar-__file__] PASSED
test_5.py::test_foobar[test_foobar-__loader__] PASSED
test_5.py::test_foobar[test_foobar-__name__] PASSED
test_5.py::test_foobar[test_foobar-__package__] PASSED
test_5.py::test_foobar[test_foobar-__spec__] PASSED
test_5.py::test_foobar[test_foobar-pytest] PASSED
test_5.py::test_foobar[test_foobar-pytest_generate_tests] PASSED
test_5.py::test_foobar[test_foobar-test_foobar] PASSED
test_5.py::test_foobar[test_5-@py_builtins] PASSED
test_5.py::test_foobar[test_5-@pytest_ar] PASSED
test_5.py::test_foobar[test_5-__builtins__] PASSED
test_5.py::test_foobar[test_5-__cached__] PASSED
test_5.py::test_foobar[test_5-__doc__] PASSED
test_5.py::test_foobar[test_5-__file__] PASSED
test_5.py::test_foobar[test_5-__loader__] PASSED
test_5.py::test_foobar[test_5-__name__] PASSED
test_5.py::test_foobar[test_5-__package__] PASSED
test_5.py::test_foobar[test_5-__spec__] PASSED
test_5.py::test_foobar[test_5-pytest] PASSED
test_5.py::test_foobar[test_5-pytest_generate_tests] PASSED
test_5.py::test_foobar[test_5-test_foobar] PASSED

В этом примере fixture1 - это либо имя функции, либо имя модуля (имя файла тестового модуля), а fixture2 - список объектов в тестовом модуле (вывод функции dir()).

То, что ты не можешь делать

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

def fixture1():
    yield "one" 
    yield "uno"  # Pytest forbids yielding twice from a fixture.

Причина в том, что фикстуры необходимо параметризовать во время сбора данных. Если фикстура выполняет несколько выходов, это означает, что тесты появляются «во время тестирования», а это несовместимо с внутренними функциями Pytest. Сам yield полезен, если вы хотите выполнить некоторую очистку после того, как значение было поглощено и использовано. Вы можете поместить код очистки после yield.. Вы получите управление от оператора yield, как только значение больше не понадобится.

Поскольку мы передаем аргументы декоратору Pytest, мы не можем использовать какие-либо фикстуры в качестве аргументов. Это был бы неправильный тип объекта (если мы напишем params=fixture3), или они были бы отклонены Pytest (если мы напишем params=fixture3()), поскольку мы не можем вызывать фикстуры как функции.

Внутри pytest_generate_tests мы можем видеть имена приборов, требуемых функцией, но мы не можем получить доступ к значениям этих приборов. Есть только .fixturenames, а не .fixtures или что-то в этом роде.

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

Этот пример невозможно написать правильно:

def pytest_generate_tests(metafunc):
    if "count" in metafunc.fixturenames:
        if count % 15 == 0:
             metafunc.parametrize("fizzbuzz", ["fizzbuzz"])
        elif count% 3 == 0:
            metafunc.parametrize("fizz", ["fizz"])
        elif count % 5 == 0:
           metafunc.parametrize("buzz", ["buzz"])

По многим причинам:

  1. У вас нет доступа к count значению
  2. Вы не можете передать некоторые приспособления, но не другие, для проверки работоспособности. Это всегда катезианское слово (хотя можно использовать пропуски).

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

Это не сработает:

def pytest_generate_tests(metafunc):
    metafunc.parametrize("always", ["must", "add", "fixtures"])
def test_no_fixtures_please():
    assert "fixtures" not in "test"

Послесловие

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

Мой совет - старайтесь, чтобы тестовый код был как можно более простым. Каждый уровень косвенного обращения к тестам делает тесты более хрупкими и менее «глупыми» (нам нужны «глупые» тесты, поскольку мы можем быстро проверить их на правильность, что не относится к тестам smartass).

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

Кроме того, построение алгоритмической фиксации позволяет параметризацию на основе внешних факторов, таких как содержимое файлов, параметры командной строки или запросы к базе данных.

Подтверждение

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

Я глубоко признателен за исправления, внесенные Алланом Сильверстайном.