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 Django, you don’t need to pick a framework. Everything you need to write unit tests is already built-in.
Apps contain a file called
tests.py by default. You can put your tests here or you can create a sub-directory called
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
- Add a file called
__init__.pyinto 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.pyfrom 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.
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.
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.user . This sets the variable as an attribute of the class, which allows you to access the variable from your tests.
setUpTestData is an alternative to
setUp . The difference is
setUp is run before every test of the class, and
setUpTestData is run once.
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.
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.
If you want to check an object has a certain attribute, you can use
Then there are some assertions that are more specific to web development, which have been added by Django.
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.
Use this to test the success URLs of your class based views.
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-autoslugrather 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.
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.
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 using
Post.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.
Django provides a class called
Client which makes it possible to test views by simulating requests.
You can also use client to perform
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.
""" Tests that a non-logged in user is redirected """
response = self.client.get(self.url)
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.
""" Tests that a GET request works and renders the correct
response = self.client.get(self.url)
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.
How to get started with the Debugger in VS Code
I have a confession to make. Until yesterday, I was using print statements to debug my Python code.
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
POST requests to
To set up your debugger for testing, add a configuration to your
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.