Django Testing for Beginners
Update! I’ve written an updated version of this article. It’s free to read exclusively on my website. I’ve also written a separate post focused on testing Django models.
There are two things I regret not learning properly before starting my first development job: git and testing.
By learning how to write tests, I found my workflow became more efficient as I was spending less time doing manual testing in the browser.
If I was moving chunks of code around or adding new features, running my test suite would alert me right away if my changes had broken anything.
When I was writing tests, it would force me to think of edge-cases and ways that users could break the app. This resulted in fewer bugs in the first place.
There is one major downside. Learning how write tests is tough.
Not only do you have to learn your framework, you also have to figure out what needs testing and how. There aren’t really any hard and fast rules about what has to be tested, and compared with regular Django tutorials, online resources are scarce.
This guide is aimed at self-taught developers who know how to build basic applications in Django, but don’t have testing experience from another framework.
Let’s get back to basics.
Testing In Django
In the world of JavaScript, there are lots of choices for testing frameworks. Jest and Mocha are two of the most popular.
In Django, you don’t need to pick a framework. Everything you need to write unit tests is already built-in.
Getting Started
Folder Structure
Apps contain a file called tests.py
by default. You can put your tests here or you can create a sub-directory called tests
.
When your project grows and you want to better organise your tests, you can put them into a separate directory. If you go down this route, make sure you do these four things:
- Name the directory
tests
- Add a file called
__init__.py
into the tests folder and all of its sub-folders. The file can be empty, it just needs to exist. - Begin each file name with
test_
. This is so Django knows where to look for tests. - Remove the original
tests.py
from the app’s directory.
Your First Test
Testing that two plus two equals four might seem pointless but it’s a quick way to check that our test suite is structured correctly.
Go to tests.py
and copy/paste in the following code.
It’s important that all tests are prefixed with test_
. Django will ignore methods which don’t follow this convention.
Running Tests
Tests are run from the command line using this command:
$ python manage.py test
This will find and run every test in the project. You may want to save time by running tests for just one app. To do this, use:
$ python manage.py test <app-name>
The TestCase class
Test classes usually inherit from the TestCase class. For large projects, you might want to customise the base class, though this will most likely inherit from TestCase as well.
Lots of Django’s testing tools are built into the TestCase class. There are methods like setUp
, which will automatically be run before each test and assertions like assertEqual
which will determine if a test passes or fails.
Setting Up Tests
When you run python manage.py test
, you will start with an empty database. This is crucial for unit testing, because it prevents your actual database from introducing unwanted side-effects.
This might feel wrong at first. After all, wouldn’t a working database create real-world conditions for testing? Yes, but you want your tests to be repeatable and have as few moving parts as possible. Unit tests are about testing one thing at a time and creating laboratory-like conditions so you can confidently understand what’s going on in the code.
Unfortunately, it means a lot of effort needs to be put in to setting up good tests. This often means creating objects in the database like users, products and orders.
setUp
is a method used to prepare the data you need to run tests. It is run before each test, so the code to create objects only needs to be written once.
If you’re not already familiar with object-orientated programming, it’s important to know that variables need to be prefixed with self.
e.g. self.user
. This sets the variable as an attribute of the class, which allows you to access the variable from your tests.
setUpTestData
setUpTestData
is an alternative to setUp
. The difference is setUp
is run before every test of the class, and setUpTestData
is run once.
Assertions
Assertions are methods that will return True or False, and are used to determine if a test has passed or failed. You can put multiple assertions into one test.
Some assertions come straight from Python’s unittest library. Here are some examples from a Django project.
assertEqual
self.assertEqual(response.status_code, 404)
assertIn
This is used to check for keys in dictionaries. When testing function-based views, you might want to check whether the context contains a certain key.
self.assertIn('posts', response.context)
assertTrue
If you want to check an object has a certain attribute, you can use assertTrue
.
self.assertTrue(hasattr(post, 'published_date'))
Then there are some assertions that are more specific to web development, which have been added by Django.
assertTemplateUsed
self.assertTemplateUsed(response, 'blog/add_post.html')
Using this assertion can feel like overkill, but one time I made a mistake in urls.py
which led to a URL loading the wrong view, which loaded a different template.
assertRedirects
Use this to test the success URLs of your class based views.
self.assertRedirects(response, "/user123/my-title/draft")
Testing Models
Update! I’ve written a separate post on this, which you can read here.
One of the biggest challenges of testing is figuring out what to test.
I like to split my tests into models, views and forms. Tests for models are some of the easiest to set up, making it a good starting point for absolute beginners.
Here is an example from a blogging project. The model being tested is the Post
model. Here is what I’ve tested:
- Am I able to create a post in the database with a title, body and author?
- Does the string representation (i.e. how the object is represented in Django admin) behave as expected?
- Is a slug automatically created? Slugs for this project were generated by a external package called
django-autoslug
rather than by the user, so this needs to be tested. - If I have two identical post names from the same user, will unique slugs be created? (This is important as I was using slugs to construct URLs).
A link to the model under test can be found here.
Choosing what to test
Coverage reports will indicate how much of the code-base has been tested.
In models.py
, I defined the __str__
method and testing this counted towards the coverage score.
If my only goal of testing was to get a good coverage score, I could have gotten away with not testing the slugs.
In this case, I have treated these tests as a way of documenting the requirements. It was important that unique slugs were generated, because I used them to construct URLs. Therefore, I included tests for these as well.
Testing Views
You might be wondering why I used Post.objects.create
when posts will be created by a user submitting a form.
Tests are a debugging tool. If I were to just write integration tests that simulated a form input, it would be harder to locate bugs because there are more moving parts.
By writing tests usingPost.objects.create
, I can be sure that failing tests can be traced back to the model.
I find it helps to test the model before moving onto testing views.
The Client
Django provides a class called Client
which makes it possible to test views by simulating requests.
You can also use client to perform GET
and POST
requests on your URLs. The Client
will return a response object, which will become the basis for your assertions.
More moving parts
Technically speaking, tests which use the Client
are not unit tests, as there are multiple things under test. Simulating a request via the Client
will involve looking up the URL in urls.py
before running the view in views.py
and making calls to the database.
The downside here is there are more moving parts, so finding the bug behind a failing test is more difficult. This is why it’s important to test your models separately and test any utility functions that can be unit tested. This will help you eliminate some of those moving parts.
Testing that a user is logged in
When you use the Client to perform a GET
request, it will assume no user is logged in.
def test_user_must_be_logged_in(self):
""" Tests that a non-logged in user is redirected """
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
If a user is not logged in and attempts to view a protected page, then I would expect them to be redirected. Therefore, I simulated a GET
request on the URL which I set up in setUpTestData
(see the gist further down), and checked the status code of the response. A code of 302 indicates a redirection.
I also wanted to test that a logged in user can successfully access the page to add a post. To avoid a 302 response, I used the client to force login.
def test_get_add(self):
""" Tests that a GET request works and renders the correct
template"""
self.client.force_login(self.user)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/add.html')
Testing the AddPost view
Below are the tests for the view to add a post. This view will accept GET
requests, which will display the form, and POST
requests which will accept form data.
When your views accept multiple types of request, it’s important that each type is tested.
A link to the view under test can be found here.
Test who can access your views
It’s important to test who can access each view. This might not just be a case of testing whether a user is logged in or not. You might have views that display content that belongs to a particular user, so you will not want other logged in users to be able to access that content.
This is also cumbersome to test manually in the browser, as you will have to log out and log back in again as other users- all the more reason to write tests.
Here is an example of a test class for a view that should only be accessed by the content owner. You wouldn’t want other users to be able to edit, publish or delete their posts.
Using the Debugger
The debugger of VScode can be run on your test suite as well as your development server.
If you’re not familiar with the debugger, I recommend you read my post on setting it up.
Sometimes, failure and error messages aren’t clear. If you are new to Python and Django, you might find that syntax errors hold you back. The debugger can help you locate these errors. It is also useful for exploring the response
object, which is returned from GET
and POST
requests to Client()
.
To set up your debugger for testing, add a configuration to your launch.json
:
Deciding What To Test
Testing takes time and when you’re a developer working towards a tight deadline, it is easy for testing to fall by the wayside.
My tests for views for the blog app took 633 lines and several hours. If getting a high coverage score was my only goal, I could have saved a lot of time. However, there will have been many tests that provided value but did not contribute towards the score.
This is why it is important to only use coverage reports as a rough guide. Your own intuition, project requirements and known edge-cases should come first.
The key here is to prioritise. Look for things that could go unnoticed when testing in the browser. Think of ways that a user could break your app.
If you work in a team, consider writing tests that will alert your colleagues if their code accidentally breaks one of your features.
Share your experiences
What are your tips for writing tests? What online resources have you found useful? Share your experiences in the responses.
Thank you for reading.