关于python:在pytest测试类中使用@ mark.incremental和metafunc.parametrize

Using @mark.incremental and metafunc.parametrize in a pytest test class

@ mark.incremental的目的是,如果一个测试失败,则将随后的测试标记为预期失败。

但是,当我将其与参数化结合使用时,会出现不良行为。

例如,在此伪造代码的情况下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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,

  • 然后在True上输入test_correct,

  • 然后在false上输入test_input,

  • 接着是False的test_correct,

  • 由test_input遵循无,

  • 其次是(xfailed)test_correct,无,等等,等等。

相反,发生的是测试类

  • 在True上运行test_input,
  • 然后在False上运行test_input
  • 然后在None上运行test_input
  • 然后将从该点开始的所有内容都标记为xfailed(包括test_corrects)。

我假设正在发生的事情是,参数化的优先级高于通过类中的函数进行的优先级。问题是,是否有可能重写此行为或以某种方式解决它,因为当前的情况使得将类标记为增量对我完全没有用。

(解决此问题的唯一方法是每次使用不同的参数一次又一次地复制粘贴该类的代码吗?这种想法对我很反感)


https://docs.pytest.org/zh-CN/latest/example/parametrize.html标题A quick port of"testscenarios"下描述了解决方案。

这是此处列出的代码,conftest.py中的代码正在执行的操作是在测试类中寻找变量scenarios。当找到变量时,它将遍历场景的每个项目,并期望一个用于标记测试的id字符串和一个'argnames:argvalues'字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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)
您可以通过以下方式使用同一列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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)

id将是一个自动生成的数字(您可以将其更改为使用指定的数字),并且在此实现中,它不会处理多个参数化变量,因此您必须将它们编译在一个列表中(或满足pytest_generate_tests的要求来处理)您)


以下解决方案不要求更改您的测试班级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_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将是

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

显示类别test.TestClass的第3次运行(index = 2)失败。
在类中为给定参数运行测试方法之前,它会检查该类中的任何先前测试方法对于给定参数是否均未失败,如果是,则通过首次失败的方法名称信息xfail测试。

没有经过100%的测试,但是已经投入使用并且可以满足我的需求。