提案:使用夹具进行参数化

警告

本文档概述了关于将夹具用作参数化测试或夹具输入的提案。

问题

作为用户,我有一些功能测试,希望针对各种场景运行它们。

在这个特定的例子中,我们希望基于 cookiecutter 模板生成一个新项目。我们想测试默认值,但也想测试模拟用户输入的数据。

  • 使用默认值

  • 模拟用户输入

    • 指定“author”

    • 指定“project_slug”

    • 指定“author”和“project_slug”

一个功能测试可能看起来像这样

import pytest


@pytest.fixture
def default_context():
    return {"extra_context": {}}


@pytest.fixture(
    params=[
        {"author": "alice"},
        {"project_slug": "helloworld"},
        {"author": "bob", "project_slug": "foobar"},
    ]
)
def extra_context(request):
    return {"extra_context": request.param}


@pytest.fixture(params=["default", "extra"])
def context(request):
    if request.param == "default":
        return request.getfuncargvalue("default_context")
    else:
        return request.getfuncargvalue("extra_context")


def test_generate_project(cookies, context):
    """Call the cookiecutter API to generate a new project from a
    template.
    """
    result = cookies.bake(extra_context=context)

    assert result.exit_code == 0
    assert result.exception is None
    assert result.project.isdir()

问题

  • 通过使用 request.getfuncargvalue(),我们依赖实际的夹具函数执行来了解涉及哪些夹具,因为它是动态的

  • 更重要的是,request.getfuncargvalue() 不能与参数化的夹具结合使用,例如 extra_context

  • 如果您希望通过某些参数来扩展已用于测试的夹具的现有测试套件,这非常不便

如果您尝试运行上述代码,pytest 3.0 版会报告错误

Failed: The requested fixture has no parameter defined for the current
test.

Requested fixture 'extra_context'

建议的解决方案

可以在模块中使用一个新函数,用于从现有夹具动态定义夹具。

pytest.define_combined_fixture(
    name="context", fixtures=["default_context", "extra_context"]
)

新夹具 context 继承了所用夹具的作用域并产生以下值。

  • {}

  • {'author': 'alice'}

  • {'project_slug': 'helloworld'}

  • {'author': 'bob', 'project_slug': 'foobar'}

替代方法

一个名为 fixture_request 的新辅助函数会告诉 pytest 产生所有标记为夹具的参数。

注意

pytest-lazy-fixture 插件实现了与以下提案非常相似的解决方案,请务必查看。

@pytest.fixture(
    params=[
        pytest.fixture_request("default_context"),
        pytest.fixture_request("extra_context"),
    ]
)
def context(request):
    """Returns all values for ``default_context``, one-by-one before it
    does the same for ``extra_context``.

    request.param:
        - {}
        - {'author': 'alice'}
        - {'project_slug': 'helloworld'}
        - {'author': 'bob', 'project_slug': 'foobar'}
    """
    return request.param

相同的辅助函数可以与 pytest.mark.parametrize 结合使用。

@pytest.mark.parametrize(
    "context, expected_response_code",
    [
        (pytest.fixture_request("default_context"), 0),
        (pytest.fixture_request("extra_context"), 0),
    ],
)
def test_generate_project(cookies, context, exit_code):
    """Call the cookiecutter API to generate a new project from a
    template.
    """
    result = cookies.bake(extra_context=context)

    assert result.exit_code == exit_code