Advanced Fixtures

This is part of a series of posts on Python unit testing.

In Fixtures we saw that fixtures are pytest’s alternative to setup() and teardown() methods or to test helper functions. This post will try to show that pytest fixtures are much more than just a more convenient alternative to helper functions, by explaining some of their more advanced features.

Fixtures can use other fixtures

We saw in Simple Fixtures that if a test wants to use a fixture it simply takes that fixture as an argument, by name. Here’s a test that uses the pyramid_request fixture:

def test_current_page_defaults_to_1(pyramid_request):
    ...

Pytest runs the pyramid_request() fixture function and passes the fixture function’s return value into test_current_page_defaults_to_1(pyramid_request) as the pyramid_request argument.

What if a fixture function wants to have access to another fixture object? It can just take that other fixture as argument, by name, exactly like a test function would.

As an example we’ll look at the tests for UserSearchController. UserSearchController is the Pyramid view class for the “user search” page, which is the page that lets you search and browse all of a user’s annotations, for example https://hypothes.is/users/jeremydean.

The factories fixture (see this tutorial’s post on factories) is often used by other fixtures. For example, the user fixture in TestUserSearchController uses it to return a User object with a registration date, URI and ORCID:

class TestUserSearchController(object):

    ...

    @pytest.fixture
    def user(self, factories):
        return factories.User(
            registered_date=datetime.datetime(year=2016, month=8, day=1),
            uri='http://www.example.com/me',
            orcid='0000-0000-0000-0000',
        )

Now test methods in TestUserSearchController can just take user as argument and get this User object, rather than each individual test using the factories fixture and constructing the User itself:

    def test_something(self, user):
        # Some test code that uses `user`.

Since the user() fixture method takes the factories fixture as argument, pytest knows that it needs to call factories() and get its return value before it can call user(). Pytest does something like this for each test that uses the user fixture:


test_obj          = TestSearchController()
factories_fixture = conftest.factories()
user_fixture      = test_obj.user(factories=factories_fixture)
test_obj.test_something(user=user_fixture)

Note that by using the user fixture the test has indirectly caused the factories fixture function to be called as well.

A fixture can use multiple other fixtures

Just like a test method can take multiple fixtures as arguments, a fixture can take multiple other fixtures as arguments and use them to create the fixture value that it returns.

In order to test one of UserSearchController’s methods the test first needs to create a UserSearchController object. UserSearchController.__init__() requires a user as argument, so the test will use the user fixture from above. __init__() also requires a Pyramid request as argument, so the test will also use the pyramid_request fixture. A first pass at a test might look like this:

class TestUserSearchController(object):

    def test_some_search_controller_method(self, user, pyramid_request):
        controller = activity.UserSearchController(user, pyramid_request)

        # Test something using `controller`.

    @pytest.fixture
    def user(self, factories):
        ...

    @pytest.fixture
    def pyramid_request(self):
        ...

But controller = activity.UserSearchController(user, pyramid_request) would be duplicated in every UserSearchController test, and every test would need both the user and pyramid_request arguments. We can get rid of this duplication by making a fixture for controller:


class TestUserSearchController(object):

    def test_some_search_controller_method(self, controller):
        # Test something using `controller`.

    def test_some_other_search_controller_method(self, controller):
        # Test something else using `controller`.

    @pytest.fixture
    def controller(self, user, pyramid_request):
        return activity.UserSearchController(user, pyramid_request)

    @pytest.fixture
    def user(self, factories):
        ...

    @pytest.fixture
    def pyramid_request(self):
        ...

The controller fixture takes the user and pyramid_request fixtures as arguments and returns the UserSearchController object. Now each test method can just receive the UserSearchController as the controller argument and use it directly.

Since controller takes both user and pyramid_request as arguments pytest knows that, for each test that uses controller, it needs to run the user and pyramid_request fixtures first before it can pass their results into the controller fixture. What pytest does for each test method that uses controller is something like this:


test_obj                = TestSearchController()
factories_fixture       = conftest.factories()
user_fixture            = test_obj.user(factories=factories_fixture)  
pyramid_request_fixture = test_obj.pyramid_request()
controller_fixture      = test_obj.controller(user=user_fixture,
                                              pyramid_request=pyramid_request_fixture)
