Search code examples
pythondjangounit-testingpostgresqldjango-nose

How can I clean up database changes made in django-nose test functions?


We write our test suite using nose function tests for a variety of reasons.

When running the test suite for our Django application, we would like to avoid leaking any data out of these tests (as with django.test.TestCase), because that leads to coupling and hard to diagnose failures.

The most obvious means of solving this is a decorator which we can just wrap around tests that we want to be cleaned-up after, but I'm not married to that if a different solution can get us what we want.

We run on PostgreSQL, so Postgres-specific solutions would be fine.


Solution

  • I've spent a bit of time looking at this today, and have come up with the following decorator:

    from functools import wraps
    
    from django.db import transaction
    from mock import patch
    
    def rollback_db_changes(func):
        """Decorate a function so that it will be rolled back once completed."""
        @wraps(func)
        @transaction.commit_manually
        def new_f(*args, **kwargs):
            def fake_commit(using=None):
                # Don't properly commit the transaction, so we can roll it back
                transaction.set_clean(using)
            patcher = patch('django.db.transaction.commit', fake_commit)
            patcher.start()
            try:
                return func(*args, **kwargs)
            finally:
                patcher.stop()
                transaction.rollback()
        return new_f
    

    We perform the patching so that the Django test client doesn't close the transaction without us being able to roll it back. This allows the following tests to pass:

    from django.contrib.auth.models import User
    
    @rollback_db_changes
    def test_allowed_access():
        user = User.objects.create(username='test_user')
        eq_(1, User.objects.count())
    
    
    @rollback_db_changes
    def test_allowed_access_2():
        user = User.objects.create(username='test_user')
        eq_(1, User.objects.count())
    

    Previously the second test to run could not create a user with a duplicated user name.