Test-driving Django-application development

February 8th, 2009 by bodhi Leave a reply »
3
Digg me

Test-Driven Development (TDD) is something that I’m currently really excited about. It’s nothing new in the world of software development, but it’s very new to me. I have read lots about it but I haven’t been able to start experiencing with it at work for various reasons (excuses!). Anyway, finally I decided to start a personal project of my own, where I would begin to start learning TDD in practice. I chose to re-create a webstore-application I had previously written in PHP many years ago. I guess it would’ve been easier to start with something else than web-development but I’ve been playing around with a webframework called Django, which is excellent, and I figured I should combine those two. For a second I thought about using some PHP framework like Zend, but I just prefer Python.

I started by googling, if someone had used TDD while developing Django-applications. There was not much to be found, and for the most part it was outdated. However, I found some interesting blog posts about testing in general with django. I especially found small django tips from one newbie to another by Adam Smith very useful. I had worked with Django before and I knew the basics of TDD so low search results didn’t matter much. I was ready to begin.

The first thing I did was to set-up the development environment. I created the Django-project and the app inside it. I guess there are many ways you could Test-Drive in this situation but I wanted to use Django’s way of testing. So basically this means that I need to run the tests by executing “manage.py test” within my project. This will sniff the insides of django-applications for tests.py file (and models.py with tests inside that). The result of the command is the same you get from standard Python unittests runs. Of course you can specify the app with the command, but in the beginning there is just one app to begin with, so it was ok to run all tests. Everything so far, was created by Django, including example tests.py file within the application. You can use Python Unittest or Docstring frameworks. I chose Unittest, since I’m more familiar with the xUnit frameworks such as JUnit. I think there was a mention in the Django docs, that you can also use other test tools.

I experimented a little with the Python interactive interpreter on how the tests work. At this time I started to think that having just one tests.py is going get crowded pretty soon so I created a module my_tests under the application and created model_tests.py and view_tests.py under it. I figured I wanted to keep the views as compact as possible and write the logic to either to the model classes or somewhere else, which would be decided later. For now, model and view tests should be enough to get me started. I also wanted to separate my test files from the actual code. Here is a sketch of my project configuration:

Django-project/
--settings.py
--/my_webstore_application/
----views.py
----models.py
----tests.py
------/my_tests/
--------model_tests.py
--------view_tests.py
--------test_utils.py

The tests.py is just a collection of suites:

import unittest
import my_tests.view_tests
import my_tests.model_tests
 
def suite():
    suite1 = unittest.TestLoader().loadTestsFromModule(my_tests.view_tests)
    suite2 = unittest.TestLoader().loadTestsFromModule(my_tests.model_tests)
    alltests = unittest.TestSuite([suite1, suite2])
    return alltests

I also created a run-configuration for Eclipse, so the RED-GREEN-REFACTOR –cycle would be as fluent as possible. Here are some of the first tests I wrote (These are actually somewhat refactored, but the basic idea has remained):

*model_tests.py

from django.test import TestCase
from django.core.files.images import ImageFile  
from webstore.models import Product, Category, SubCategory, Image
from utils import *
 
 
class ProductTest(TestCase):
    def test_create_product(self):
        product = Product(name='test product 1', price="15.50", 
                          description='What a great product!')
        self.assertEquals(product.name, str(product))
 
class CategoryTest(TestCase):
    def test_create_category(self):
        category = Category(name='test category 1')
        self.assertEquals(category.name, str(category))

*view_tests.py

from django.test.client import Client
from django.test import TestCase
from utils import *
from merinostore.webstore.models import *
 
class ProductTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.category = Category(name='test category 1')
        self.category.save()
        self.subcategory = SubCategory(name='test subcategory 2')
        self.subcategory.save()
        self.product = Product(name='test product 1', price='15.50', 
                               description='What a great product!',
                               category=self.category, subcategory=self.subcategory)
        self.product.save() 
 
    def test_list_products_template(self):
        response = self.client.get('/products/')
        self.failUnlessEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'product/list_products.html')
 
    def test_list_products(self):
        create_products(self.category, amount=10)
        response = self.client.get('/products/')
        self.failUnlessEqual(len(response.context[1]['products']), 11)