test_obj.test_something(controller=controller_fixture)

Note that the user and pyramid_request fixtures will be called, even though the test doesn’t directly use those fixtures (either as arguments or via usefixtures) - the test uses the controller fixture, which uses the user and pyramid_request fixtures, so the test indirectly uses the user and pyramid_request fixtures as well.

Fixtures override other fixtures with the same name

In Simple Fixtures we saw that @pytest.fixture functions can be defined in conftest.py files, in the test modules themselves, or as methods on the test classes. A fixture defined in a class overrides any fixtures with the same name defined higher up in the module or in a conftest.py file. Any test methods in that class that use that fixture will call the fixture method from the class rather than the one defined elsewhere. In the same way a fixture defined in a module overrides any fixtures with the same name defined higher up in a conftest.py file.

By overriding fixtures a certain group of tests (a test class or a test module) can use its own different version of that fixture, without having to come up with a different fixture name. Another class or module can use another version. And others can just use the “default” version defined higher up.

In the Hypothesis tests you’ll often see fixture overriding used with the pyramid_request fixture. A generic pyramid_request fixture is defined in conftest.py and many tests simply use that generic fixture. But often tests need something different - for example a request object that represents a request from a logged-in user. The TestUserSearchController class provides its own pyramid_request fixture that returns a logged-in request from the user represented by the user fixture:


class TestUserSearchController(object):

    def test_something(self, pyramid_request):
        ...

    ...

    @pytest.fixture
    def user(self, factories):
        return ...

    @pytest.fixture
    def pyramid_request(self, user):
        request = testing.DummyRequest(db=db_session, feature=fake_feature)
        request.auth_domain = text_type(request.domain)
        request.create_form = mock.Mock()
        request.matched_route = mock.Mock()
        request.registry.settings = pyramid_settings
        request.is_xhr = False

        pyramid_request.matchdict['username'] = user.username
        pyramid_request.authenticated_user = user

        return pyramid_request

For test_something(self, pyramid_request) pytest will call TestSearchController.pyramid_request() rather than the pyramid_request fixture function in conftest.py.

An overriding fixture can take the “parent” fixture as argument

The problem with TestUserSearchController’s custom pyramid_request fixture above is that it duplicates all the code from the generic pyramid_request fixture in conftest.py. The first half a dozen lines of the method are the same as from the other pyramid_request, and only the last couple of lines contain code specific to TestUserSearchController.

Every test module or class that overrides pyramid_request with its own custom version is going to duplicate the same lines.

To get around this, an overriding pyramid_request fixture can simply take the higher up pyramid_request fixture by name as argument. This is how the real TestUserSearchController.pyramid_request works:


class TestUserSearchController(object):

    ...

    @pytest.fixture
    def pyramid_request(self, pyramid_request, user):
        pyramid_request.matchdict['username'] = user.username
        pyramid_request.authenticated_user = user
        return pyramid_request

The pyramid_request argument that pytest passes to this pyramid_request fixture method will be the next higher up pyramid_request fixture that it finds - a pyramid_request fixture defined in the test module or in a conftest.py file. In this example it happens to be in conftest.py. When a test method in TestSearchController uses the pyramid_request feature, pytest does something like this:


test_obj                       = TestUserSearchController()
factories_fixture              = conftest.factories()
user_fixture                   = test_obj.user(factories=factories_fixture)
higher_pyramid_request_fixture = conftest.pyramid_request()
lower_pyramid_request_fixture  = test_obj.pyramid_request(
    user=user_fixture,
    pyramid_request=higher_pyramid_request_fixture
)
test_obj.test_something(pyramid_request=lower_pyramid_request_fixture)

Try not to over-reuse fixtures

We introduced fixtures by pointing out that they’re an effective way to avoid coupling test methods together like setup() and teardown() methods can - each test method just depends on whichever fixtures that test method needs and nothing else, rather than having a single setup() method that’s run for every test method in the class.

It’s important to remember then, especially when using fixture overriding, not to make a fixture method more complicated by trying to make a single complex fixture that can be shared between many test methods with different requirements.

For example a test class may contain some test methods that require a Pyramid request for a logged-in user, some that require an unauthenticated request, some that require a request from a group administrator or a request with certain query parameters, etc.

