Writing Tests for a Foolproof Django Project
How to achieve 100% test coverage by writing multi-layered tests in Django
Let’s start by answering a question, when you’re writing code, how much do you care about writing tests? Well, I assume this really depends on what kind of project that you’re currently working on and how much work you need to do to complete the project.
In my opinion, if we have such a tight deadline and a lot of things to do, we’ll forget about writing tests and resulting in a test-less code. But if you don’t write tests then the tests won’t fail, right? 😁
Without tests, then we need to manually check our code every single time we change something or refactor our code. That’s certainly not a great thing and I’d say that’s a very bad project’s design.
Just like Jacob Kaplan-Moss (one of the core developers of the Django web framework) said…
“Code without tests is broken by design.”
By knowing this, this article will walk you through writing a fully tested Todo List app, with the hope to make it foolproof.
Prerequisites
Before moving on, while this article will walk you through building a fully tested project, I assume you already have some basic knowledge about Django because I’ll tend to overlook the actual code in favor of writing test code. Yet, I’ll still give the actual code and give some brief explanations.
A little bit of disclaimer, since there will be a lot of test cases, so I’ll just provide a snippet for each layer but you can get the full code here (actual and test code):
Start with setting up our Django project.
Summon up a Python virtual environment by running:
python -m venv venv
Activate virtual environment:
# Mac OS / Linux
source /bin/activate# Windows
venv\Scripts\activate
Install dependencies:
pip install django coverage selenium
Since this application is fairly simple, we’ll only need three main dependencies. We need coverage
to know our test coverage and selenium
for running functional tests.
Create Django project and Todo application
# Create Django project
django-admin startproject core .# Create Todo Application
python manage.py startapp todo
Now, we have our project set up. In the next section, we’ll go step by step through multiple layers of unit code, starting with models
.
Models
Actual Code
Go to the todo
folder and open models.py
. Put this snippet there:
It’s just a basic Todo model consisting of title, description, completed, created_at, and user. Notice that I am using default User model. You certainly can use your own custom User model.
If you want to run the server, you need to make the migrations script and migrate it first.
# Make migration script
python manage.py makemigrations# Migrate
python manage.py migrate# Run server
python manage.py runserver
Currently, we don’t have the views
yet, so there is nothing much we can do to interact with our app. We’ll create the views
later on.
Test Code
We’ll write our very first test in this subsection. Since for this project we’ll write multi-layered tests, then it’d be better for us to have a dedicated folder to store all these tests. First, we need to create a new folder called tests
in todo
folder. Note that there are other ways of structuring test code but in this case, I’ll use an approach that each Django application will have its own tests folder.
Create a file named test_models.py
and put this initial code there:
There we have it, our first test function. In the above, we created a Test Class which is inheriting unittest.TestCase
. This class will serve as a suite/block for all test cases of models.
ThesetUp
method will take care of user initialization so we don’t need to create a new user in each test function. Note thatt setUp
method will run before each test function and if you need to do some clean-up after each test function, you can use tearDown
method.
As you might have guessed, we’ll have positive and negative test cases because if we want to build a foolproof project, I’m sure that having to cover a whole range of user inputs and behaviors will certainly make our project robust. That’s exactly what we’ll do here.
Now, let’s cover other test cases:
Here, we have 13 tests only for Todo models.
Two things that in my opinion important while writing tests, the first one is making sure that you avoid having false-positive tests. One thing you can do is to change the assertion condition, from assert equal to assert not equal. If in one condition the test passes then in the other condition, the test certainly won’t pass. This is an important sanity check we often forget.
The other important thing is when you want to make your project foolproof, you need to consider a lot of possible permutations of inputs. If you have a lot of inputs, then the number of tests to be included will bubble up. So, it goes back to the trade-off between time to write tests and completing our tasks in time.
One bit of reminder, every test is always composed of three steps, they are Arrange-Act-Assert. Taking an example from the above code, first we create a user that will create a todo, then create the todo, then assert whether that todo is indeed what we’re expecting.
Forms
Actual Code
Now, we’ll work on a different layer, forms
layer.
Simple and pretty much self-explanatory right?
Test Code
Comparing to test code for models
, here, we’re focusing on UserForm
and TodoForm
The above code is only a snippet of the entire test code for forms
. You can directly check the rest in the code resource for this article.
Pretty similar to the test code for models
. We have setUp
method to initialize user data (we need this to create a valid todo). Then assign the newly created user to self.user
so we can access this user in each test function.
We can check if user’s inputs are actually valid using form.is_valid()
and we can assert form’s error message by accessing form.errors[“field_name”]
.
Views
Actual Code
Here is the content of view.py
file
Here we have eight view functions, three of them are related to authentication and the rest are related to Todo CRUD operations. To run this server properly we need to create all of the templates. But I choose to overlook this matter and you can get the HTML templates here.
Test Code
We can test views
by using test client. First of all, we need to import Client
from django.test
and initialize that inside setUp
method. After that, we can use it in every test function. Here is the test code for views
Just like before, here is only the snippet and you can get the rest in the coding material.
Let’s take the example of registering a user. First of all, inside setUp
method, we initialize the test client and now take a look at the test function test_register_user_GET
. Here, we call the register URL and assert its response, such as its status code and template used to render the register page.
Take another example inside test function test_negative_home_user_not_logged_in
. Here, we also check for the value inside context
dictionary. Basically, this is a dictionary that is injected to Django template and we can check for the values inside this context
dictionary. In the above code, I checked whether a user is already logged in or not (if is_authenticated
is true, then the user is logged in).
Urls
Actual Code
Here is where we’re registering views
to urls
.
Test Code
Testing urls
is considered not that important compared to testing other layers. Because when we’re testing views
, we’re actually testing the urls
at the same time. I choose to provide these tests for completeness.
Functional Tests
Now, we arrived at the final test code. Before going any further, you need to download Selenium based on your Chrome version. Then you can put the chromedriver
inside a folder named driver
in the project’s root folder.
Then add new additional variables to settings.py
# For Windows
WEB_DRIVER_PATH = os.path.join(BASE_DIR, "driver", "chromedriver.exe")# Running functional tests without opening up a chrome window
WEB_DRIVER_HEADLESS = True
Here is the snippet of the test code.
Note that for the functional tests, we’re not actually inheritingunittest.TestCase
, instead, the test class is inheriting StaticLiveServerTestCase
.
Here, we set up Selenium web driver inside setUp
method and quit the web driver inside tearDown
method. For the rest of the test code, we follow Arrange-Act-Assert principle. Find the element we want to interact with, act on it (like clicking or typing) and assert the behavior and result.
Coverage
We’ve written all tests for all layers, but how about the test coverage that I promise you will be 100 percent. We’ll talk about that now!
First, create .coveragerc
to store test coverage settings. We’ll omit manage.py
and all files inside venv
:
[run]
omit =
*/manage.py
./venv/*
We can know the test coverage by running:
coverage run ./manage.py test
By running the above command, we created a file named .coverage
. We’ll get the report from this file.
Get the test coverage report:
coverage report
Get a nice looking HTML report by running:
coverage html
The above command will create htmlcov
folder. Then, you can open up the index.html
to see the test coverage.
Voila, we achieve 100% test coverage… Congratulations!
Conclusion
In this article, we learned about how to write tests for every layer in a typical Django project. We cover how to write models, forms, views, urls, and functional tests.
I urge you to look at the final code in the coding material because I tend to overlook the actual code and only give snippets of the entire test code. Because I don’t want to bore you by having to lay out all test cases.
In conclusion, I believe that writing test code is as important as the actual code because tests give us predictability, well at least give us high predictability. We don’t want our code to behave unpredictably and find bugs lurking here and there unnoticed. In addition, that’s always a fun experience to me watching all these green “OK” or “PASSED” words, right?
Coding material for this article:
Thank you for reading and happy coding!