Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 413
Matthias Avatar asked Jun 21 '14 10:06

Matthias


People also ask

Can fixture accept multiple parameters?

Generally, the parameter can be any object, so you can always put your fixture parameters in a suitable object. With a tuple or a list parameter, you can also access the values per index as in your example.

Can fixtures have parameters pytest?

You can pass arguments to fixtures with the params keyword argument in the fixture decorator, and you can also pass arguments to tests with the @pytest.

Can pytest fixtures use other fixtures?

A fixture can use multiple other fixtures. Just like a test method can take multiple fixtures as arguments, a fixture can take multiple other fixtures as arguments and use them to create the fixture value that it returns.

Which decorator is used for tests using multiple fixtures?

usefixtures("fixture-name") . This special decorator adds the fixture to a test class, and the fixture will be executed before any test function. Check out the logs below. There is a special usage of yield statement in Pytest that allows you to execute the fixture after all the test functions.


3 Answers

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 ;) )

like image 61
smarie Avatar answered Oct 23 '22 20:10

smarie


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.

like image 3
Mad Physicist Avatar answered Oct 23 '22 20:10

Mad Physicist


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
like image 1
Varvara Mizurova Avatar answered Oct 23 '22 20:10

Varvara Mizurova