1

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.

Daniel Watkins
  • 1,656
  • 1
  • 15
  • 25
  • 1
    django automatically tears down database for each test run, this creates an isolated test environment – dm03514 Feb 21 '13 at 17:24
  • @dm03514 After poking around for a bit, I've realised that this is happening with django-nose test functions, not the more general case that I'd originally drafted the question with. – Daniel Watkins Feb 21 '13 at 17:27

1 Answers1

1

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.

Daniel Watkins
  • 1,656
  • 1
  • 15
  • 25
  • Well, strictly speaking those tests fail because other places in our codebase aren't cleaning up their mess. But you get the point. – Daniel Watkins Feb 22 '13 at 10:25