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.