I’ve never written any tests in my life, but I’d like to start writing tests for my Django projects. I’ve read some articles about tests and decided to try to write some tests for an extremely simple Django app or a start.

The app has two views (a list view, and a detail view) and a model with four fields:

class News(models.Model):
    title = models.CharField(max_length=250)
    content = models.TextField()
    pub_date = models.DateTimeField(default=datetime.datetime.now)
    slug = models.SlugField(unique=True)

I would like to show you my tests.py file and ask:

Does it make sense?

Am I even testing for the right things?

Are there best practices I’m not following, and you could point me to?

my tests.py (it contains 11 tests):

# -*- coding: utf-8 -*-
from django.test import TestCase
from django.test.client import Client
from django.core.urlresolvers import reverse
import datetime
from someproject.myapp.models import News

class viewTest(TestCase):
    def setUp(self):
        self.test_title = u'Test title: b?rek??'
        self.test_content = u'This is a content 156'
        self.test_slug = u'test-title-bareksc'
        self.test_pub_date = datetime.datetime.today()

        self.test_item = News.objects.create(
            title=self.test_title,
            content=self.test_content,
            slug=self.test_slug,
            pub_date=self.test_pub_date,
        )

        client = Client()
        self.response_detail = client.get(self.test_item.get_absolute_url())
        self.response_index = client.get(reverse('the-list-view'))

    def test_detail_status_code(self):
        """
        HTTP status code for the detail view
        """
        self.failUnlessEqual(self.response_detail.status_code, 200)

    def test_list_status_code(self):
        """
        HTTP status code for the list view 
        """
        self.failUnlessEqual(self.response_index.status_code, 200)

    def test_list_numer_of_items(self):
        self.failUnlessEqual(len(self.response_index.context['object_list']), 1)      

    def test_detail_title(self):
        self.failUnlessEqual(self.response_detail.context['object'].title, self.test_title)    

    def test_list_title(self):
        self.failUnlessEqual(self.response_index.context['object_list'][0].title, self.test_title)

    def test_detail_content(self):
        self.failUnlessEqual(self.response_detail.context['object'].content, self.test_content)    

    def test_list_content(self):
        self.failUnlessEqual(self.response_index.context['object_list'][0].content, self.test_content) 

    def test_detail_slug(self):
        self.failUnlessEqual(self.response_detail.context['object'].slug, self.test_slug)    

    def test_list_slug(self):
        self.failUnlessEqual(self.response_index.context['object_list'][0].slug, self.test_slug)

    def test_detail_template(self):
        self.assertContains(self.response_detail, self.test_title)
        self.assertContains(self.response_detail, self.test_content)

    def test_list_template(self):       
        self.assertContains(self.response_index, self.test_title) 

I am not perfect in testing but a few thoughts:

Basically you should test every function, method, class, whatever, that you have written by yourself.

This implies that you don’t have to test functions, classes, etc. which the framework provides.

That said, a quick check of your test functions:

  • test_detail_status_code and test_list_status_code:
    Ok to check whether you have configured the routing properly or not. Even more important when you provide your own implementation of get_absolute_url().

  • test_list_numer_of_items:
    Ok if a certain number of items should be returned by the view. Not necessary if the number is not important (i.e. arbitrary).

  • test_detail_template and test_list_template:
    Ok to check whether template variables are correctly set.

  • All the other functions: Not necessary.
    What your are basically testing here is whether the ORM worked properly, whether lists work as expected and whether object properties can be accessed (or not). As long as you don’t change e.g. the save() method of a model and/or provide your custom logic, I would not test this. You should trust the framework developers that this works properly.

You only should have to test what you have (over)written.

The model classes are maybe a special case. You basically have to test them, as I said, if you provide custom logic. But you should also test them against your requirements. E.g. it could be that a field is not allowed to be null (or that it has to be a certain datatype, like integer). So you should test that storing an object fails, if it has a null value in this field.
This does not test the ORM for correctly following your specification but test that the specification still fulfills your requirements. It might be that you change the model and change some settings (by chance or because you forgot about the requirements).
But you don’t have to test e.g. methods like save() or wether you can access a property.

Of course when you use buggy third party code… well things can be different. But as Django uses the test framework itself to verify that everything is working, I would assume it is working.

To sum up:
Test against your requirements, test your own code.

This is only my point of view. Maybe others have better proposals.

Break your tests into two completely separate kinds.

  • Model tests. Put these in your models.py file with your model. These tests will exercise the methods in your model classes. You can do simple CRUD (Create, Retrieve, Update, Delete) to simply prove that your model works. Don’t test every attribute. Do test field defaults and save() rules if you’re curious.

    For your example, create a TestNews class that creates, gets, updates and deletes a News item. Be sure to test the default date results. This class should be short and to the point. You can, if your application requires it, test various kinds of filter processing. Your unit test code can (and should) provide examples of the “right” way to filter News.

  • UI Tests. Put these in a separate tests.py file. These tests will test the view functions and templates.

    • Name the TestCase with the “condition” you’re creating. “TestNotLoggedIn”. “TestLoggedIn”. “TestNoValidThis”. “TestNotAllowedToDoThat”. Your setUp will do the login and any other steps required to establish the required condition.

    • Name each test method with the action and result. “test_get_noquery_should_list”, “test_post_should_validate_with_errors”, “test_get_query_should_detail”.