Использование @mark.incremental и metafunc.parametrize в тестовом классе pytest

#python #testing #pytest

#python #тестирование #pytest

Вопрос:

цель @mark.incremental заключается в том, что если один тест завершается неудачей, последующие тесты помечаются как ожидаемые неудачи.

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

Например, в случае этого поддельного кода:

 //conftest.py:

def pytest_generate_tests(metafunc):
    metafunc.parametrize("input", [True, False, None, False, True])

def pytest_runtest_makereport(item, call):
    if "incremental" in item.keywords:
        if call.excinfo is not None:
            parent = item.parent
            parent._previousfailed = item

def pytest_runtest_setup(item):
    if "incremental" in item.keywords:
        previousfailed = getattr(item.parent, "_previousfailed", None)
        if previousfailed is not None:
            pytest.xfail("previous test failed (%s)" %previousfailed.name)

//test.py:
@pytest.mark.incremental
class TestClass:
    def test_input(self, input):
        assert input is not None
    def test_correct(self, input):
        assert input==True
  

Я бы ожидал, что тестовый класс будет запущен

  • test_input на True,

  • за которым следует test_correct на True,

  • за которым следует test_input для False,

  • за которым следует test_correct на False,

  • после test_input на None,

  • за которым следует (xfailed) test_correct на None и т. Д. И т. Д.

Вместо этого происходит то, что тестовый класс

  • запускает test_input при значении True,
  • затем запускает test_input для False,
  • затем запускает test_input для None,
  • затем помечает все, начиная с этого момента, как xfailed (включая test_corrects).

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

(единственный способ справиться с этим — копировать-вставлять код для класса снова и снова, каждый раз с разными параметрами? Эта мысль мне отвратительна)

Ответ №1:

Решение этой проблемы описано в https://docs.pytest.org/en/latest/example/parametrize.html под заголовком A quick port of “testscenarios”

Это код, указанный там, и то, что conftest.py делает код, — это поиск переменной scenarios в тестовом классе. Когда он находит переменную, он перебирает каждый элемент сценариев и ожидает id строку, с помощью которой можно пометить тест, и словарь «имена аргументов: значения аргументов»

 # content of conftest.py
def pytest_generate_tests(metafunc):
    idlist = []
    argvalues = []
    for scenario in metafunc.cls.scenarios:
        idlist.append(scenario[0])
        items = scenario[1].items()
        argnames = [x[0] for x in items]
        argvalues.append(([x[1] for x in items]))
    metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")

# content of test_scenarios.py
scenario1 = ('basic', {'attribute': 'value'})
scenario2 = ('advanced', {'attribute': 'value2'})

class TestSampleWithScenarios(object):
    scenarios = [scenario1, scenario2]

    def test_demo1(self, attribute):
        assert isinstance(attribute, str)

    def test_demo2(self, attribute):
        assert isinstance(attribute, str)
  

Вы также можете изменить функцию pytest_generate_tests , чтобы принимать входные данные другого типа. Например, если у вас есть список, который вы обычно передаете
@pytest.mark.parametrize("varname", varval_list)
вы можете использовать тот же список следующим образом:

 # content of conftest.py
def pytest_generate_tests(metafunc):
    idlist = []
    argvalues = []
    argnames = metafunc.cls.scenario_keys
    for idx, scenario in enumerate(metafunc.cls.scenario_parameters):
        idlist.append(str(idx))
        argvalues.append([scenario])
    metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")

# content of test_scenarios.py
varval_list = [a, b, c, d]
class TestSampleWithScenarios(object):
    scenario_parameters = varval_list
    scenario_keys = ['varname']

    def test_demo1(self, attribute):
        assert isinstance(attribute, str)

    def test_demo2(self, attribute):
        assert isinstance(attribute, str)
  

Идентификатор будет автоматически сгенерированным числом (вы можете изменить его на использование того, что вы укажете), и в этой реализации он не будет обрабатывать несколько переменных параметризации, поэтому вам придется скомпилировать их в один список (или pytest_generate_tests обработать это для вас)

Комментарии:

1. Пожалуйста, добавьте основные моменты решения вместо простой ссылки.

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

3. Для меня это только половина решения. Вы нашли решение @dWitty

Ответ №2:

Следующее решение не запрашивает изменение вашего тестового класса

 _test_failed_incremental = defaultdict(dict)


def pytest_runtest_makereport(item, call):
    if "incremental" in item.keywords:
        if call.excinfo is not None and call.excinfo.typename != "Skipped":
            param = tuple(item.callspec.indices.values()) if hasattr(item, "callspec") else ()
            _test_failed_incremental[str(item.cls)].setdefault(param, item.originalname or item.name)


def pytest_runtest_setup(item):
    if "incremental" in item.keywords:
        param = tuple(item.callspec.indices.values()) if hasattr(item, "callspec") else ()
        originalname = _test_failed_incremental[str(item.cls)].get(param)
        if originalname:
            pytest.xfail("previous test failed ({})".format(originalname))
  

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

 defaultdict(<class 'dict'>, {"<class 'test.TestClass'>": {(2,): 'test_input'}})
  

показывает, что 3-й запуск (index= 2) не удался для теста класса.Тестовый класс.
Перед запуском тестового метода в классе для данного параметра он проверяет, не сработал ли какой-либо предыдущий тестовый метод в классе для данного параметра, и если это так, завершите тест с информацией об имени метода, который сначала не удался.

Не протестировано на 100%, но используется и работает для моих нужд.