如何根据pytest中的夹具参数来参数化测试?

时间:2020-07-01 17:20:42

标签: python pytest

我有一个Python程序,可以根据输入规范生成C代码。我正在用pytest编写测试。自然,测试策略包括对生成的C代码进行一些测试。

对于这些测试,计划如下所示:

  • 我们有一组目录,每个目录包含一个规范文件和一组适用的输入/预期输出用例。

  • 一个夹具将处理生成C代码并进行编译。该固定装置将在一组规范文件(通过测试脚本以编程方式读取)上进行参数化。这样做的好处是,在该规范下,所有测试用例只能进行一次构建(因为构建成本很高)。

  • 一个测试函数将从夹具中获取GeneratedCode对象,使用特定的输入运行它,并验证预期的输出。这将在一组输入/输出用例(也可以通过脚本以编程方式读取)上进行参数化。

这样,添加新的测试用例就像添加新的规范或测试用例文件一样简单。无需在测试脚本中复制和粘贴代码。

我想象它看起来像这样:

# Get the list of specification files and test cases programmatically
specification_names = get_list_of_specifications()
test_cases = dict()
for spec in specification_names:
    # get_list_of_test_cases() returns a list of (input, output) tuples
    test_cases[spec] = get_list_of_test_cases(spec)

class GeneratedCode:
    def __init__(spec):
        """Generate the C code for spec in a temp directory"""
        self.name = spec
        ...
    
    def build():
        """Build the generated C code"""
        ...
    
    def run(input):
        """Run the code on given input."""
        ...
    
    def cleanup():
        ...

@pytest.fixture(scope="module", params=specification_names)
def generated_code(request):
    code = GeneratedCode(request.param)
    code.build()
    yield code
    code.cleanup()

@pytest.mark.parametrize('test_input,expected_output', test_cases[???])
def test_generated_code(generated_code, test_input, expected_output):
    assert generated_code.run(test_input) == expected_output

当然,这里的问题是@pytest.mark.parametrize()不能每次都使用相同的测试用例集,因为它取决于生成代码的规范。如果我们可以获取当前灯具的参数,则可以在test_cases字典中查找它,但是我不确定该怎么做,甚至不确定。

有没有办法做到这一点?我还有其他方法可以进行这些测试吗?

2 个答案:

答案 0 :(得分:2)

通过将规范作为generate_code中的元组的一部分传回,可能能够将数据连接在一起。

@pytest.fixture(scope="module", params=specification_names)
def generated_code(spec):
    code = GeneratedCode(spec)
    code.build()
    yield code, spec
    code.cleanup()

def test_generated_code(generated_code):
    code, spec = generated_code
    test_input, expected_output = test_cases[spec]
    assert generated_code.run(test_input) == expected_output```

我可以想到的另一种方法是使用subTest,如果您可以访问python标准库的一部分unittest

import unittest

class TestSequence(unittest.TestCase):

    def _setup(self, spec):
        self.code = GeneratedCode(spec)
        self.code.build()

    def tearDown(self):
        self.code.cleanup()

    def test_generated_code(self):
        for spec, (test_input, expected_output) in test_cases.items():
            with self.subTest(spec):
                self._setup(spec)
                assert self.code.run(test_input) == expected_output

答案 1 :(得分:1)

indirect argument to @pytest.mark.parametrize可以帮助完成这项工作。本质上,它可以通过测试功能对夹具进行参数化。

specification_names = get_list_of_specifications()
test_cases = []
for spec in specification_names:
    test_cases.extend([(spec, input, output) for (input, output) in
                       get_list_of_test_cases(spec)])

...

@pytest.fixture(scope="module")
def generated_code(request):
    code = GeneratedCode(request.param)
    code.build()
    yield code
    code.cleanup()

@pytest.mark.parametrize(
        'generated_code,test_input,expected_output',
        test_cases,
        indirect=['generated_code'],
        scope="module" # <-- This is important!
)
def test_generated_code(generated_code, test_input, expected_output):
    assert generated_code.run(test_input) == expected_output

请注意scope="module"装饰器中的parametrize。如果未指定,它将默认为'function',在某些情况下(包括该情况),它似乎优先于灯具的指定范围。

对于我而言,有关细节非常模糊。关于scope甚至对@pytest.mark.parameterize意味着什么的文档还不清楚。但是,如果parametrize中的所有 all 参数为indirect,则固定装置将使用其自己的范围,否则将使用parametrize中的范围。但是,如果您在indirect上使用同一夹具使用多个测试功能,则无论您指定什么功能,它们通常最终会处于不同的作用域,我不确定为什么。这是previously buggy的区域,很有可能still be

在任何情况下,上面的代码都可以执行您想要的操作,但是将夹具作用域更多地视为性能优化而不是依靠它来进行正确的测试行为(这听起来像您已经在使用中)可能是一个好主意。做)。