Testing in Django

Introduction to Testing

Like all mature programming languages, Django provides inbuilt unit testing capabilities. Unit testing is a software testing process where individual units of a software application are tested to ensure they do what they are expected to do.

Unit testing can be performed at multiple levels – from testing an individual method to see if it returns the right value and how it handles invalid data, up to testing a whole suite of methods to ensure a sequence of user inputs leads to the desired results.

Unit testing is based on four fundamental concepts:

  1. A test fixture is the setup needed to perform tests. This could include databases, sample datasets and server setup. A test fixture may also include any clean-up actions required after tests have been performed.
  2. A test case is the basic unit of testing. A test case checks whether a given set of inputs leads to an expected set of results.
  3. A test suite is a number of test cases, or other test suites, that are executed as a group.
  4. A test runner is the software program that controls the execution of tests and feeds the results of tests back to the user.

Software testing is a deep and detailed subject and this chapter should be considered to be only a bare introduction to unit testing. There are a large number of resources on the Internet on software testing theory and methods and I encourage you to do your own research on this important topic. For a more detailed discussion on Django’s approach to unit testing, see the Django Project website.

Introducing Automated Testing

What Are Automated Tests?

You have been testing code right throughout this book; maybe without even realizing it. Each time you use the Django shell to see if a function works, or to see what output you get for a given input,
you are testing your code. For example, back in Chapter 2 we passed a string to a view that expected an integer to generate a TypeError exception.

Testing is a normal part of application development, however what’s different in automated tests is that the testing work is done for you by the system. You create a set of tests once, and then as you make changes to your app, you can check that your code still works as you originally intended, without having to perform time consuming manual testing.

So Why Create Tests?

If creating simple applications like those in this book is the last bit of Django programming you do, then true, you don’t need to know how to create automated tests. But, if you wish to become a professional programmer and/or work on more complex projects, you need to know how to create automated tests.

Creating automated tests will:

  • Save you time. Manually testing the myriad complex interactions between components of a big application is time consuming and error prone. Automated tests save time and let you focus on programming.
  • Prevent problems. Tests highlight the internal workings of your code, so you can see where things have gone wrong.
  • Look professional. The pros write tests. Jacob Kaplan-Moss, one of Django’s original developers, says “Code without tests is broken by design.”
  • Improve teamwork. Tests guarantee that colleagues don’t inadvertently break your code (and that you don’t break theirs without knowing).

Basic Testing Strategies

There are many ways to approach writing tests. Some programmers follow a discipline called “test-driven development”; they actually write their tests before they write their code. This might seem counter-intuitive, but in fact it’s similar to what most people will often do anyway: they describe a problem, then create some code to solve it.

Test-driven development simply formalizes the problem in a Python test case. More often, a newcomer to testing will create some code and later decide that it should have some tests. Perhaps it would have been better to write some tests earlier, but it’s never too late to get started.

Writing A Test

To create your first test, let’s introduce a bug into your Book model.

Say you have decided to create a custom method on your Book model to indicate whether the book has been published recently. Your Book model may look something like this:

import datetime
from django.utils import timezone

from django.db import models

# ... #

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

    def recent_publication(self):
        return self.publication_date >= timezone.now().date() - datetime.timedelta(we\
eks=8)

    # ... #

First we have imported two new modules: Python’s datetime and timezone from django.utils. We need these modules to be able to do calculations with dates. Then we have added a custom method to the Book model called recent_publication that works out what date it was 8 weeks ago and returns true if the publication date of the book is more recent.

So let’s jump to the interactive shell and test our new method:

python manage.py shell

>>> from books.models import Book
>>> import datetime
>>> from django.utils import timezone
>>> book = Book.objects.get(id=1)
>>> book.title
'Mastering Django: Core'
>>> book.publication_date
datetime.date(2016, 5, 1)
>>>book.publication_date >= timezone.now().date() - datetime.timedelta(weeks=8)
True 

So far so good, we have imported our book model and retrieved a book. Today is the 11th June, 2016 and I have entered the publication date of my book in the database as the 1st of May, which is less than 8 weeks ago, so the function correctly returns True.

Obviously, you will have to modify the publication date in your data so this exercise still works for you based on when you complete this exercise.

Now let’s see what happens if we set the publication date to a time in the future to, say, 1st September:

>>> book.publication_date
datetime.date(2016, 9, 1)
>>>book.publication_date >= timezone.now().date() - datetime.timedelta(weeks=8)
True 

Oops! Something is clearly wrong here. You should be able to quickly see the error in the logic – any date after 8 weeks ago is going to return true, including dates in the future.

So, ignoring the fact that this is a rather contrived example, lets now create a test that exposes our faulty logic.

Creating A Test

When you created your books app with Django’s startapp command, it created a file called tests.py in your app directory. This is where any tests for the books app should go. So let’s get right to it and write a test:

import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Book

class BookMethodTests(TestCase):

    def test_recent_pub(self):
        """
        recent_publication() should return False for future publication 
        dates.
        """

        futuredate = timezone.now().date() + datetime.timedelta(days=5)
        future_pub = Book(publication_date=futuredate)
        self.assertEqual(future_pub.recent_publication(), False)

This should all be pretty straight forward as it’s nearly exactly what we did in the Django shell, the only real difference is that we now have encapsulated our test code in a class and created an assertion that tests our recent_publication() method against a future date.

We will be covering test classes and the assertEqual method in greater detail later in the chapter – for now we just want to look at how tests work at a very basic level before getting on to more complicated topics.

Running Tests

Now we have created our test, we need to run it. Fortunately, this is very easy to do, jump into your terminal and type:

python manage.py test books 

After a moment, Django should print out something like this:

Creating test database for alias 'default'...
F
======================================================================
FAIL: test_recent_pub (books.tests.BookMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Nigel\ ... mysite\books\tests.py", line 25, in test_recent_pub
    self.assertEqual(future_pub.recent_publication(), False)
AssertionError: True != False

----------------------------------------------------------------------
Ran 1 test in 0.000s

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

What happened is this:

  • python manage.py test books looked for tests in the books application
  • it found a subclass of the django.test.TestCase class
  • it created a special database for the purpose of testing
  • it looked for methods with names beginning with “test”
  • in test_recent_pub it created a Book instance whose publication_date field is 5 days in the future; and
  • using the assertEqual() method, it discovered that its recent_publication() returns True, when it was supposed to return False
  • The test informs us which test failed and even the line on which the failure occurred. Note also that if you are on a *nix system or a Mac, the file path will be different.

That’s it for a very basic introduction to testing in Django. As I said at the beginning of the chapter, testing is a deep and detailed subject that is highly important to your career as a programmer. I can’t possibly cover all the facets of testing in a single chapter, so I encourage you to dig deeper into some of the resources mentioned in this chapter as well as the Django documentation.

For the remainder of the chapter, I will be going over the various testing tools Django puts at your disposal.