views:

46

answers:

2

In my tests I do not only test for the perfect case, but especially for edge cases and error conditions. So I wanted to ensure some uniqueness constraints work.

While my test and test fixtures are pretty complicated I was able to track the problem down to the following example, which does not use any custom models. To reproduce the behaviour just save the code into tests.py and run the django test runner.

from django.contrib.auth.models import User
from django.db import IntegrityError
from django.test import TransactionTestCase

class TransProblemTest(TransactionTestCase):
    def test_uniqueness1(self):
        User.objects.create_user(username='user1', email='[email protected]', password='secret')
        self.assertRaises(IntegrityError, lambda :
            User.objects.create_user(username='user1', email='[email protected]', password='secret'))

    def test_uniqueness2(self):
        User.objects.create_user(username='user1', email='[email protected]', password='secret')
        self.assertRaises(IntegrityError, lambda :
            User.objects.create_user(username='user1', email='[email protected]', password='secret'))

A test class with a single test method works, but fails with two identical method implementations. The first test throwing exception breaks the Django testing environment and makes all the following tests fail.

I am using Django 1.1 with Ubuntu 10.04, Postgres 8.4 and psycopg2.

Does the problem still exist in Django 1.2?

Is it a known bug or am I missing something?

+1  A: 

I'm assuming when you say "a single test method works", you mean that it fails, raises the exception, but doesn't break the testing environment.

That said, you are running with AutoCommit turned off. In that mode, everything on the shared database connection is a single transaction by default and failures require the transaction to be aborted via ROLLBACK before a new one can be started. I recommend turning AutoCommit on, if possible--unless you have a need for wrapping multiple write operations into a single unit, having it off is overkill.

Matthew Wood
With "turning AutoCommit on" do you mean `DATABASE_OPTIONS = { "autocommit": True, }` as described in http://docs.djangoproject.com/en/1.1/ref/databases/#ref-databases? Sounds promising, so I tried to set it, but it did not have any effect - testing environment is still broken after running the first test method. Using TransactionMiddleware http://docs.djangoproject.com/en/1.1/topics/db/transactions/#tying-transactions-to-http-requests did not help either.
geekQ
If AutoCommit is on, then you cannot get the error "InternalError: current transaction is aborted, commands ignored until end of transaction block" since there is never a transaction block. If you are still getting this error, then it's not turned on. If you are getting a different error then AutoCommit isn't your solution and the "transaction is aborted" error is a side-effect of some other problem.
Matthew Wood
+1  A: 

Django has two flavors of TestCase: "plain" TestCase and TransactionTestCase. The documentation has the following to say about the difference between the two of them:

TransactionTestCase and TestCase are identical except for the manner in which the database is reset to a known state and the ability for test code to test the effects of commit and rollback. A TransactionTestCase resets the database before the test runs by truncating all tables and reloading initial data. A TransactionTestCase may call commit and rollback and observe the effects of these calls on the database.

A TestCase, on the other hand, does not truncate tables and reload initial data at the beginning of a test. Instead, it encloses the test code in a database transaction that is rolled back at the end of the test.

You are using TransactionTestCase to execute these tests. Switch to plain TestCase and you will see the problem vanish provided you maintain the existing test code.

Why does this happen? TestCase executes test methods inside a transaction block. Which means that each test method in your test class will be run inside a separate transaction rather than the same transaction. When the assertion (or rather the lambda inside) raises an error it dies with the transaction. The next test method is executed in a new transaction and therefore you don't see the error you've been getting.

However if you were to add another identical assertion in the same test method you would see the error again:

class TransProblemTest(django.test.TestCase):
    def test_uniqueness1(self):
        User.objects.create_user(username='user1', email='[email protected]', password='secret')
        self.assertRaises(IntegrityError, lambda :
            User.objects.create_user(username='user1', email='[email protected]', password='secret'))
        # Repeat the test condition.
        self.assertRaises(IntegrityError, lambda :
            User.objects.create_user(username='user1', email='[email protected]', password='secret'))

This is triggered because the first assertion will create an error that causes the transaction to abort. The second cannot therefore execute. Since both assertions happen inside the same test method a new transaction has not been initiated unlike the previous case.

Hope this helps.

Manoj Govindan
Thanks! This explains what is going on behind the scene. The docmntn gives the impression that TransactionTestCase offers even more isolation than TestCase. But due to a (bug in Django?) it offers less isolation - one test destroys the whole testing environment. Can I expect similar problems in production - should I avoid using constraints at all because hitting a constraint will break the code running afterwards or will autocommit and catching exceptionshttp://docs.djangoproject.com/en/1.1/topics/db/transactions/#database-level-autocommit work? "autocommit":True didn't help in testing env.
geekQ