Intrastellar Explorer
Intrastellar Explorer

Reputation: 2411

pytest: passing keyword arg to fixture using pytest.mark.parametrize with indirect parameterization

I have a pytest.fixture that has one positional arg and one keyword arg.

Per Pass a parameter to a fixture function, one can pass args to a fixture using pytest.mark.parametrize with the indirect arg set to the fixture's name.

Please see the below sample code.

import pytest

class Foo:
    def __init__(self, a: str, b: str):
        self.a = a
        self.b = b

@pytest.fixture
def a() -> str:
    return "alphabet"

@pytest.fixture
def foo_obj(a: str, b: str = "bar") -> Foo:
    return Foo(a, b)

@pytest.mark.parametrize("foo_obj", [("applesauce", "baz")], indirect=["foo_obj"])
def test_thing(foo_obj) -> None:
    assert foo_obj.a == "applesauce"
    assert foo_obj.b == "baz"

This test fails currently: the "applesauce" and "baz" aren't getting passed into the fixture foo_obj.

My questions:

  1. What am I doing wrong in passing the args to the fixture foo_obj?
  2. Is it possible to only enter the kwarg b in the pytest.mark.parametrize decorator call?

Versions

Python==3.8.5
pytest==6.0.1

Upvotes: 1

Views: 2400

Answers (2)

Intrastellar Explorer
Intrastellar Explorer

Reputation: 2411

The answer from @MrBeanBremen put me on the scent of this answer, which talks about implicit indirect parametrization.

@pytest.fixture
def foo_obj(a: str, b: str) -> Foo:  # Note: b was changed to positional arg
    print("hi")
    return Foo(a, b)

@pytest.mark.parametrize("a, b", [("applesauce", "baz")])
def test_thing(foo_obj) -> None:
    assert foo_obj.a == "applesauce"
    assert foo_obj.b == "baz"

The tests pass in the above case.

It seems this implicit indirect parametrization can be used for positional args, but not for keyword args. For example, if b was left as a keyword argument in the fixture foo_obj, pytest fails with the message: In test_thing: function uses no argument 'b'.

I am left wondering, is it possible to have keyword args be parameterized in this manner?


Trying a 2nd time to use keyword arg

I decided to make a new attempt again pulling on the answer from @MrBeanBremen and also this answer: Provide default argument value for py.test fixture function

@pytest.fixture
def a() -> str:
    return "alphabet"

@pytest.fixture
def foo_obj(request, a) -> Foo:
    return Foo(request.param.get("a", a), request.param.get("b", "bar"))

@pytest.mark.parametrize("foo_obj", [dict(a="applesauce", b="baz")], indirect=True)
def test_thing(foo_obj) -> None:
    # Override both defaults, passes
    assert foo_obj.a == "applesauce"
    assert foo_obj.b == "baz"

@pytest.mark.parametrize("foo_obj", [dict(b="baz")], indirect=True)
def test_thing_only_b(foo_obj) -> None:
    # Use the default for a
    assert foo_obj.a == "alphabet"
    assert foo_obj.b == "baz"

This works! I feel as if it's a little convoluted with the foo_obj fixture accepting two args, when it sometimes uses one or the other.

Upvotes: 5

MrBean Bremen
MrBean Bremen

Reputation: 16815

I think you are mixing two things here: passing parameters to a fixture via request.params, and basing a fixture an another fixture (or fixtures).

To use the parameters used in mark.parametrize in your fixture, you have to take them from request.params, as shown in your linked question:

@pytest.fixture
def foo_obj(request) -> Foo:
    return Foo(request.param[0], request.param[1])

What you are doing instead, is basing foo_obj on the fixture a, meaning, that a is the result of that fixture (e.g. it is always "a"), and a constant parameter b. Both have nothing to do with the values set by parametrize - these are set in request.params as shown above.

Upvotes: 3

Related Questions