How to concatenate several parametrized fixtures into a new fixture in py.test?

时光总嘲笑我的痴心妄想 提交于 2019-11-30 02:51:16

问题


If I have two parametrized fixtures, how can I create a single test function that is called first with the instances of one fixture and then with the instances of the other fixture?

I guess it would make sense to create a new fixture that somehow concatenates the two existing fixtures. This works well for "normal" fixtures, but I don't seem to get it to work with parametrized fixtures.

Here is a simplified example of what I tried:

import pytest

@pytest.fixture(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest.fixture(params=[1, 2])
def upper(request):
    return "I" * request.param

@pytest.fixture(params=['lower', 'upper'])
def all(request):
    return request.getfuncargvalue(request.param)

def test_all(all):
    assert 0, all

When I run this I get this error:

request = <SubRequest 'lower' for <Function 'test_all[lower]'>>

    @pytest.fixture(params=[1, 2, 3])
    def lower(request):
>       return "i" * request.param
E       AttributeError: 'SubRequest' object has no attribute 'param'

... and the same error for upper().

What did I do wrong?

How can I fix this?


UPDATE:

There is a PyTest plugin that can be used to solve this problem: https://github.com/TvoroG/pytest-lazy-fixture.

After pip-installing this plugin, the only necessary change to the above code is the following:

@pytest.fixture(params=[pytest.lazy_fixture('lower'),
                        pytest.lazy_fixture('upper')])
def all(request):
    return request.param

Note, however, that there are some complex cases in which it will not work:

https://github.com/pytest-dev/pytest/issues/3244#issuecomment-369836702

Related PyTest issues:

  • https://github.com/pytest-dev/pytest/issues/349
  • https://github.com/pytest-dev/pytest/issues/460
  • https://github.com/pytest-dev/pytest/issues/3244

回答1:


It is not beautiful, but may be today you know the better way.

Request object inside 'all' fixture know only about own params: 'lower', 'upper'. One way using fixtures from a fixture function.

import pytest

@pytest.fixture(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest.fixture(params=[1, 2])
def upper(request):
    return "I" * request.param

@pytest.fixture(params=['lower', 'upper'])
def all(request, lower, upper):
    if request.param == 'lower':
        return lower
    else:
        return upper

def test_all(all):
    assert 0, all



回答2:


I had the exact same question (and received a similar, but distinct answer). The best solution I was able to come up with was to reconsider how I parametrize my tests. Instead of having multiple fixtures with compatible outputs, I used the fixtures as regular functions, and just parametrized your meta-fixture to accept the function name and arguments:

import pytest

def lower(n):
    return 'i' * n

def upper(n):
    return 'I' * n

@pytest.fixture(params=[
    (lower, 1),
    (lower, 2),
    (upper, 1),
    (upper, 2),
    (upper, 3),
])
def all(request):
    func, *n = request.param
    return func(*n)

def test_all(all):
    ...

In your particular case, unpacking n into a list and passing it with * is slightly overkill, but it provides generality. My case has fixtures that all accept different parameter lists.

Until pytest allows us to properly chain fixtures, this is the only way I have come up with to run 5 tests instead of 12 in your situation. You can make the list shorter with something like

@pytest.fixture(params=[
    *[(lower, i) for i in range(1, 3)],
    *[(upper, i) for i in range(1, 4)],
])

There is an actual advantage of doing it this way. You can pick and chose which tests you want to do special things to, like XFAIL, without affecting a whole swath of other tests if you have additional dependencies in your pipeline.




回答3:


There is now a solution available in pytest-cases, named fixture_union. Here is how you create the fixture union that you requested in your example:

from pytest_cases import fixture_union, pytest_fixture_plus

@pytest_fixture_plus(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest_fixture_plus(params=[1, 2])
def upper(request):
    return "I" * request.param

fixture_union('all', ['lower', 'upper'])

def test_all(all):
    print(all)

It works as expected:

<...>::test_all[lower-1] 
<...>::test_all[lower-2] 
<...>::test_all[lower-3] 
<...>::test_all[upper-1] 
<...>::test_all[upper-2] 

Note that I used pytest_fixture_plus in the above example because if you use pytest.fixture you will have to handle yourself the cases where a fixture is not actually used. This is done as follows, for example for the upper fixture:

import pytest
from pytest_cases import NOT_USED

@pytest.fixture(params=[1, 2])
def upper(request):
    # this fixture does not use pytest_fixture_plus 
    # so we have to explicitly discard the 'NOT_USED' cases
    if request.param is not NOT_USED:
        return "I" * request.param

See documentation for details. (I'm the author by the way ;) )



来源:https://stackoverflow.com/questions/24340681/how-to-concatenate-several-parametrized-fixtures-into-a-new-fixture-in-py-test

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!