Tag Archives: Django

Testing Django – part 4 – setting up a dedicated CouchDB for tests

In this part of the Testing Django series we will learn how to set up a dedicated CouchDB database for our automated Django tests. It builds on the previous parts that can be found here:

Django supports different SQL systems natively. If you want to use NoSQL solutions for making your data persistent you need to tinker a little bit yourself sometimes. I used CouchDB and Couchdbkit in a Django project. One problem was that the test data was saved in the same CouchDB database as data that was entered through the “productive” web interface. Here I describe how to have separate a database for each case.

So let us get into the described trouble by changing our model to be made persistent as CouchDB document.

First we install Couchdbkit:

$ sudo easy_install -U Couchdbkit

Then we have to modify the settings.py so that our Django project is aware of it:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'couchdbkit.ext.django', # Add this
    'fruitsalad.fruits', # Added in previous parts
    'django_nose', # Added in previous parts
    'lettuce.django', # Added in previous parts
)

And we have to specify (also in settings.py) a database to use. The database does not have to exist as Couchdbkit will take care of this.

COUCHDB_DATABASES = (
     ('fruitsalad.fruits', 'http://127.0.0.1:5984/fruits')
)

Then we adapt our model:

from couchdbkit.ext.django.schema import *

class Fruit(Document):
    name = StringProperty()
    color = StringProperty()

    def set_name(self, name):
        self.name = name

    def set_color(self, color):
        self.color = color

    def is_yummy(self):
        return(True)

    def become_brown(self):
        self.color = "brown"

    def disappear(self):
        self.color = "transparent"

Okay, let’s use Django‘s shell to create such a fruit object and save it to the CouchDB database.

$ ./manage.py shell
Python 2.6.5  
Type "copyright", "credits" or "license" for more information.

IPython 0.10 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object'. ?object also works, ?? prints more.

In [1]: from fruitsalad.fruits.models import Fruit

In [2]: f = Fruit()

In [3]: f.name = "Banana"

In [4]: f.color = "yellow"

In [5]: f.save()

We can check the existence by using curl:

$ curl -X GET http://127.0.0.1:5984/fruits/_all_docs
{"total_rows":1,"offset":0,"rows":[
{"id":"4292816409d02b99378f31dd79e9b347","key":"4292816409d02b99378f31dd79e9b347","value":{"rev":"1-88a1fdab4ca60892f3143ba09cdf6205"}}
]}

This means we have (as expected) one document in the database. To have a closer look at it:

$ curl -X GET http://127.0.0.1:5984/fruits/4292816409d02b99378f31dd79e9b347
{"_id":"4292816409d02b99378f31dd79e9b347","_rev":"1-88a1fdab4ca60892f3143ba09cdf6205","color":"yellow","doc_type":"Fruit","name":"Banana"}

Alternatively use CouchDB‘s dashboard Futon that might be accessible at

http://localhost:5984/_utils/database.html?orange_test
depending on your CouchDB configuration.

We have to adapt our model test a little bit for the new constellation:

class TestFruit(object):

    def setup(self):
        self.fruit = Fruit()
        self.fruit.set_name("Papaya")
        self.fruit.set_color("orange")
        self.fruit.save()

    def test_color(self):
        nt.assert_equal(self.fruit.name, "Papaya")
        nt.assert_equal(self.fruit.color, "orange")

    def test_yumminess(self):
        nt.assert_true(self.fruit.is_yummy())

    def test_color_change(self):
        self.fruit.become_brown()
        self.fruit.save()
        nt.assert_not_equal(self.fruit.color, "orange")
        nt.assert_equal(self.fruit.color, "brown")

    def teardown(self):
        self.fruit.disappear()
        self.fruit.save()

If we run the test now …

$ ./manage.py test
Creating test database 'default'...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_user_permissions
Creating table auth_user_groups
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
sync `fruitsalad.fruits` in CouchDB
Installing index for auth.Permission model
Installing index for auth.Group_permissions model
Installing index for auth.User_user_permissions model
Installing index for auth.User_groups model
Installing index for auth.Message model
No fixtures found.
nosetests --verbosity 1
......
----------------------------------------------------------------------
Ran 6 tests in 0.352s