So I started with the models. I soon figured out that at this point there is virtually no code created by me in models so I stopped doing those and concentrated on the view tests. If the models are broken, they cannot be used anywhere. Later I started to write some more model tests, as I defined my own functions for them. As you can see, I use the Django helpers such as Client, which can be used to emulate the browser. I also use Django’s own TestCase class for assertions.

I was a little bit concerned about testing through views. This means I deal with real instances of the models, which exist in the database. It isn’t unit testing anymore if the tests hit the database, right? Luckily, Django’s test system is pretty good by default. It creates in-memory sqlite database for testing and you can even use fixtures with that. I just couldn’t find any reason for doing any heavier separation between the code and the database as running the tests were fast enough for me. Webstore application itself is mostly just about displaying items from the database. Of course, with the order handling and such tasks, I still plan to keep the logic separated from the views as much as possible. I figured as long as I write tests in order to drive design, it’s good.

I’m still in the process of developing the application, I have just finished the first feature, which is displaying of product list by categories (I copied the html stuff from the old store). What I didn’t do is test drive the templates. It just feels too much work, but I guess I could try that one as well.

I’m quite happy about the whole process in generally. At first I felt that I was proceeding very slowly, but at the same time it felt surprisingly good to get the GREEN showing up. I also created a simple script to import the data (products for now) from my previous webstore implementation, and I didn’t use TDD for that. The reason I didn’t Test-Drive is because I felt the script is so small and will be used only once so I wanted just to be done with it. It turned out to be hideous, and now I feel bad I didn’t TDD it (I think I’m going to rewrite that as well for practice). So I guess I’m learning something!

I’ll post more on the subject as I proceed or get the application ready. I’m sure that my tests aren’t very good and I’m doing many things wrong, but in order to better myself I hope I get some feedback. Till next time!

Share:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • StumbleUpon
Advertisement

8 comments

  1. Tom says:

    Great post. In your, view_tests.py – test_list_products(self): the last line is, self.failUnlessEqual(len(response.context[1]['products']), 11)

    Do you know why the context is a list?

    I have stumbled upon this issue recently. I figure it should not be.

  2. bodhi says:

    That was something I also just found out, while working on this project. At first, when I didn’t have any template inheritance defined, it wasn’t a list. So in my test I had only; self.failUnlessEqual(len(response.context['products']), 11)

    Then I created a base template which the current template (list_products.html) extended, and that broke my test. I can only guess what’s going on, but it seems that the context is somehow template specific, and adding another layer to the template hierarchy, adds another item to the list. I might be wrong, I haven’t tried to add a template in between to see what happens. But I guess the response.context is actually always a list.

  3. Tom says:

    http://docs.djangoproject.com/en/dev/topics/testing/

    The link proves your assumption.

    “context¶

    The template Context instance that was used to render the template that produced the response content.

    If the rendered page used multiple templates, then context will be a list of Context objects, in the order in which they were rendered.

  4. Tommygun says:

    Great Post. This inspired me to clean up my ugly test cases.

  5. Rory says:

    Wow thanks, best Django testing post I’ve found! Props!

  6. Dima says:

    Hey. I am just wondering if you’re still using TDD with django? If so, i’d like to hear what’s new? I am newbie at django, as well newbie in TDD, but i think it’s great approach, so i am trying to become familiar with it. For now i am using tddspry for composing tests (you can check that out on github). Anyway, i feel tedium writting tests and it’s hard to precreate a test case for future functionality. Any advices?

  7. bodhi says:

    Sorry for my late reply, I’ve just moved to China and things have been little hectic. Unfortunately I have not being using Django lately, but if I was, I would definitely continue my exploration with TDD. Tddspry seems interesting, I will definitely check that out when I’m next developing with Django. And what comes to test first, it shouldn’t feel tedious, doing test-last is tedious :) I wish I could give you some brilliant advice, but I’m all out. The only thing I can think of is that when you really start to notice the benefits of doing TDD, writing tests becomes a joy instead of a pain. So it might take some time and maybe some bigger project, as at least I’ve found doing TDD in very small applications (write once, use once) is not that beneficial.

  8. Dima says:

    Huh, just finished not that big project where i was writting test-last :D And I can confidently tell that TDD is a really great approach, even for such a lazy monkey as me. I am starting working at company which implements all projects using TDD from monday on, hope it will be a nice practice and doze of experience for me. Maybe i will be able to write some interesting post about TDD with django by my own soon.

Leave a Reply