How to use indirect parameterization when fixtures have same parameters?

Viewed 1176

I am using pytest's indirect parameterization to parameterize an upstream fixture. This has been working fine for me.

However, now I am stuck on when the upstream fixtures have the same argument names, and I want to pass them different values.

How can I use indirect parameterization when the upstream fixture to be parameterized's arguments have the same names?


Sample Code

import pytest

class Config:
    """This is a configuration object."""
    def __init__(self, a: int, b: int):
        self._a = a
        self._b = b

@pytest.fixture
def config(a: int, b: int) -> Config:
    return Config(a, b)

class Foo:
    def __init__(self, config: Config):
        """This does some behavior."""
        self.config = config

@pytest.fixture
def foo(config: Config) -> Foo:
    return Foo(config)

class Bar:
    def __init__(self, config: Config):
        """This does some other behavior."""
        self.config = config

@pytest.fixture
def bar(config: Config) -> Bar:
    return Bar(config)

class TestFooBarTogether:
    @pytest.mark.parametrize("a, b", [(1, 2)])
    def test_foo_alone(self, foo: Foo) -> None:
        """This works fine, since parameters get passed all the way to Config."""

    @pytest.mark.parametrize("a, b", [(1, 2)])
    def test_together(self, foo: Foo, bar: Bar) -> None:
        """Test interactions between the two objects.

        This does not work, because both foo and bar get the same config.
        How can I pass a different config to foo and bar, even though Config
        takes both an "a" and a "b"?
        
        I would like to pass `foo` something like (1, 2) and 
        `bar` something like (3, 4).

        """
2 Answers

Update

I don't see an option of using the config fixture when parametrizing both foo and bar with different Config instances. Dropping the config fixture and parametrizing foo and bar indirectly, I come to:

@pytest.fixture
def foo(request) -> Foo:
    return Foo(Config(*request.param))


@pytest.fixture
def bar(request) -> Bar:
    return Bar(Config(*request.param))


@pytest.mark.parametrize("foo", [(1, 2)], indirect=True)
def test_foo_alone(foo: Foo) -> None:
    assert foo.config._a == 1
    assert foo.config._b == 2


@pytest.mark.parametrize("foo,bar", [((1, 2), (3, 4))], indirect=True)
def test_together(foo: Foo, bar: Bar) -> None:
    assert foo.config._a == 1
    assert foo.config._b == 2
    assert bar.config._a == 3
    assert bar.config._b == 4

The config fixture expecting a and b parametrized wouldn't fit here anyway IMO, since it would be expected to be called new on each invocation, like a usual function - I simply use the Config constructor instead.

Original answer

You can always switch to factory fixtures if you need to ensure new objects on each config invocation. The a and b args are encapsulated in the factory, so it shouldn't be necessary to drag them along.

ConfigFactory = Callable[[], Config]

@pytest.fixture
def config(a: int, b: int) -> ConfigFactory:
    return lambda: Config(a, b)


@pytest.fixture
def foo(config: ConfigFactory) -> Foo:
    return Foo(config())


@pytest.fixture
def bar(config: ConfigFactory) -> Bar:
    return Bar(config())


@pytest.mark.parametrize("a, b", [(1, 2)])
def test_together(foo: Foo, bar: Bar) -> None:
    assert not foo.config == bar.config

A possible workaround may be to create a new fixture that accepts two different configurations. Here are two ways to do it.

@pytest.fixture
def foo_or_bar(config1: Config, config2: Config):
    return Foo(config1), Bar(config2)


class TestFooBarTogether:
    @pytest.mark.parametrize("a, b", [(1, 2)])
    def test_foo_alone(self, foo: Foo) -> None:
        """This works fine, since parameters get passed all the way to Config."""

    @pytest.mark.parametrize("config1, config2", [(Config(1, 2), Config(3, 4))])
    def test_together(self, foo_or_bar) -> None:
        """Test interactions between the two objects.

        """
        (foo, bar) = foo_or_bar
        assert foo.config._a == 1
        assert foo.config._b == 2
        assert bar.config._a == 3
        assert bar.config._b == 4

Or

@pytest.fixture
def foo_or_bar(a_b, c_d):
    (a, b) = a_b
    (c,d) = c_d
    
    config1 = Config(a,b)
    config2 = Config(c,d)

    return Foo(config1), Bar(config2)


class TestFooBarTogether:
    @pytest.mark.parametrize("a, b", [(1, 2)])
    def test_foo_alone(self, foo: Foo) -> None:
        """This works fine, since parameters get passed all the way to Config."""
        assert isinstance(foo.config, Config)

    @pytest.mark.parametrize("a_b, c_d", [((1, 2), (3, 4))])
    def test_together(self, foo_or_bar) -> None:
        """Test interactions between the two objects.

        This does not work, because both foo and bar get the same config.
        How can I pass a different config to foo and bar, even though Config 
        takes both an "a" and a "b"??

        """
        (foo, bar) = foo_or_bar
        assert foo.config._a == 1
        assert foo.config._b == 2
        assert bar.config._a == 3
        assert bar.config._b == 4
Related