Note
A newer version of this tutorial using Django 1.9 is available from Leanpub: https://leanpub.com/tangowithdjango19
It is good practice to get into the habit of writing and developing tests. A lot of software engineering is about writing and developing tests and test suites in order to ensure the software is robust. Of course, most of the time, we are too busy trying to build things to bother about making sure that they work. Or too arrogant to believe it would fail.
According to the Django Tutorial, there are numerous reasons why you should include tests:
According to the Python Guide http://docs.python-guide.org/en/latest/writing/tests/, there are a number of general rules you should try to follow when writing tests. Below are some main rules:
Note
Currently this chapter provides the very basics of testing and follows a similar format to the Django Tutorial, with some additional notes. We hope to expand this further in the future.
In built in Django is machinery to test the applications built. You can do this by issuing the following command:
$ python manage.py test rango
Creating test database for alias 'default'...
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
Destroying test database for alias 'default'...
This will run through the tests associated with the rango application. At the moment, nothing much happens. That is because you may have noticed the file rango/tests.py only contains an import statement. Everytime you create an application, Django automatically creates such a file to encourage you to write tests.
From this output, you might also notice that a database called default is referred to. When you run tests, a temporary database is constructed, which your tests can populate, and perform operations on. This way your testing is performed independently of your live database.
Ok, lets create a test. In the Category model, we want to ensure that views are either zero or positive, because the number of views, let’s say, can never be less than zero. To create a test for this we can put the following code into rango/tests.py:
from django.test import TestCase
from rango.models import Category
class CategoryMethodTests(TestCase):
def test_ensure_views_are_positive(self):
"""
ensure_views_are_positive should results True for categories where views are zero or positive
"""
cat = Category(name='test',views=-1, likes=0)
cat.save()
self.assertEqual((cat.views >= 0), True)
The first thing you should notice, if you have not written tests before, is that we have to inherit from TestCase. The naming over the method in the class also follows a convention, all tests start with test_ and they also contain some type of assertion, which is the test. Here we are check if the values are equal, with the assertEqual method, but other types of assertions are also possible. See the Python Documentation on unit tests, https://docs.python.org/2/library/unittest.html for other commands (i.e. assertItemsEqual, assertListEqual, assertDictEqual, etc). Django’s testing machinery is derived from Python’s but also provides an number of other asserts and specific test cases.
Now lets run test:
$ python manage.py test rango
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_ensure_views_are_positive (rango.tests.CategoryMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/leif/Code/tango_with_django_project_17/rango/tests.py", line 12, in test_ensure_views_are_positive
self.assertEqual((cat.views>=0), True)
AssertionError: False != True
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
As we can see this test fails. This is because the model does not check whether the value is less than zero or not. Since we really want to ensure that the values are non-zero, we will need to update the model, to ensure that this requirement is fulfilled. Do this now by adding some code to the Category models, save() method, that checks the value of views, and updates it accordingly.
Once you have updated your model, you can now re-run the test, and see if your code now passes it. If not, try again.
Let’s try adding another test, that ensures an appropriate slug line is created i.e. one with dashes, and in lowercase. Add the following code to rango/tests.py:
def test_slug_line_creation(self):
"""
slug_line_creation checks to make sure that when we add a category an appropriate slug line is created
i.e. "Random Category String" -> "random-category-string"
"""
cat = Category('Random Category String')
cat.save()
self.assertEqual(cat.slug, 'random-category-string')
Does your code still work?
So far we have writtent tests that focus on ensuring the integrity of the data housed in the models. Django also provides testing mechanisms to test views. It does this with a mock client, that internally calls a django view via the url. In the test you have access to the response (including the html) and the context dictionary.
Let’s create a test that checks that when the index page loads, it displays the message that There are no categories present, when the Category model is empty.
from django.core.urlresolvers import reverse
class IndexViewTests(TestCase):
def test_index_view_with_no_categories(self):
"""
If no questions exist, an appropriate message should be displayed.
"""
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "There are no categories present.")
self.assertQuerysetEqual(response.context['categories'], [])
First of all, the django TestCase has access to a client object, which can make requests. Here, it uses the helper function reverse to look up the url of the index page. Then it tries to get that page, where the response is stored. The test then checks a number of things: did the page load ok? Does the response, i.e. the html contain the phrase “There are no categories present.”, and does the context dictionary contain an empty categories list. Recall that when you run tests, a new database is created, which by default is not populated.
Let’s now check the resulting view when categories are present. First add a helper method.
from rango.models import Category
def add_cat(name, views, likes):
c = Category.objects.get_or_create(name=name)[0]
c.views = views
c.likes = likes
c.save()
return c
Then add another method to the class IndexViewTests(TestCase):
def test_index_view_with_categories(self):
"""
If no questions exist, an appropriate message should be displayed.
"""
add_cat('test',1,1)
add_cat('temp',1,1)
add_cat('tmp',1,1)
add_cat('tmp test temp',1,1)
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "tmp test temp")
num_cats =len(response.context['categories'])
self.assertEqual(num_cats , 4)
In this test, we populate the database with four categories, and then check if the page loads, if it contains the text tmp test temp and if the number of categories is equal to 4.
#TODO(leifos): add in some tests showing how to test different forms i.e. login etc.
#TODO(leifos): add an example using either Django’s test client and/or Selenium, which is are “in-browser” frameworks to test the way the HTML is rendered in a browser.
Code coverage measures how much of your code base has been tested, and how much of your code has been put through its paces via tests. You can install a package called coverage via with pip install coverage which automatically analyses how much code coverage you have. Once you have coverage installed, run the following command:
$ coverage run --source='.' manage.py test rango
This will run through all the tests and collect the coverage data for the rango application. To see the coverage report you need to then type:
$ coverage report
Name Stmts Miss Cover
--------------------------------------------------------------
manage 6 0 100%
populate 33 33 0%
rango/__init__ 0 0 100%
rango/admin 7 0 100%
rango/forms 35 35 0%
rango/migrations/0001_initial 5 0 100%
rango/migrations/0002_auto_20141015_1024 5 0 100%
rango/migrations/0003_category_slug 5 0 100%
rango/migrations/0004_auto_20141015_1046 5 0 100%
rango/migrations/0005_userprofile 6 0 100%
rango/migrations/__init__ 0 0 100%
rango/models 28 3 89%
rango/tests 12 0 100%
rango/urls 12 12 0%
rango/views 110 110 0%
tango_with_django_project/__init__ 0 0 100%
tango_with_django_project/settings 28 0 100%
tango_with_django_project/urls 9 9 0%
tango_with_django_project/wsgi 4 4 0%
--------------------------------------------------------------
TOTAL 310 206 34%
We can see from the above report that critical parts of the code have not been tested, ie. rango/views. For more details about using the package coverage visit: http://nedbatchelder.com/code/coverage/
Check out the tutorial on test driven development by Harry Percival.