【问题标题】:How to parameterize a test depending on fixture parameters in pytest?如何根据 pytest 中的夹具参数对测试进行参数化?
【发布时间】:2020-07-01 17:20:42
【问题描述】:

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

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

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

  • fixture 将处理生成 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 字典中查找它,但我不确定如何做到这一点,或者是否有可能。

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

【问题讨论】:

    标签: python pytest


    【解决方案1】:

    也许能够通过将规范作为 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,如果你可以访问unittest,它是python标准库的一部分:

    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
    

    【讨论】:

    • 我不确定第一个收益有多大:GeneratedCode 对象已经包含 self.spec。测试仍然需要参数化(test_cases[spec] 是测试用例列表,而不是单个输入/输出用例)。第二个会起作用(没有什么能真正阻止我使用unittest),但为此不得不引入第二个测试框架感觉很可惜。似乎真的应该有一种方法来完成它,只需 pytest
    • 其实我觉得这个indirect parameterization会是一种方式,但是接下来fixture作用域就被忽略了。
    • @DominickPastore 同意了。希望有更多经验的人可以在这里权衡。只是提供了想到的东西,这可能会起作用。希望对您有所帮助。
    • 谢谢。毕竟我找到了使用indirect 的解决方案。但是 +1 是另一种可行的解决方案。
    【解决方案2】:

    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
    

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

    对我来说,细节很模糊。关于 scope 甚至对 @pytest.mark.parameterize 意味着什么的文档不是很清楚。但是,如果parametrize 中的所有参数都是indirect,则夹具使用自己的范围,否则它使用来自parametrize 的范围。而且,如果你有多个测试函数使用indirect 的同一个fixture,那么无论你指定什么,它们通常都会在不同的范围内结束,我不知道为什么。这是previously buggy 的区域,它可能是still be

    在任何情况下,上面的代码都应该做你想做的事,但最好将夹具范围更多地视为性能优化,而不是依赖它来实现正确的测试行为(听起来你已经做)。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-02-24
    • 1970-01-01
    • 1970-01-01
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多