Jake Trent

Pass Params to Pytest Fixture

This is what it takes to pass params to pytest fixtures.

Just Use Functions

Instead of using pytest fixtures, the easier method would be to just create a function that sets up and tears down what you need. Like this:

def test_something():
    my_fixture = setup_something('special sauce')

    # assertion...

    teardown_something(my_fixture)

def setup_something(special_param):
    # setup ...
    return my_fixture_with_special_param

def teardown_something(my_fixture):
    # cleanup my_fixture...

The problem with this approach is in the case of assertion failure. In that case, teardown_something would be skipped, and cleanup would not happen.

try/finally in Test?

I didn’t actually try this method because it seems like I’m working against/outside the test framework, and the test aesthetics of every test body needing wrapped in try/finally feel yucky. But maybe we could ensure teardown_something is called with:

def test_something():
    my_fixture = None
    try:
        my_fixture = setup_something('special sauce')

        # assertion...
    finally:
        teardown_something(my_fixture)

def setup_something(special_param):
    # setup ...
    return my_fixture_with_special_param

def teardown_something(my_fixture):
    # cleanup my_fixture...

Does this actually work? I haven’t tried it yet, but it seems like it should.

Using pytest Fixtures

Let’s try the pytest way. pytest has its own way to set up and tear down fixtures:

import pytest

@pytest.fixture(name="my_fixture")
def setup_something():
    my_fixture = # setup...

    yield my_fixture

    # cleanup my_fixture...

def test_something(my_fixture):
    # assertion...

A pytest fixture is created with the pytest.fixture decorator. It is identified by name. The thing that the generator function yields is the fixture value. When the generator is resumed, it will have a chance to clean up the fixture. The generator is guaranteed to be resumed, allowing this cleanup, even in the case of assertion failure.

But we have removed a key feature of our fixture in this example. setup_something now doesn’t take special_param any more. We need to re-create a way to allow this parameter using pytest fixtures.

Params in pytest fixtures

The difficulty lies in the fanciness of how a test function gains access to the fixture. The name of the fixture is passed as a parameter to the test function. pytest uses reflection to load the proper test function. This function invocation happens behind the scenes. We do not invoke the function, so we have difficulty in passing arguments. The way we can get pytest to take a parameter is not pretty, as you will see.

We will accomplish this via markers. Markers are a pytest concept that usually allows identifying tests to be a part of certain categories (eg, integration tests). But we will use it to pass parameters.

Markers must be registered in order to avoid warnings. This can be done in pytest.ini or elsewhere:

[pytest]
markers =
    my_fixture_data(special_param): param to pass to something fixture

The marker is named my_fixture_data arbitrarily. In this case, the name is meant to associate it with the my_fixture fixture. The _data suffix is meant to generally mean that this is a parameter associated with its use.

Then mark the test that uses the fixture:

import pytest

@pytest.mark.my_fixture_data("special sauce")
def test_something(my_fixture):
    # assertion only...

The value "special sauce" is passed as an argument to the marker function.

Now we must make the fixture itself use the marker:

@pytest.fixture(name="my_fixture")
def setup_something():
    marker = request.node.get_closest_marker("my_fixture_data")
    special_param = marker.args[0]
    my_fixture_with_special_param = # setup ...

    yield my_fixture_with_special_param

    # cleanup my_fixture_with_special_param...

From within the test, the closest marker is found. This will be the marker decorating the test function. It’s found by name. Then its first argument is accessed. This is the "special sauce" value. The setup of the fixture can then use this parameter value.

Convoluted? Yes. Does it work? Yes.

Do you know know of a better way to pass parameters to pytest fixtures? I hope so.