Jul 01

Django Unit Tests and Transactions

While these are more properly integration tests than unit tests, it can be handy to have Django roll back the database transaction after each test method runs.

Coming to automated testing in Django from the Zope and Plone world, I was pleased to find full support for all the testing machinery that I've become used to: regular Python unit tests, and doctests. Of course, these being unit tests, they don't do any 'framework' management out of the box.

Unit tests are supposed to test your code, and just your code. However, once you're in a framework environment (be that Zope and Plone, Django, or anything else) then testing how your code integrates with that framework is vital. Zope and Plone provide unittest.TestCase subclasses (ZopeTestCase and PloneTestCase respectively) which provide a lot of scaffolding for you to be able to run integration tests. Part of that scaffolding is automatic transaction management. This hooks into Zope's transaction API to roll back the transaction after each test runs.

I wanted to do something similar for my Django test cases; I was finding 'state pollution' between my unit test runs, since data created by one test method isn't automatically cleaned out.

Django's transaction handling is much simpler than Zope's: it cares only about the one database transaction that the current request has, and only if the transaction support middleware is installed. This means that we can pretty easily crib the code from that middleware and use it in a test case base class:

from django.db import transaction

class TransactionalTestCase(unittest.TestCase):

def setUp(self):
super(TransactionalTestCase, self).setUp()

transaction.enter_transaction_management()
transaction.managed(True)

def tearDown(self):
super(TransactionalTestCase, self).tearDown()

if transaction.is_dirty():
transaction.rollback()
transaction.leave_transaction_management()

UPDATE: Fixed an error in the call to the base class' tearDown() method, which caused open transactions to hang around and (among other things) prevented the test database being cleanly dropped at the end of the test run.

After this, you can simply derive your test fixture classes from TransactionalTestCase, and make sure that you call the base setUp() and tearDown() methods if you do need to override them to perform your own setup and teardown.

My next spare time (hah!) project will be to integrate Django's transaction management into repoze.tm (which is Zope's transaction management suitably WSGI-fied). This would let a Django application participate in transactions with other transaction-aware components, making integration at the WSGI layer much more straightforward.

Comments

1 Osvaldo Santana Neto says...

def tearDown(self): - super(TransactionalTestCase, self).setUp() + super(TransactionalTestCase, self).tearDown()

Posted at 12:59 a.m. on July 2, 2008

2 Remco Wendt says...

Hi Dan,

I think you're better off using Django's testcase (django.test.TestCase) which does exactly this and more.

Remco

Posted at 8:48 a.m. on July 4, 2008

3 Dan Fairs says...

Yep - spot on! I seem to have a habit of only finding out about things after I've written something similar. Must get to know Google better...

Thanks for the comment.

Posted at 10:21 p.m. on July 28, 2008

4 Dan Fairs says...

Good spot - fixed.

Posted at 10:21 p.m. on July 28, 2008

5 Ben says...

It's probably worth a mentioning that django's transaction handling doesn't actually create a transaction (with a BEGIN) when you do enter_transaction_management. Postgres is the only db that does anything and it just calls set_isolation_level on the underlying dbapi connection. Ben

Posted at 11 a.m. on January 11, 2010

I've disabled comments for now due to spam problems - I'll turn them back on when I've fixed it!

This won't be published anywhere, it's just in case I need to contact you.

You can use Markdown in your comments. Be sensible!

Sorry about this, but I don't want spam comments.