如何参数化 Pytest 夹具

Posted

技术标签:

【中文标题】如何参数化 Pytest 夹具【英文标题】:How to parametrize a Pytest fixture 【发布时间】:2017-07-02 21:42:52 【问题描述】:

考虑以下 Pytest:

import pytest

class TimeLine(object):
    instances = [0, 1, 2]

@pytest.fixture
def timeline():
    return TimeLine()

def test_timeline(timeline):
    for instance in timeline.instances:
        assert instance % 2 == 0

if __name__ == "__main__":
    pytest.main([__file__])

测试test_timeline 使用Pytest 夹具timeline,它本身具有instances 属性。该属性在测试中被迭代,因此只有当断言对timeline.instances 中的每个instance 都成立时,测试才会通过。

然而,我实际上想做的是生成 3 个测试,其中 2 个应该通过,其中 1 个会失败。我试过了

@pytest.mark.parametrize("instance", timeline.instances)
def test_timeline(timeline):
    assert instance % 2 == 0

但这会导致

AttributeError: 'function' object has no attribute 'instances'

据我了解,在 Pytest 固定装置中,函数“成为”它的返回值,但这似乎在测试参数化时还没有发生。如何以所需的方式设置测试?

【问题讨论】:

这能回答你的问题吗? Pass a parameter to a fixture function 【参考方案1】:

这实际上可以通过indirect parametrization 实现。

这个例子用 pytest 3.1.2 做你想做的事:

import pytest

class TimeLine:
    def __init__(self, instances):
        self.instances = instances

@pytest.fixture
def timeline(request):
    return TimeLine(request.param)

@pytest.mark.parametrize(
    'timeline',
    ([1, 2, 3], [2, 4, 6], [6, 8, 10]),
    indirect=True
)
def test_timeline(timeline):
    for instance in timeline.instances:
        assert instance % 2 == 0

if __name__ == "__main__":
    pytest.main([__file__])

另见this similar question。

【讨论】:

您的测试失败,FAILED indir-param.py::test_timeline[timeline0] - assert (1 % 2) == 0 :-)(我知道答案并不重要)。【参考方案2】:

除了间接参数化,或者我下面涉及继承的 hacky 解决方案,您也可以只使用 params 参数到 @pytest.fixture() -- 我认为这可能是最简单的解决方案?

import pytest

class TimeLine:
    def __init__(self, instances=[0, 0, 0]):
        self.instances = instances


@pytest.fixture(params=[
    [1, 2, 3], [2, 4, 5], [6, 8, 10]
])
def timeline(request):
    return TimeLine(request.param)


def test_timeline(timeline):
    for instance in timeline.instances:
        assert instance % 2 == 0

https://docs.pytest.org/en/latest/how-to/fixtures.html#parametrizing-fixtures

【讨论】:

【参考方案3】:

这是一个令人困惑的话题,因为人们倾向于认为夹具和参数是一回事。它们不会,甚至不会在 pytest 运行的同一阶段收集。按照设计,参数是在收集测试时收集的(测试节点列表正在构建中),而夹具是在测试节点运行时执行的(测试节点列表是固定的,不能修改)。所以fixture内容不能用来改变测试节点的数量——这是参数的作用。另请参阅我的detailed answer here。

这就是您的问题没有“原样”解决方案的原因:您应该将 Timeline 实例放在参数中,而不是固定装置中。像这样:

import pytest

class TimeLine(object):
    instances = [0, 1, 2]

@pytest.fixture(params=TimeLine().instances)
def timeline(request):
    return request.param

def test_timeline(timeline):
    assert timeline % 2 == 0

Kurt Peek's answer 提到了另一个主题,恕我直言,这里增加了混乱,因为它与您的问题没有直接关系。既然提到了,甚至被接受为解决方案,让我详细说明一下:确实在 pytest 中,您不能在@pytest.mark.parametrize 中使用固定装置。但即使你能这样做也不会改变任何事情。

由于此功能现已在pytest-cases 中作为测试版提供(我是作者),您可以自己尝试。即使使用该功能,使您的示例工作的唯一方法仍然是相同的:从夹具中删除列表。所以你最终得到:

import pytest
from pytest_cases import parametrize, fixture_ref

class TimeLine(object):
    instances = [0, 1, 2]

@pytest.fixture(params=TimeLine().instances)
def timeline(request):
    return request.param

@parametrize("t", [fixture_ref(timeline)])
def test_timeline(t):
    assert t % 2 == 0

这与前面的示例相同,但多了一个可能无用的层。 注意:另见this discussion。

【讨论】:

【参考方案4】:

使用间接参数化是可行的,但我发现需要使用request.param 作为一个神奇的、未命名的变量有点尴尬。

这是我使用的一种模式。可以说,它以不同的方式很尴尬,但也许你也会更喜欢它!

import pytest

class TimeLine:
    def __init__(self, instances):
        self.instances = instances


@pytest.fixture
def instances():
    return [0, 0, 0]


@pytest.fixture
def timeline(instances):
    return TimeLine(instances)


@pytest.mark.parametrize('instances', [
    [1, 2, 3], [2, 4, 5], [6, 8, 10]
])
def test_timeline(timeline):
    for instance in timeline.instances:
        assert instance % 2 == 0

timelinefixture 依赖于另一个名为instances 的fixture,它的默认值为[0,0,0],但在实际测试中,我们使用parametrizeinstances 注入了一系列不同的值。

在我看来,它的优点是所有东西都有一个好名字,而且你不需要传递 indirect=True 标志。

【讨论】:

【参考方案5】:

Using fixtures in pytest.mark.parametrize pytest 问题来看,目前似乎无法在 pytest.mark.parametrize 中使用固定装置。

【讨论】:

这是不正确的,参数化标记接受indirect=True 或indirect=['specific', 'named'] 来说明哪些参数将调用各自的fixture。 您帖子中引用的提案已移至 GitHub,网址为 github.com/pytest-dev/pytest/issues/349。 正如@msudder 评论的那样,查找indirect parameterization【参考方案6】:

您的参数化参数集正在引用该函数。也许您的意思是引用 Timeline.instances,而不是夹具函数 /timeline/:

@pytest.mark.parametrize("instance", Timeline.instances)
def test_func(instance):
    pass

我认为您希望为一组参数添加标记 xfail,也许是有原因的?

def make_my_tests():
    return [0, 2, mark.xfail(1, reason='not even')]

@mark.parametrize('timeline', paramset=make_my_tests(), indirect=True)
def test_func(timeline):
    assert timeline % 2 == 0

【讨论】:

以上是关于如何参数化 Pytest 夹具的主要内容,如果未能解决你的问题,请参考以下文章

在参数化的夹具中缓存测试数据

如何通过自定义装饰器提供非夹具 pytest 参数?

pytest如何跳过参数化excel里面某些条件的数据

pytest自动化测试框架详解+mark标记+fixture夹具

pytest自动化测试框架详解+mark标记+fixture夹具

Pytest进阶之参数化