DEV Community

Cover image for Django testing - best practices and Examples
Ousseynou Diop
Ousseynou Diop

Posted on • Updated on

Django testing - best practices and Examples

Originally posted on my blog

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
Enter fullscreen mode Exit fullscreen mode
  • Run migrations
python manage.py migrate
Operations to perform:
 Apply all migrations: admin, auth, contact, contenttypes, sessions
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Create urls.py inside pages

touch pages/urls.py
Enter fullscreen mode Exit fullscreen mode
# 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'),
]
Enter fullscreen mode Exit fullscreen mode

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
]

Enter fullscreen mode Exit fullscreen mode

Create your templates

mkdir templates/pages
touch templates/pages/home.html
touch templates/pages/about.html
Enter fullscreen mode Exit fullscreen mode

Let's add some html content

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

<!-- templates/pages/about.html -->
<h1>About page</h1>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.')
Enter fullscreen mode Exit fullscreen mode

Now run the tests

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

OK
Enter fullscreen mode Exit fullscreen mode

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.')
Enter fullscreen mode Exit fullscreen mode

Then run

python manage.py test

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

OK
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Then run

python manage.py test

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

OK
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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'...

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
ibrahima92 profile image
Ibrahima Ndaw

Nice article!

Collapse
 
xarala221 profile image
Ousseynou Diop

Welcome Ibrahima

Collapse
 
sm0ke profile image
Sm0ke

Nice, Thanks for writing!

Collapse
 
xarala221 profile image
Ousseynou Diop

Welcome Sm0ke

Collapse
 
corentinbettiol profile image
Corentin Bettiol

For django apps I usually choose to write unit tests with pytest, pytest-django & pytest-cov :)

Example output.

Collapse
 
xarala221 profile image
Ousseynou Diop

Thanks !
They are great tools.
This is some advance testing tools i will cover them next...