This post covers the basics of pytest fixtures and how we use them at Hypothesis.
A “test fixture” is some code that needs to be run in order to set things up for a test. When we created the objects that we needed to pass into the method we were testing in the arrange part of the arrange, act, assert recipe, those were fixtures. Test factories are helpers for easily creating fixtures. Fixtures don’t have to be objects that we pass into the method we’re testing as arguments, they can be anything that needs to be set up for a test. For example if we want to test what a method does when there are three documents in the database, setting up the db with those three documents is a fixture.
Setup and teardown methods
Most unit testing frameworks, for example
the standard unittest library that ships with Python,
use the familiar setup()
and teardown()
methods to set up all the necessary
fixtures before running a test method and, if necessary, tear things down after
running the test (before running the next test). The test code might look
something like this:
class MyTestClass(object):
def setup(self):
self.fixture_object = SomeClass()
def test_something(self):
# Some test code that uses self.fixture_object
The test framework runs the setup()
method once before it runs each test
method (and the teardown()
method, if there is one, gets run once after each test).
The drawbacks of setup()
methods begin to show when a test class contains
different test methods that use different fixture objects or setup:
class MyTestClass(object):
def setup(self):
self.first_fixture_object = SomeClass()
self.second_fixture_object = SomeOtherClass()
def test_something(self):
# Some test code that uses self.first_fixture_object
def test_something_else(self):
# Some test code that uses self.second_fixture_object
...
Some of the test methods only need first_fixture_object
, and some
only need second_fixture_object
, but both fixture
objects get created for each test. In a small example this isn’t important but
over a large test base unnecessarily running a lot of unused fixtures for a lot
of tests can start to add significantly to how long it takes to run the tests
every time any developer needs to do so.
As they get bigger and more complicated tests like this can also become hard
to read. A whole lot of setup is done in the setup()
method, a lot of which
may be depended upon by the test method that you’re currently trying to
understand, and a lot of it not. You have to scan the whole test method body
looking for all the self.something
s to see what fixtures this test depends
on, and then look in setup()
to see what each of these fixtures is.
Worse, if a fixture sets up something in the environment, for example adding some objects to the test database, then a test method may be depending on that fixture (assuming that certain objects exist in the db) without explicitly referencing that fixture in the test body at all.
It can quickly get to the point where setup()
code can be difficult to change
without potentially breaking multiple test methods for unclear reasons, and
where it can become hard to tell exactly what a given test method is really testing.
The problem gets worse when different test fixtures conflict with each other.
This can happen when there’s a singleton resource such as a database.
If one test needs to test what happens when there are no users in the database,
and another needs to test what happens when there are three users in the
database, well a setup()
method can’t setup the database with zero users in
it and also setup the same database with three users in it as the same time.
Hacking around this kind of issue can lead to hard to read code and some nasty
coupling between test methods.
One solution would be to break the methods up into multiple test classes. You think of each test class as its own fixture. If some tests need no users and one group in the db then that’s one fixture and one test class, if some other tests need to test what happens when there’s three users and no groups in the db then that’s another class:
class TestWithNoUsersAndOneGroup(object):
def setup(self):
# Add one group to the test db.
...
def test_something(self):
...
def test_something_else(self):
...
class TestWithThreeUsersAndNoGroup(object):
def setup(self):
# Add three users to the test db.
...
def test_another_thing(self):
...
You may end up with some code duplication between the different setup()
methods, but that can always be moved into helper methods that each of the
setup()
methods calls.
Clearly coming up with good names for the test classes can get pretty awkward if the different fixtures that they represent become complex and varied.
In practice, test classes hardly ever get written like this.
Most commonly you see test classes named things like TestFoo
containing all of
the tests for the Foo
class (or perhaps the foo()
method),
and this is how test classes are mostly used in h as well - to group together
the tests for a given unit of code that’s being tested.
Another alternative is to use test helper methods…
Test helper methods
The problems with setup()
methods can be worked around by simply not using
setup()
methods or self
, and instead using simple helper methods -
non-test methods in the test classes or modules that are called by each test
method as needed:
class MyTestClass(object):
def add_one_group_to_the_db(self):
# Code that adds one group to the db and returns the group.
def add_three_users_to_the_db(self):
# Code that adds three users to the db and returns the users/
def test_something(self):
group = add_one_group_to_the_db()
...
def test_something_else(self):
group = add_one_group_to_the_db()
...
def test_another_thing(self):
users = add_three_users_to_the_db()
...
This works just fine:
-
Each test calls only the helper methods that test needs. No helper methods are run unnecessarily for any tests that don’t need them.
-
It’s easy to look at a test and see exactly what setup is done for that test and what isn’t, no more implicit dependencies on things that are done or not done in a
setup()
method. -
The tests are independent from each other, modifying a test or adding a new test isn’t going to break existing tests. If a new test wants to change what a helper method does, but doing so would break existing tests, you can always just add a new helper method and call that instead.
But simple helper methods like this do have a couple of drawbacks:
-
Each test needs to call each helper method that it needs, which clutters up the test bodies with a lot of repetitive, boilerplate code (this gets worse when helper methods require arguments to be passed to them).
-
There isn’t a good way to do any tearing down of something that was set up by a call to a helper method (you wouldn’t want each test method to have to call corresponding teardown methods).
Pytest comes with a feature that it calls pytest fixtures that solve this problem very nicely. Pytest fixtures are like test helper methods on steroids…
pytest fixtures
pytest’s fixtures are its
answer to setup()
and teardown()
methods (pytest actually
supports setup()
and teardown()
as well
but fixtures are better). Pytest fixtures are a form of
dependency injection that
allow you to do something very similar to what you can do with test helper
methods but without the drawbacks.
An example of a simple fixture is h’s pyramid_request
fixture:
@pytest.fixture
def pyramid_request():
"""Dummy Pyramid request object."""
request = testing.DummyRequest()
request.auth_domain = text_type(request.domain)
request.create_form = mock.Mock()
request.matched_route = mock.Mock()
request.is_xhr = False
return request
Pyramid is the web framework that h uses to receive HTTP requests from users’ web browsers and send back HTTP responses to those requests. The request object is the object that Pyramid makes available to our code that represents the HTTP request that we’re currently responding to. Many methods in h require the request object as an argument, so tests often need a request object to pass to the method they’re testing.
The pyramid_request()
function above creates a DummyRequest
object, sets various fields on the request that are needed for h, and returns it.
The @pytest.fixture
decorator on the top of the function registers it with
pytest as a fixture function, which means that a test can make use of it by
simply taking it as an argument. For example, let’s look
at a test for the paginate()
function:
def test_current_page_defaults_to_1(pyramid_request):
"""If there's no 'page' request param it defaults to 1."""
pyramid_request.params = {}
page = paginate(pyramid_request, 600, 10)
assert page['cur'] == 1
paginate()
is a function that takes the request and returns some data, including the
current page, that’s used to render this pagination control at the bottom of
search results pages:
The test needs a Pyramid request argument in order to call the paginator
function, so it adds the pyramid_request
fixture to its arguments:
def test_current_page_defaults_to_1(self, pyramid_request):
...
When pytest comes to run this test it’ll see the pyramid_request
argument,
it knows that this matches up to an @pytest.fixture
function, so it first
calls the pyramid_request()
fixture function and then passes
the value that the fixture function returns to the test as the
pyramid_request
argument. What pytest does is something like this:
pyramid_request_fixture = pyramid_request()
test_current_page_defaults_to_1(pyramid_request=pyramid_request_fixture)
The pyramid_request()
fixture returns a Pyramid DummyRequest
object all set
up for use in h, and pytest passes that DummyRequest
to the test as the
pyramid_request
argument. It’s as simple as that - all the test has to do
is have an argument with the same name as the fixture function and it gets the
request object passed right into it.
At its core, that’s all a pytest fixture is - a perfectly normal Python
function that pytest runs before running a test, only if that test has the
fixture as an argument. An @pytest.fixture
decorator is needed on the top of
the function to register it with pytest as a fixture.
Fixtures are no different to test helper methods, except the boilerplate of each test needing to call every helper method that it needs is avoided. It’s not often needed, but a fixture can also contain teardown code to be called after any test that used that fixture simply by using yield instead of return.
Lots of different tests can reuse the same fixture
Just like different tests can call the same helper method, once you’ve written a fixture once you can use it in as many tests as you want just by adding it as an argument to each test:
@pytest.fixture
def pyramid_request():
...
def test_current_page_defaults_to_1(pyramid_request):
...
def test_numbers_large_result_set(pyramid_request):
...
def test_numbers_small_result_set(pyramid_request):
...
One test can use lots of different fixtures
And of course just like a test can call multiple helper methods, a test can also use multiple fixtures by just having all of the fixture names as arguments.
In the factories post we looked at the factories
object
which can be used to easily create realistic objects for use in tests.
factories
is a fixture
that’s used by any test that wants to use it to create test objects.
pyramid_settings
is another fixture that returns the settings used to configure the h app in the
test environment.
A test method can use all three fixtures, pyramid_request
, factories
and
pyramid_settings
just by taking all three as arguments by name:
def test_that_uses_multiple_fixtures(pyramid_request, factories, pyramid_settings):
...
Fixtures don’t have to return anything
Sometimes a test just needs a fixture function to be run before the test,
because the test depends on some setup that the fixture function does,
but the test doesn’t actually need to use the value that the fixture function
returns.
A fixture function doesn’t even have to return anything, it might just do some
setup that’s needed by some tests, for example creating some entries in the
database, and return nothing. If a test wants that fixture function to be run
before it then it adds the fixture as an argument, if it doesn’t then it
doesn’t. A good example of this is the routes
fixture, which is found in
many places in the h tests.
Routes are
Pyramid’s way of deciding which method it should call to handle a request for a given URL. For example if a user loads the /users/seanh page
then Pyramid calls the h.views.activity.UserSearchController.search()
method
to generate the response. The routes.py file
tells Pyramid how to do this by configuring which URLs map to which methods.
Routes can also be used in reverse, to generate URLs. For example if some
code wants the URL for the user search page for the “seanh” user then it can
get it from the route_url() method
by doing request.route_url("activity.user_search", username="seanh")
, which
will return "https://hypothes.is/users/seanh"
.
Many methods in h call methods like route_url()
and others that require all
the routes to be setup in order to work. If the routes aren’t configured then
calls to route_url()
will crash with some sort of “unknown route” error.
So the tests need some code to setup the routes before the test.
This is what a routes
fixture does, for example:
@pytest.fixture
def routes(self, pyramid_config):
pyramid_config.add_route('activate', '/activate/{id}/{code}')
The fixture doesn’t return anything, but it adds the 'activate'
route to the
Pyramid config so that any calls to request.route_url("activate", ...)
will
now work. A test can use this fixture and then test a function that uses this
route without having it crash:
def test_something(routes):
result = some_function_that_uses_the_activate_route()
assert result == <something>
@pytest.mark.usefixtures
The test above doesn’t actually use the routes
argument in the body of the
test, and the value of routes
is just None
anyway, it’s just there to tell
pytest to call the routes()
function before it calls test_something()
.
@pytest.mark.usefixtures()
is an alternative way for a test to use a fixture when it just needs the fixture
function to be run, but doesn’t actually need the fixture value in the test body:
@pytest.mark.usefixtures('routes')
def test_something():
result = some_function_that_uses_the_activate_route()
assert result == <something>
@pytest.mark.usefixtures("routes")
tells pytest to run the routes
fixture
before this test, just the same as routes
as an argument would do.
To use multiple fixtures this way, just pass multiple strings to usefixtures()
:
@pytest.mark.usefixtures("routes", "pyramid_request", "factories", "pyramid_settings")
Where to find fixtures
When you see test methods that have arguments in h, unless the test is using parametrize, those arguments are probably fixtures. But where do you find the fixture functions for the fixtures that a test uses? And where should you put any new fixtures that you want to write for your tests? Fixtures can be defined in a number of places:
-
pytest comes with some builtin fixtures.
-
In a
conftest.py
file such astests/h/conftest.py
(for the tests intests/h
) ortests/memex/conftest.py
for the tests intests/memex
. -
Fixture functions can go in the the test modules themselves. You’ll see this all over the place in the h tests, the fixture functions usually all go at the bottom of the test file and in alphabetical order. For example, here’s a bunch of fixtures defined at the bottom of
views_test.py
.Fixtures defined in a test file are only available to the tests in that file.
A fixture in a test file will override any fixture with the same name in a
conftest.py
file. -
Fixture functions can go in test classes. A fixture defined in a class is only available to the tests in that class, and overrides any fixture with the same name defined outside of the class or in a
conftest.py
file.We use this a lot in h as well, if a fixture is only needed by the tests in one test class then we define the fixture as a method of that test class. It keeps related code together, reducing noise and travel. It also allows other test classes to have their own fixture with the same name without conflicting.
Like any class method these fixtures need to take
self
as their first argument. We usually put all the fixtures at the bottom of the class and in alphabetical order. For example, here’s some fixtures defined at the bottom of the TestAddApiView class.
Tip: You can find the definition of a fixture by using the --fixtures
argument to pytest
, for example:
$ tox -e py27-memex tests/memex/ -- --fixtures
This prints out the names of all of the available fixtures and the line and file at which each one is defined. You can grep the output of this command to find a particular fixture by name:
$ tox -e py27-memex tests/memex/ -- --fixtures | grep '^annotation'
If you’re only interested in the fixtures available to a particular test module, class or method you can drill down:
$ tox -e py27-h \
tests/h/views/activity_test.py::TestSearchController::test_search_checks_for_redirects \
-- --fixtures
Conclusion
So far we’ve seen that fixtures are a more convenient alternative to test helper methods, with less boilerplate required and with a good way to do teardown code. You define each fixture in a separate function, but rather than each test needing to call each helper method that it wants the tests can just receive the fixture objects as arguments.
Pytest fixtures also have a couple of advanced features that take them well beyond what simple helper methods can do. We’ll cover some of those in Advanced pytest Fixtures.