9

With pytest, I can define a fixture like so:

@pytest.fixture
def foo():
    return "blah"

And use it in a test like so:

def test_blah(foo):
    assert foo == "blah"

That's all very well. But what I want to do is define a single fixture function that "expands" to provide multiple arguments to a test function. Something like this:

@pytest.multifixture("foo,bar")
def foobar():
    return "blah", "whatever"

def test_stuff(foo, bar):
    assert foo == "blah" and bar == "whatever"

I want to define the two objects foo and bar together (not as separate fixtures) because they are related in some fashion. I may sometimes also want to define a fixture that depends on another fixture, but have the second fixture incorporate the result of the first and return it along with its own addition:

@pytest.fixture
def foo():
    return "blah"

@pytest.multifixture("foo,bar")
def foobar():
    f = foo()
    return f, some_info_related_to(f)

This example may seem silly, but in some cases foo is something like a Request object, and the bar object needs to be linked to that same request object. (That is, I can't define foo and bar as independent fixtures because I need both to be derived from a single request.)

In essence, what I want to do is decouple the name of the fixture function from the name of the test-function argument, so that I can define a fixture which is "triggered" by a particular set of argument names in a test function signature, not just a single argument whose name is the same as that of the fixture function.

Of course, I can always just return a tuple as the result of the fixture and then unpack it myself inside the test function. But given that pytest provides various magical tricks for automatically matching names to arguments, it seems like it's not unthinkable that it could magically handle this as well. Is such a thing possible with pytest?

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • My attempted solution that fails: https://stackoverflow.com/questions/49808989/unpacking-multiple-arguments-in-pytest – Yair Daon Apr 13 '18 at 03:46

3 Answers3

6

You can now do this using pytest-cases:

from pytest_cases import fixture

@fixture(unpack_into="foo,bar")
def foobar():
    return "blah", "whatever"

def test_stuff(foo, bar):
    assert foo == "blah" and bar == "whatever"

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

smarie
  • 4,568
  • 24
  • 39
  • Cool! Ironic since my github ticket was just closed as a wontfix a few weeks ago, but a happy surprise! :-) – BrenBarn Jul 04 '19 at 23:07
  • Glad this could help :) Could you please update your comment with the issue url so that I can find it and append a comment pointing here? Thanks! – smarie Jul 05 '19 at 08:47
0

note: this solution not working if your fixture depends on another fixtures with parameters

Don't really know if there are any default solution in pytest package, but you can make a custom one:

import pytest
from _pytest.mark import MarkInfo


def pytest_generate_tests(metafunc):
    test_func = metafunc.function
    if 'use_multifixture' in [name for name, ob in vars(test_func).items() if isinstance(ob, MarkInfo)]:
        result, func = test_func.use_multifixture.args
        params_names = result.split(',')
        params_values = list(func())
        metafunc.parametrize(params_names, [params_values])


def foobar():
    return "blah", "whatever"


@pytest.mark.use_multifixture("foo,bar", foobar)
def test_stuff(foo, bar):
    assert foo == "blah" and bar == "whatever"


def test_stuff2():
    assert 'blah' == "blah"

So we defined pytest_generate_tests metafunction. This function

  1. checks if multifixture mark is on the test
  2. if the mark is on - it takes variables names "foo,bar" and fucntion foobar that will be executed on generation

    @pytest.mark.multifixture("foo,bar", foobar)

iHelos
  • 121
  • 4
0

You can do this with two pytest fixtures, like so:

import pytest

@pytest.fixture
def foo():
    return [object()]

# value derived from foo
@pytest.fixture
def bar(foo):
    return foo[0]

# totally independent fixture
@pytest.fixture
def baz():
    return object()


def test_fixtures(foo, bar, baz):
    assert foo[0] is bar
    assert foo[0] is not baz
    # both assertions will pass

Here the foo and bar fixtures have a specific relation between their values (referencing the same object). This is the same result as you wanted from your multi fixture. (the baz fixture is included for comparison, and uses an unrelated instance of object().

If both values are derived from some shared context you can put the shared context in a fixture, and then derive the final results independently.

@pytest.fixture
def shared():
    return [object()]

@pytest.fixture
def derived_1(shared):
    return shared[0]

@pytest.fixture
def derived_2(shared):
    return shared[-1]

def test_derived(derived_1, derived_2):
    assert derived_1 is derived_2
Will
  • 415
  • 8
  • 15