I know exactly what you’re thinking: here comes another complicated concept that I need to get my head around! But think again! In this chapter, you’ll see that fixtures aren’t complicated to set up and are very useful when creating your tests.
So, what are fixtures then?
The purpose of a fixture is to provide a controlled development environment so that you can configure a set of tests using the same context or the same dataset. This means that the environment created by the fixture will be consistent and identical for each test that calls it.
A simpler way of describing it would be to say that the fixture contains everything a test needs to do its work and all of this is encapsulated into a single function.
You might already be aware of some examples where fixtures are used:
Preparing objects
Starting or stopping services
Initializing a database with a dataset
Creating a test client for a web project
Configuring mocks
But don’t worry if it’s still not very clear. Let’s look at a specific example.
Let’s say that you created a web project using the Django framework and you set up lots of features that required creating an account and being logged in as the user of the account. Up to now, you’ve had to add lines of code to create the user and log them in for each scenario.
With fixtures, all this is a thing of the past!
The fixture will contain all the code you need to create the user and log them into the application. Then, any test that needs a logged-in user can call it. This will help to simplify the configuration of your test environment and save time when creating tests.
Now you’ve seen what fixtures can do, let’s create some.
Create a Fixture
It’s pretty easy to create a fixture. If you know how to create a function, you know how to create a fixture. You just need one additional step to add a decorator above the function. Thanks, Pytest, for making it so easy for us! 😉
Have a look at this example of a fixture that builds a dictionary using some data and then returns the dictionary:
import pytest
@pytest.fixture
def first_fixture():
data = {"first_name" : "Ranga",
"surname" : "Gonnage"}
return data
Steps required:
Import
pytest
.Add the
@pytest.fixture
decorator.Define the function using the fixture name and the relevant data.
Return all of the data.
Now that you know how to create a fixture, we’re going to see how we call them.
Call a Fixture
Let’s look at the fixture that we created earlier and see how to declare our test using the first_fixture
fixture: test_with_first_fixture(first_fixture)
.
After declaring the name of the test, I declared the first_fixture
fixture as an argument so that the configured data can be accessed. Now you have access to all of the data items returned by the fixture.
Once you run the test, Pytest will actually examine the arguments and check to see if a fixture exists with the same name as the parameters passed as arguments. In our case, it will look for a fixture called first_fixture
and, if it finds one, it will capture the value returned by the fixture and provide the object as an argument for the test function.
Okay, now for a little example to illustrate all of this theory:
import pytest
@pytest.fixture
def first_fixture():
data = {"first_name" : "Ranga",
"surname" : "Gonnage"}
return data
def test_with_first_fixture(first_fixture):
print(first_fixture)
assert first_fixture["first_name"] == "Ranga"
assert first_fixture["surname"] == "Gonnage"
You might have noticed in the above image that when the test is run, the first_fixture
argument contains the dictionary returned by the fixture. So, the first_fixture
argument equals {'first_name': 'Ranga', 'surname': 'Gonnage'}
.
Here’s a screencast showing you these steps in detail. You’ll see how to create your fixture and how to use it in your testing:
Reuse a Fixture
You haven’t seen why it’s useful to create fixtures yet. One of the most powerful aspects when using fixtures is the ability to create a generic configuration and reuse it for several tests. This means that you can call the same fixture for two different, independent tests.
So, if we take the previous example, we can create a second test that will call the first_fixture
fixture:
import pytest
@pytest.fixture
def first_fixture():
data = {"first_name" : "Ranga",
"surname" : "Gonnage"}
return data
def test_fixture(first_fixture):
assert first_fixture["first_name"] == "Ranga"
assert first_fixture["surname"] == "Gonnage"
def test_fixture(first_fixture):
assert first_fixture["first_name"] == "Ranga"
assert first_fixture["surname"] == "Gonnage"
For a project that uses the Flask or Django framework, you need to instantiate a client to create a test web browser. You can make use of fixtures to make the client configuration easier. So, you’ll call the fixture for each of your tests.
For example, within the Flask framework:
import pytest
from myapp import create_app
@pytest.fixture
def client():
app = create_app({"TESTING": True})
with app.test_client() as client:
yield client
And for the Django framework:
import pytest
from django.test import Client
@pytest.fixture
def client():
c = Client()
return c
Reuse a Fixture in a Fixture
One final point to finish off this part about fixtures. In some test scenarios, you might need additional data or a different configuration for a test to be successful.
But this data might only be useful for one test. It’s not advisable to add this data item to the main fixture, but instead you can add another fixture, which will call the main fixture with the extra data item.
As you can tell, Pytest is a truly flexible test framework. This is how it enables you to call a fixture within a fixture.
import pytest
@pytest.fixture
def first_fixture():
data = {"first_name" : "Ranga",
"surname" : "Gonnage"}
return data
@pytest.fixture
def second_fixture(first_fixture):
first_fixture["email"] = "test@test.com"
return first_fixture
def test_fixture(second_fixture):
assert second_fixture["first_name"] == "Ranga"
assert second_fixture["surname"] == "Gonnage"
assert second_fixture["email"] == "test@test.com"
The second fixture second_fixture
supplements the first fixture first_fixture
with a new element in the dictionary. So, in this second fixture, we are adding a data item with a key of email
and a value of test@test.com
.
The test_fixture
test shows that we also have access to the data item with email
as its key. Go ahead and try out this section of code and you’ll see that the test works.
Over to You!
Right, we’ve almost finished with fixtures. Go back to your Django project (OC-Commerce) that we’ll be working on in the next part of the course.
Your Mission:
Add fixtures to simplify your testing.
You could for example configure a logged-in user, some mocks, or you could even initialize the database.
Find a suggested solution on GitHub!
Let’s Recap!
Creating fixtures means that you can have a controlled environment to execute your testing so that results are always reproducible.
The
@pytest.fixture
decorator converts a simple function into a fixture.To use a fixture in your testing, it must be defined as a test argument and it can be used as many times as you need.
A fixture can call another to provide additional configuration.
Now that you understand the concept of a fixture, you know how to configure a unit test without having to duplicate code. However, this isn’t the only solution. We’re going to see how to organize testing into classes to simplify test creation.