1

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
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119

2 Answers2

5

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.

Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • 1
    Ah, nice! I think I have seen this before, but completely forgot about it - I couldn't find it in the documentation. – MrBean Bremen Sep 15 '20 at 20:14
  • Okay check it now @MrBeanBremen. Any ideas as to how I could clean up the 2nd part? – Intrastellar Explorer Sep 15 '20 at 20:37
  • This looks very good to me - the way to use `dict.get` to have a default that behaves like a keyword arg is probably the best you can do. As for the 2 arguments (if you mean `request` and `a`) - they serve different purposes. The first is needed to get the paramatrized values, the second allows to customize one of the default values in a separate fixture, so as long as you to customize one default value (or both) , you need the additional fixture argument (or two...) – MrBean Bremen Sep 16 '20 at 16:28
3

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.

MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46