OK
Destroying test database 'default'...

… we can see that the documents produced by it are tainting the “productive” database:

$ curl -X GET http://127.0.0.1:5984/fruits/_all_docs
{"total_rows":4,"offset":0,"rows":[
{"id":"4292816409d02b99378f31dd79e9b347","key":"4292816409d02b99378f31dd79e9b347","value":{"rev":"1-88a1fdab4ca60892f3143ba09cdf6205"}},
{"id":"c85742c0ea99bd21549a1f058e4f10e6","key":"c85742c0ea99bd21549a1f058e4f10e6","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"de4e8d3ef236bc243f3768aa7b476ffc","key":"de4e8d3ef236bc243f3768aa7b476ffc","value":{"rev":"3-b227881c2cda9384dd26934854e31347"}},
{"id":"f8f15a0be7715ae852e75379f8d0b115","key":"f8f15a0be7715ae852e75379f8d0b115","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}}
]}

With every run it will become more documents. But we can circumvent this problem easily. We simply modify settings.py so that the application uses a different database if it is invoked by the test runner.

if "test" in sys.argv:
    COUCHDB_DATABASES = (
	('fruitsalad.fruits', 'http://127.0.0.1:5984/fruits_test'),
        )

If we run the tests againg …

$ ./manage.py test

we see that the database fruits_test is filled instead of the fruits database.

$ curl -X GET http://127.0.0.1:5984/fruits_test/_all_docs
{"total_rows":12,"offset":0,"rows":[
{"id":"008a039053c555d05a16ecf3aac1f9d0","key":"008a039053c555d05a16ecf3aac1f9d0","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"07dcba342255cec410c78f5252dde9c3","key":"07dcba342255cec410c78f5252dde9c3","value":{"rev":"3-b227881c2cda9384dd26934854e31347"}},
{"id":"19fe1c9088dce91d618cc763675c4f66","key":"19fe1c9088dce91d618cc763675c4f66","value":{"rev":"3-b227881c2cda9384dd26934854e31347"}},
{"id":"3feb80ae5aa35730a987cf90e1de34a0","key":"3feb80ae5aa35730a987cf90e1de34a0","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"456bc0ce470e5d333122bcc9d67b4aae","key":"456bc0ce470e5d333122bcc9d67b4aae","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"6028d26a7bf1b6ee23c43feda9341680","key":"6028d26a7bf1b6ee23c43feda9341680","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"69504e487a73a114757f55f256b4597b","key":"69504e487a73a114757f55f256b4597b","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"86f5a61dee535ed60374ffa4426abcc9","key":"86f5a61dee535ed60374ffa4426abcc9","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"8d620c6b6399e94233c06034506c4779","key":"8d620c6b6399e94233c06034506c4779","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}},
{"id":"96317d87c5f1f27ad68dfb702458c79d","key":"96317d87c5f1f27ad68dfb702458c79d","value":{"rev":"3-b227881c2cda9384dd26934854e31347"}},
{"id":"a8ae90da0b0e60adac137fb92a16883f","key":"a8ae90da0b0e60adac137fb92a16883f","value":{"rev":"3-b227881c2cda9384dd26934854e31347"}},
{"id":"e9f63c6460d8fad804f174ff850869ce","key":"e9f63c6460d8fad804f174ff850869ce","value":{"rev":"2-62250a0970ba7b627322c0a3ef889f09"}}
]}

The only downer is that our test database is getting bigger and bigger. So we have to clean it. To that we modify
fruits/tests.py slightly. In the top of the file we add

import fruitsalad.settings as settings
from couchdbkit.client import Database

and then we chance the setup method of the testing class :

    def setup(self):
        # cleaning the database
        db_url = settings.COUCHDB_DATABASES[0][1]  
        database = Database(db_url)
        database.flush()
        # nothing new
        self.fruit = Fruit()
        self.fruit.set_name("Papaya")
        self.fruit.set_color("orange")
        self.fruit.save()

Now before every test run the previous database content is removed. Alternatively move this cleaning part into the teardown method. The advantage of the here presented solution is that you can have a look a the data in the database if you need to.

Testing Django – part 3 – tdaemon

In this part we have a short look at the tool tdaemon and it’s application for testing Django.

Check out the other parts of this Testing Django series:

Writing tests in one thing – running them frequently is another thing. The lazy coder uses an change induced, automated test runner for this – this is not yet proper continuous integration testing but a first, local step into that direction. A good option for Django (and Python in general) is tdaemon.

The installation is pretty simple. Download the tdaemon.py from github, make the file executable and put it in your ~/bin folder (alternative just put it the project folder and call it with it’s path).

Once you have set it up you can run it in your Django project folder in the following manner:.

$ tdaemon --test-program=django

Whenever a file is changed tdaemon will detect that and automatically run the tests for you. Isn’t that handy?

Testing Django – part 2 – lettuce

The first part of this series was an introduction to Django testing with nose. In this part we will shed light on lettuce to practice Behaviour Driven Development (BDD). You can find all the other parts here:

The frame work lettuce is heavily inspired by cucumber – a BDD framework for Ruby. The key idea is to make specifications a communication tool between the different stake holders of a project (e.g. owner and the developer) and testable as well. To accomplish that the feature descriptions are written in a business-readable domain-specific language. A second layer (the so called step definition) is used to translate these specifications into testable code.

Starting with the dummy project from the first part we first need to install lettuce:

$ sudo pip install lettuce

And we have to configure the app to make use of lettuce (in the settings.py file):

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'fruitsalad.fruits', # From the first part
    'django_nose', # From the first part
    'lettuce.django' # Add now
)

By doing so manage.py gets a new command called harvest. Running this command will give the following output:

$ ./manage.py harvest
Django's builtin server is running at 0.0.0.0:8001
Oops!
could not find features at ./fruits/features

0 feature (0 passed)
0 scenario (0 passed)
0 step (0 passed)

This means we have to add features. So we setup a dedicated folder

$ mkdir fruits/features

and create a file called fruits/features/fruits_basics.feature. The precise name is not important as long as it ends with .feature. Let’s add the a feature definition to this file:

Feature: Fruits should be yummy.

    Scenario: Having a cherry
        Given I access the url "/fruits/cherry"
        Then I see the content of the cherry page

Lettuce digests this feature and looks for the step definition of it. As there is none so far it offers us some snippets that we can use:

$ ./manage.py harvest
Django's builtin server is running at 0.0.0.0:8000

Feature: Fruits should be yummy.              # fruits/features/fruits_basics.feature:1

  Scenario: Having a cherry                   # fruits/features/fruits_basics.feature:3
    Given I access the url "/fruits/cherry"   # fruits/features/fruits_basics.feature:4
    Then I see the content of the cherry page # fruits/features/fruits_basics.feature:5

1 feature (0 passed)
1 scenario (0 passed)
2 steps (2 undefined, 0 passed)

You can implement step definitions for undefined steps with these snippets:

# -*- coding: utf-8 -*-
from lettuce import step

@step(u'Given I access the url "(.*)"')
def given_i_access_the_url_group1(step, group1):
    pass
@step(u'Then I see the content of the cherry page')
def then_i_see_the_content_of_the_cherry_page(step):
    pass

0 feature (0 passed)
0 scenario (0 passed)
0 step (0 passed)
----------------------------------------

We copy the given code into a the step file fruits/features/fruits_basics.py and run lettuce again:

$ ./manage.py harvest
Django's builtin server is running at 0.0.0.0:8000

Feature: Fruits should be yummy.              # fruits/features/fruits_basics.feature:1

  Scenario: Having a cherry                   # fruits/features/fruits_basics.feature:3
    Given I access the url "/fruits/cherry"   # fruits/features/fruits_basics.py:5
    Then I see the content of the cherry page # fruits/features/fruits_basics.py:9

1 feature (1 passed)
1 scenario (1 passed)
2 steps (2 passed)
----------------------------------------

The test passes now but only as we did not assert anything so far. Let change this. We use Django‘s testing client which we covered shortly in the first part to test the existence of a page:

                                                            
from lettuce import step
from lettuce.django import django_url
from lettuce import world
from django.test.client import Client

@step(u'Given I access the url "(.*)"')
def given_i_access_the_url_group1(step, url):
    world.client = Client()
    world.response = world.client.get(django_url(url))

@step(u'Then I see the content of the cherry page')
def then_i_see_the_content_of_the_cherry_page(step):
    assert world.response.content == "Show the cherry page"

world is a little helper construction that is explained here. Let’s run lettuce again with these real specs:

/manage.py harvest
Django's builtin server is running at 0.0.0.0:8000

Feature: Fruits should be yummy.              # fruits/features/fruits_basics.feature:1

  Scenario: Having a cherry                   # fruits/features/fruits_basics.feature:3
    Given I access the url "/fruits/cherry"   # fruits/features/fruits_basics.py:8
    Then I see the content of the cherry page # fruits/features/fruits_basics.py:13

1 feature (1 passed)
1 scenario (1 passed)
2 steps (2 passed)

The test passes and we are happy. We only applied Python‘s assert function in these examples but as described here nose’s assertion (e.g. assert_equal, assert_true) functions that we met in the first part of the series can be used here, too.

Testing Django – part 1 – nose

The following series is meant to be a hands-on introduction to selected tools that can be used for automated Django testing. You can find all the parts here:

Intro

If you want to bring the good practice of automated testing or even Test Driven Development (TDD) to your Django project you have many different testing framework to choose from. In the first part of this series I want to introduce nose, in the second part we will have a look at Behavior Driven Development (BDD) with lettuce.

It is important to keep in mind that there are different levels of testing. The most important once are:

  1. unit testing
  2. integration testing
  3. system testing

Python‘s standard libraries and Django offer the unittest and doctest libs but I personally prefer the nose testing framework as it is powerful and has an easy syntax. Actually you can also use tests written for unittest or doctest and use nose a the test runner. You can even mix different types of testing styles. This is what I would call flexible.

Starting a project

Let’s start a little dummy project with one app. I am one of these folks who like to work with fruity examples:

$ django-admin.py startproject fruitsalad
$ cd fruitsalad/
$ django-admin.py startapp fruits

This should generate something that looks like this:

$ find 
.
./manage.py
./fruits
./fruits/views.py
./fruits/tests.py
./fruits/models.py
./fruits/__init__.py
./__init__.py
./settings.py
./urls.py

Set up the basic database information in the settings.py:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'fruit.db',
    }
}

