..

Using Fixtures in Pytest to Handle Test Dependencies

Fixtures in pytest offer a great way to establish ‘things’ required to perform a test. These ‘things’ can include class instances, closures, values, or setup and teardown instructions. When utilised, fixtures can clear code from tests, leaving only the essential elements. This post discusses some of the key parts of fixtures, showing how they can be used to greatly improve testing.

Dependency injection

Dependency injection is the idea of passing objects required by another object so that they are immediately available for use. This can be achieved using fixtures, as follows:

from pytest import fixture

class App:
    def __init__(self, identifier=1):
        self.identifier = identifier

@fixture
def app():
    return App()

def test_create_app(app):
    assert app.identifier == 1

In this example, the test_create_app test function requires an instance of App, i.e. is dependent on App, to check that it can be created successfully. This dependency can be passed to, i.e. injected into, test_create_app by stating the fixture app as an argument. The app fixture will create an instance of the class App and passed it to the test function. Once it has been injected, the instance can be used as if it was created within test_create_app.

Passing values to fixtures

In some situations a fixture may require values to be able to create and inject a dependency. Introspection enables a fixture to inspect the test that is currently running and allows values to be passed to a fixture through examining either a test’s parameters or marked values. A test’s parameters and marked values are exposed by the request object, which can be injected into a test function as discussed above.

Examining parameters

The example below shows how a test’s parameters can be examined by injecting the request object into the app fixture and accessing its param attribute:

from pytest import fixture, mark

class App:
    def __init__(self, identifier):
        self.identifier = identifier

@fixture
def app(request):
    return App(request.param)

@mark.parametrize("identifier", [1, 2])
def test_create_app(identifier, app):
    assert app.identifier == identifier

This example can be extended by matching an argument name of the app fixture with an argument name of the test_create_app test function. In doing so, pytest assumes that the value of the test function argument should be passed to the fixture, as shown below:

from pytest import mark

class App:
    def __init__(self, identifier):
        self.identifier = identifier

@fixture
def app(identifier):
    return App(identifier)

@mark.parametrize("identifier", [1, 2])
def test_create_app(identifier, app):
    assert app.identifier == identifier

Marked values

Values can also be passed to a fixture by adding a marker to a test function and accessing its value using the request object.

from pytest import fixture, mark

class App:
    def __init__(self, identifier):
        self.identifier = identifier

@fixture
def app(request):
    return App(request.node.get_closest_marker("identifier").args[0])

@mark.identifier(1)
def test_create_app(app):
    assert app.identifier == 1

Creating multiple instances

Occasionally a test may require multiple instances of a dependency, however, it is not possible to inject a fixture twice into a test function due to pytest’s fixture caching mechanism. To solve this, a fixture can inject a closure into the test function, which is capable of producing multiple instances of a dependency, as known as a factory function. An example of this is shown below:

from pytest import fixture

class App:
    def __init__(self, identifier):
        self.identifier = identifier

@fixture
def app():
    def closure():
        return App()

    return closure

def test_create_app(app):
    first_app = app()
    second_app = app()

    assert first_app != second_app

Teardowns in fixtures

Fixtures can also be used to perform teardowns once a test has finished by yielding a dependency, as shown below:

class App:
    def __init__(self, identifier=1):
        self.identifier = identifier

    def close(self):
        print('Closing app...')

@fixture
def app(request):
    app = App()

    yield app

    app.close()

def test_create_app(app):
    assert app.identifier == 1

In this example, the app fixture first creates a new instance of the App class and passes this instance to the test_create_app test function. Once the test has assert that the identifier equals 1, it returns to the app fixture and calls the close() method on the App instance.

Summary

This post has discussed how pytest fixtures can be used to inject dependencies into a test function, removing setup and teardown code from inside test functions. In some scenarios, to inject a dependency some values are required by the fixture, and introspection of the test function can be used to access marked values or parameters. Also discussed was how fixtures can provide a closure for situations where a test requires multiple instances of a dependency. Finally, an example was shown of how fixtures can yield a dependency to a test function so that teardown code can be executed after the test has finished.