Learn How testing works in Django - best practices and Examples

django Apr 23, 2020

Introduction

Django is a high level framework to build scalable web applications.
Django is used by companies like Google, Facebook, Pinterest, Nasa etc...

Code quality is very important in software engineering.
Testing is one of the best practices of a good developer, the reality is that everyone talk about it but in real word not everyone write tests 😁

We've many types of tests but in this tutorial i will cover two of them :

  • Unit Tests Is a level of software testing where a section(unit) of an application meets the requirements, it focus on one specific function.
  • Integration Tests Is a level of software testing where individual sections are combined and tested as a group, it combined multiple pieces of code and functionality.

When and Why run tests

  • Testing helps us structure good code, maintain large codebase and find bugs.
  • Well written tests make your code - easy to debug, integrate and deploy.
  • Well written tests save us lot of times.
  • Whatever you're a solo developer or you work in a team you should write tests.

Best practices

  • If it can break, it should be tested. This includes models, views, forms, templates, validators, and so forth.
    Each test should generally only test one function.
  • Keep it simple. You do not want to have to write tests on top of other tests.
  • Run tests whenever code is PULLed or PUSHed from the repo and in the staging environment before PUSHing to production.
  • When upgrading to a newer version of Django:
  • upgrade locally,
  • run your test suite,
  • fix bugs,
  • PUSH to the repo and staging, and then
  • test again in staging before shipping the code.

Learn more here

Setup

You can find the final source code here

  • Create a virtual environment then activate it Learn more
pipenv shell
(phonebook_rest_api-9zIZds3o) ╭─username@username-Latitude-7480
  • Run migrations
python manage.py migrate
Operations to perform:
 Apply all migrations: admin, auth, contact, contenttypes, sessions
  • Run the application
python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 21, 2020 - 18:37:31
Django version 3.0.2, using settings 'phonebook.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Open your browser at http://localhost:8000/

We assure that everything is working properly.

Structure

By default all new apps in a Django project come with a tests.py file. Any test within this file that starts with _test__ will be run by Django's test runner. Make sure all test files start with _test__.

As applications grow in complexity, it's recommended to delete this initial tests.py file and replace it with an app-level tests folder that contains individual tests files for each area of functionality.

In real world application you should have something like this :

β”œβ”€β”€ contact
β”‚   β”œβ”€β”€ admin.py
β”‚   β”œβ”€β”€ api.py
β”‚   β”œβ”€β”€ apps.py
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ migrations
β”‚   β”‚   β”œβ”€β”€ 0001_initial.py
β”‚   β”‚   └── __init__.py
β”‚   β”œβ”€β”€ models.py
β”‚   β”œβ”€β”€ serializers.py
β”‚   β”œβ”€β”€ tests # new
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ test_forms.py # new
β”‚   β”‚   β”œβ”€β”€ test_models.py # new
β”‚   β”‚   └── test_views.py # new
β”‚   β”œβ”€β”€ urls.py
β”‚   └── views.py

In this article i will keep it simple.

Before writing any tests we will create a new django app and called it pages it will contain our static pages(home and about)

python manage.py startapp pages

Register the app here

# Application definition

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'rest_framework',  # add this
   'contact'  # add this
   'pages' # new
]

Update pages views.py

# pages views.py
from django.shortcuts import render
from django.views.generic import TemplateView


class HomePageView(TemplateView):
   template_name = 'pages/home.html'


class AboutPageView(TemplateView):
   template_name = 'pages/about.html'

Create urls.py inside pages app

touch pages/urls.py
# pages/urls.py
from django.urls import path

from .views import HomePageView, AboutPageView

urlpatterns = [
   path('', HomePageView.as_view(), name='home'),
   path('about/', AboutPageView.as_view(), name='about'),
]

And finally update your main urls.py

from django.contrib import admin
from django.urls import path, include  # add this

urlpatterns = [
   path('admin/', admin.site.urls),
   path("", include("contact.urls")),  # add this
   path("", include("accounts.urls")),  # add this
   path("", include("pages.urls"))  # new
]

Create your templates

mkdir templates/pages
touch templates/pages/home.html
touch templates/pages/about.html

Let's add some html content

<!-- templates/pages/home.html -->
<h1>Homepage</h1>

<!-- templates/pages/about.html -->
<h1>About page</h1>

Run again your server to assure that everything is working properly.

python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 22, 2020 - 06:46:31
Django version 3.0.2, using settings 'phonebook.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[22/Apr/2020 06:46:33] "GET / HTTP/1.1" 200 2268

Open your browser then navigate to the homepage urls

Simple TestCase

Now we will test our static pages without involving a database.
Let's test the homepage

# pages/tests.py

