如何参数化 Pytest 装置

考虑以下 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。此属性在测试中迭代,以便只有在 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 fixture 中,函数的返回值是“成为”的,但是在测试被参数化的时候,这似乎还没有发生。如何按照所需的方式设置测试?

110089 次浏览

From Using fixtures in pytest.mark.parametrize pytest issue, it would appear that it is currently not possible to use fixtures in pytest.mark.parametrize.

This is actually possible via indirect parametrization.

This example does what you want with 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__])

Also see this similar question.

The use of indirect parametrization works, but I find the need to use request.param as a magic, unnamed variable a little awkard.

Here's a pattern I've used. It's awkward in a different way, arguably, but perhaps you'll prefer it too!

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

the timeline fixture depends on another fixture called instances, which has a default vale of [0,0,0], but in the actual test, we use parametrize to inject a series of different values for instances.

the advantage as I see it is that everything has a good name, plus you don't need to pass that indirect=True flag.

instead of indirect parametrization, or my hacky solution below involving inheritance, you can also just use the params argument to @pytest.fixture() -- i think this is the simplest solution maybe?

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

This is a confusing topic, because people tend to think that fixtures and parameters are the same thing. They are not and are not even collected in the same phase of the pytest run. By design, parameters are meant to be collected when tests are collected (the list of test nodes is under construction), while fixtures are meant to be executed when test nodes are run (the list of test nodes is fixed and cannot be modified). So fixture contents can not be used to change the number of test nodes - this is the role of parameters. See also my detailed answer here.

This is the reason why your question has no solution "as is": you should put the Timeline instances in a parameter, not in a fixture. Like this:

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 mentions another topic that IMHO adds confusion here because it is not directly related to your question. Since it is mentioned, and even accepted as the solution, let me elaborate: indeed in pytest, you can not use a fixture in a @pytest.mark.parametrize. But even if you could that would not change anything.

Since this feature is now available in pytest-cases (I'm the author), you can try yourself. The only way to make your example work even with that feature, is still the same: to remove the list from the fixture. So you end up with:

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

Which is the same than previous example with an extra, probably useless, layer. Note: see also this discussion.

Your parametrize paramset is referenceing the function. Perhaps you mean to reference Timeline.instances, not the fixture function /timeline/:

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

I think you are looking to add a mark xfail to one set of parameters, perhaps with a reason?

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

It is possible with vanilla 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)

Output

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