如何为 Pytest fixture 参数化?

113
考虑以下的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 fixture,timeline,它本身具有属性instances。在测试中,对这个属性进行迭代,只有当每个instancetimeline.instances中都满足断言时,测试才会通过。
然而,我实际想做的是生成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的fixtures中,函数“变成”了它的返回值,但在测试参数化时,似乎尚未发生这种情况。我该如何以期望的方式设置测试?

1
这个回答解决了你的问题吗?将参数传递给fixture函数 - congusbongus
8个回答

90

通过间接参数化,实际上可以实现这一点。

这个例子使用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__])

还可参见此类似问题


4
测试结果为 FAILED indir-param.py::test_timeline[timeline0] - assert (1 % 2) == 0 :-) (对答案没有影响)。 - gerrit
2
是的,它应该失败。1除以2没有余数(模数)为0。 - wordsforthewise

61

你可以使用@pytest.fixture()params参数,而不是间接参数化或者下面我提供的有点糟糕的继承方法。我认为这可能是最简单的解决方案。

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/zh/latest/how-to/fixtures.html#parametrizing-fixtures


48
这是一个令人困惑的话题,因为人们往往认为fixtures和parameters是同一回事。实际上它们并不相同,也不是在pytest运行的同一阶段收集的。按设计,参数应该在测试被收集时(测试节点列表正在构建中)进行收集,而fixtures应该在测试节点被运行时(测试节点列表已固定且不可修改)执行。因此,fixture的内容不能用来改变测试节点的数量 - 这是参数的作用。详细答案请参见我的这里
这就是为什么你的问题 "as is" 没有解决方案的原因:你应该将Timeline实例放在一个参数中,而不是一个fixture中。就像这样:
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中使用fixture。但即使你可以,也不会改变任何事情。

由于这个功能现在在pytest-cases中可用(我是作者),你可以自己尝试。使你的示例即使使用了这个功能,仍然能够正常工作的唯一方法是:从fixture中删除列表。所以最终你会得到:

import pytest
from pytest_cases import parametrize

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

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

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

这个例子与之前的例子相同,只是多了一层,可能是多余的。 注意:还可以参考这个讨论

这里的重要方面是,在测试方法签名中,参数应该被称为request。我提到这一点是因为我认为这个名称是任意的。 - Victor Augusto

23

间接参数化的使用是有效的,但我发现需要使用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

timeline装置依赖于另一个称为instances的装置,其默认值为[0,0,0],但在实际测试中,我们使用parametrizeinstances注入一系列不同的值。

我认为这样做的好处在于每个东西都有一个好名字,而且您不需要传递indirect=True标志。


21

4
这是不正确的,parametrize标记接受indirect=True或indirect=['specific', 'named']来指定哪些参数将调用各自对应的固件。 - msudder
1
你的帖子中提到的建议已经移动到GitHub上,链接如下:https://github.com/pytest-dev/pytest/issues/349。 - Romain
4
正如@msudder所评论的那样,请查阅间接参数化 - lifebalance

4

使用原始的pytest是可能的

test_numbers.py

import pytest


@pytest.fixture(params=[1, 2, 3, 4, 5])
def number(request):
    yield request.param


def test_number_is_integer(number):
    assert isinstance(number, int)

输出

test_numbers.py::test_number_is_integer[1] PASSED
test_numbers.py::test_number_is_integer[2] PASSED
test_numbers.py::test_number_is_integer[3] PASSED
test_numbers.py::test_number_is_integer[4] PASSED
test_numbers.py::test_number_is_integer[5] PASSED

1
例如,将夹具名称设置为fruits并将indirect=True设置为@pytest.mark.parametrize(),可以通过fruits()夹具间接地将AppleOrangeBanana传递给test(),如下所示。*我的答案解释了@pytest.mark.parametrize()indirect=Trueindirect=False之间的区别。
import pytest

@pytest.fixture
def fruits(request):
    return f'fruits {request.param}'

@pytest.mark.parametrize(
    "fruits", 
    ("Apple", "Orange", "Banana"), 
    indirect=True # Here
)
def test(fruits):
    print(fruits)
    assert True

输出:

$ pytest -q -rP
...                              [100%]
=============== PASSES ================ 
_____________ test[Apple] _____________ 
-------- Captured stdout call --------- 
fruits Apple
____________ test[Orange] _____________ 
-------- Captured stdout call --------- 
fruits Orange
____________ test[Banana] _____________ 
-------- Captured stdout call --------- 
fruits Banana
3 passed in 0.10s

1
你的parametrize paramset正在引用函数。也许你想引用Timeline.instances而不是fixture函数/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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接