#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%, но используется и работает для моих нужд.