Make manage.py executable

$ chmod u+x manage.py

and fire up the server:

$ ./manage.py runserver 
Validating models...
0 errors found

Django version 1.2.1, using settings 'fruitsalad.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

If you open http://127.0.0.1:8000/ in your browser you should get a welcome page.

Install nose and django-nose:

$ sudo pip install nose
$ sudo pip install django-nose

In the settings.py we need to tell the Django project to load our app and django-nose:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'fruitsalad.fruits', # Add this
    'django_nose',  #  Add this                                                            
)

Additionally we have to specify nose as our test runner:

TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'

Testing models

Test drive development means to write first the test and then the functionality to fulfill the expectations. So let’s write the first unit tests with nose. For nose is does not matter where in project folder you put the tests. It will find them (if you name the classes and functions correctly – i.e. starting with “Test” or “test”, respectively) but when creating the fruits app the file fruits/tests.py was created. I think it’s a good location for our tests so I will use it. django-admin.py put a unittest and a docstring based test example into this file already but we want to use nose syntax for our tests. We copy the following text into the file:

from fruitsalad.fruits.models import Fruit
import nose.tools as nt

class TestFruit(object):

    def setup(self):
	self.fruit = Fruit()
        self.fruit.set_name("Papaya")
        self.fruit.set_color("orange")

    def test_color(self):
        nt.assert_equal(self.fruit.name, "Papaya")
	nt.assert_equal(self.fruit.color, "orange")

    def test_yumminess(self):
	nt.assert_true(self.fruit.is_yummy())

    def test_color_change(self):
	self.fruit.become_brown()
        nt.assert_equal(self.fruit.color, "brown")

    def teardown(self):
	self.fruit.disappear()