from django.http import HttpRequest
from django.test import SimpleTestCase
from django.urls import reverse

from . import views


class HomePageTests(SimpleTestCase):

   def test_home_page_status_code(self):
       response = self.client.get('/')
       self.assertEquals(response.status_code, 200)

   def test_view_url_by_name(self):
       response = self.client.get(reverse('home'))
       self.assertEquals(response.status_code, 200)

   def test_view_uses_correct_template(self):
       response = self.client.get(reverse('home'))
       self.assertEquals(response.status_code, 200)
       self.assertTemplateUsed(response, 'pages/home.html')

   def test_home_page_contains_correct_html(self):
       response = self.client.get('/')
       self.assertContains(response, '<h1>Homepage</h1>')

   def test_home_page_does_not_contain_incorrect_html(self):
       response = self.client.get('/')
       self.assertNotContains(
           response, 'Hi there! No data found.')

Now run the tests

python manage.py test
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.015s

OK

It's working properly

Let's do the same thing for the about page

# pages/tests.py
class AboutPageTests(SimpleTestCase):

   def test_about_page_status_code(self):
       response = self.client.get('/about/')
       self.assertEquals(response.status_code, 200)

   def test_view_url_by_name(self):
       response = self.client.get(reverse('about'))
       self.assertEquals(response.status_code, 200)

   def test_view_uses_correct_template(self):
       response = self.client.get(reverse('about'))
       self.assertEquals(response.status_code, 200)
       self.assertTemplateUsed(response, 'pages/about.html')

   def test_about_page_contains_correct_html(self):
       response = self.client.get('/about/')
       self.assertContains(response, '<h1>About page</h1>')

   def test_about_page_does_not_contain_incorrect_html(self):
       response = self.client.get('/')
       self.assertNotContains(
           response, 'Hi there! I should not be on the page.')

Then run

python manage.py test

System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.023s

OK

This works fine

TestCase

Testcase is the most common class to use for writing test in Django it gives us the ability to mock queries to the database.

Let's test out our Contact database model

# contact/tests.py
from django.test import TestCase
from django.urls import reverse

from .models import Contact


class ContactTests(TestCase):

   def setUp(self):
       Contact.objects.create(
           first_name='Ousseynou',
           last_name="Diop",
           phone="779929900",
           email="hello@me.com"
       )

   def test_email_content(self):
       contact = Contact.objects.get(id=1)
       expected_object_name = f'{contact.email}'
       self.assertEquals(expected_object_name, 'hello@me.com')

   def test_contact_list_view(self):
       response = self.client.get(reverse('contacts'))
       self.assertEqual(response.status_code, 200)
       self.assertContains(response, 'hello@me.com')
       self.assertTemplateUsed(response, 'contact/contact_list.html')

Then run

python manage.py test

System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.023s

OK

This works fine! let's break the code.

# contact/tests.py
from django.test import TestCase
from django.urls import reverse

from .models import Contact


class ContactTests(TestCase):

   def setUp(self):
       Contact.objects.create(
           first_name='Ousseynou',
           last_name="Diop",
           phone="779929900",
           email="hello@me.com"
       )

   def test_email_content(self):
       contact = Contact.objects.get(id=1)
       expected_object_name = f'{contact.email}'
       self.assertEquals(expected_object_name, 'hello@gmail.com') # new

   def test_contact_list_view(self):
       response = self.client.get(reverse('contacts'))
       self.assertEqual(response.status_code, 200)
       self.assertContains(response, 'hello@me.com')
       self.assertTemplateUsed(response, 'contact/contact_list.html')

Then run the test again

python manage.py test
System check identified no issues (0 silenced).
.F..........
======================================================================
FAIL: test_email_content (contact.tests.ContactTests)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "/home/username/projects/phonebook_rest_api/contact/tests.py", line 20, in test_email_content
   self.assertEquals(expected_object_name, 'hello@gmail.com')
AssertionError: 'hello@me.com' != 'hello@gmail.com'
- hello@me.com
?        ^
+ hello@gmail.com
?       + ^^^


----------------------------------------------------------------------
Ran 12 tests in 0.030s

FAILED (failures=1)
Destroying test database for alias 'default'...

Our tests are failed.

Now you understand how tests are important in software engineering.

Conclusion

In this article we've learned how to write tests for our django project.
The next steps are to write TDD, setup CI/CD in order to maintain code quality.

Thank you for reading 😍 See you in the next article.

We started sharing these tutorials and articles to help and inspire developers and engineers around the world. If our blog has been helpful to you, feel free to buy us a coffee to keep us going :).

Buy us a coffeeBuy us a coffee

Ousseynou Diop

Full stack Developer & Python Lover

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.