Don’t struggle to build a complex pyramid_request fixture method in the class that can meet all these needs, perhaps by returning multiple request objects or a customizable request object etc.

It’s easier just to write separate, independent fixtures and have each test just use the one it needs:

class TestUserSearchController(object):

    def test_with_unauthorized_request(self, unauthorized_request):
        ...

    def test_with_request_from_group_admin(self, group_admin_request):
        ...

    ...

    @pytest.fixture
    def unauthorized_request(self, pyramid_request):
        ...

    @pytest.fixture
    def group_admin_request(self, pyramid_request):
        ...

Both the unauthorized_request and the group_admin_request fixtures depend on the generic pyramid_request fixture and then do different customization to it. Each test that needs an unauthorized request uses that fixture, each test that needs a group admin request uses that fixture. This is a clean and simple way to avoid duplication while also not coupling tests together.

In the above example, if a single test method used both the unauthorized_request and the group_admin_request fixtures it might not work as you’d expect it to. Can you guess why? Both of those fixtures depend on the same pyramid_request fixture, let’s look at what happens when a fixture gets used multiple times…

A test and a fixture can both use the same fixture

We’ve seen that test methods use fixtures by taking the fixture as an argument, and that fixtures can also use other fixtures in the same way. A useful technique can be to have a fixture that is both used by other fixtures and used by the test method itself.

For example, consider a test that the Back link redirects back to the user page:


class TestUserSearchController(object):

    ...

    def test_back_redirects_to_user_search(self, controller, user, pyramid_request):
        """It should redirect and preserve the search query param."""
        pyramid_request.params = {'q': 'foo bar', 'back': ''}

        result = controller.back()

        assert isinstance(result, httpexceptions.HTTPSeeOther)
        assert result.location == (
            'http://example.com/users/{username}?q=foo+bar'.format(
                username=user.username))

    @pytest.fixture
    def user(self, factories):
        return ...

    @pytest.fixture
    def pyramid_request(self, user):
        ...
        return pyramid_request

In order to do its work test_back_redirects_to_user_search() needs both a pyramid_request object representing an HTTP request from a logged-in user, and it needs the user object for that logged-in user itself, so it takes both as arguments. This means that the user fixture is used twice - once by the pyramid_request fixture, and then again by the test itself. The important thing to understand is that the user fixture is only called once, and the same user object is passed first to the pyramid_request fixture and then to the test. This is what pytest does:


test_obj                = TestUserSearchController()
factories_fixture       = conftest.factories()
user_fixture            = test_obj.user(factories=factories_fixture)
pyramid_request_fixture = test_obj.pyramid_request(user=user_fixture)
controller_fixture      = test_obj.controller()
test_obj.test_back_redirects_to_user_search(
    controller=controller_fixture,
    user=user_fixture,
    pyramid_request=pyramid_request_fixture)

Multiple fixtures can both use the same fixture

Just as a test and a fixture can both use the same other fixture, multiple fixtures can all use the same other fixture as well. You can see an example of this in TestSearchController’s controller and pyramid_request fixtures, both of which depend on its user fixture:


class TestUserSearchController(object):

    ...

    @pytest.fixture
    def controller(self, user, pyramid_request):
        return activity.UserSearchController(user, pyramid_request)

    @pytest.fixture
    def pyramid_request(self, user):
        ...
        return pyramid_request

    @pytest.fixture
    def user(self):
        return ...

Again, the user fixture will be called only once and the one user object that it returns will be passed to both the pyramid_request and controller fixtures (as well as any other fixtures that the test uses, directly or indirectly, that take the user fixture, and to the test itself if it takes the user fixture directly).

Pytest figures out what order it needs to call the fixtures in according to their dependencies - it knows that it needs to call the user fixture first in order to get the user object to pass in to the pyramid_request and controller fixtures. Notice that the controller fixture actually depends on the pyramid_request fixture as well - so pytest knows that it has to call both user and pyramid_request before it can call controller.

Conclusion

These last couple of posts have covered most of the fixture-related techniques that you’ll find in the Hypothesis Python tests. We haven’t covered everything, check out pytest’s documentation on fixtures for all the details (for example: parametrizing fixtures and inspecting the fixture request context), but you should now have enough of a basis to figure out what’s going on with all these fixtures in our tests.

Next post: mock.

Sean Hammond,