Here we have one test class that contains three test functions. nose offers different possibilities to compare the expectations with real results. In the example we use assert_equal (the two given argument have to be equal) and assert_true (the given argument has to be True) but there are many others. Check pydoc nose.tools or this page for further references. We also use the setup and the teardown method to prepare our testing environment before the tests and to clean it after the tests.

We can now run nose and see if it test fails. As we configured nose as test runner we can do this in the following way:

$ ./manage.py test

Now we have our tests which define our expected behaviour of the model. We continue by creating a model that satisfies these tests. We add the following to fruits/model.py:

from django.db import models

class Fruit(models.Model):

    name = models.CharField(max_length=50)
    color = models.CharField(max_length=50)

    def set_name(self, name):
        self.name = name

    def set_color(self, color):
        self.color = color

    def is_yummy(self):
        return(True)

    def become_brown(self):
        self.color = "brown"

    def disappear(self):
        self.color = "transparent"

Now it should be possible to run the tests successfully:

$ ./manage.py test
Creating test database 'default'...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_user_permissions
Creating table auth_user_groups
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table fruits_fruit
Installing index for auth.Permission model
Installing index for auth.Group_permissions model
Installing index for auth.User_user_permissions model
Installing index for auth.User_groups model
Installing index for auth.Message model
No fixtures found.
nosetests --verbosity 1
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK
Destroying test database 'default'...

