Writing Tests for a Foolproof Django Project

Agus Richard
Level Up Coding
Published in
8 min readMay 17, 2022

--

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.

Author’s Image

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!

--

--

Software Engineer | Data Science Enthusiast | Photographer | Fiction Writer | Freediver LinkedIn: https://www.linkedin.com/in/agus-richard/