Django Unit Tests and Transactions
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