If a test fails you get a message like this:

[...]
  File "/home/myuser/fruitsalad/../fruitsalad/fruits/tests.py", line 12, in test_color
    nt.assert_equal(self.fruit.name, "Kiwi")
AssertionError: 'Papaya' != 'Kiwi'

----------------------------------------------------------------------
Ran 6 tests in 0.012s

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

Testing views

In the next step we are going to test the views of our app (if you are not a Djangonout so far: the expression “view” is used differently as in other web frameworks). This could be already see as integration testing as interaction between model and views could be tested here. But to keep it simple for this introduction we don’t use our models here but only let the views do the work. To access the views we use Django‘s Client class. We add the following test to our fruits/test.py file:

from django.test import Client

class TestFruitView(object):

    def setup(self):
        self.client = Client()

    def test_product_index(self):
        response = self.client.get("/fruits/")
        nt.assert_equal(response.content, "The index")

    def test_product_show(self):
        response = self.client.get("/fruits/papaya")
        nt.assert_equal(response.content, "Show the papaya page")

    def test_product_add(self):
        response = self.client.get("/fruits/add")
        nt.assert_equal(response.content, "Add a fruit")


A test run might return this now:

TemplateDoesNotExist: 404.html

----------------------------------------------------------------------
Ran 6 tests in 0.019s

FAILED (errors=3)
Destroying test database 'default'...

The first step to solve this is to specify our url pattern in the file url.py:

urlpatterns = patterns(
    '',
    (r'^fruits/add', 'fruits.views.add'),
    (r'^fruits/(\w+)', 'fruitsalad.fruits.views.show'),
    (r'^fruits/', 'fruitsalad.fruits.views.index')
)

Now our app knows which view should be accessed depending on the requested url. Still we get errors:

TemplateDoesNotExist: 404.html

----------------------------------------------------------------------
Ran 6 tests in 0.020s

FAILED (errors=3)
Destroying test database 'default'...

This is due to the fact that the views don’t exits so far – what we will change now. We adapt the fruits/views.py to look like this:

from django.http import HttpResponse

def index(request):
    return HttpResponse("The index")

def show(request, fruit):
    return HttpResponse("Show the %s page" % fruit)

def add(request):
    return HttpResponse("Add a fruit")

Now the new tests run successfully.

Destroying test database 'default'...
kuf@yersinia $ ./manage.py test
Creating test database 'default'...
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_user_permissions
Creating table auth_user_groups
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table fruits_fruit
Installing index for auth.Permission model
Installing index for auth.Group_permissions model
Installing index for auth.User_user_permissions model
Installing index for auth.User_groups model
Installing index for auth.Message model
No fixtures found.
nosetests --verbosity 1
......
----------------------------------------------------------------------
Ran 6 tests in 0.013s

OK
Destroying test database 'default'...

I hope you got a basic understanding how to test your Django app with nose. In the second part we will dive into the testing of our project with lettuce.