`assertRaises` makes transaction fail and (silently) stops test execution

Hey y’all

I’ve been writing my first tests in django (and I like it).
However I was surprised to find out that assertRaises stops the execution of a test.

Writing something like

def test_no_create_if_valid_False(self):
    self.assertRaises(Exception, Foo.objects.create, valid=False)
    self.assertEqual(0, Foo.objects.count())

Results in

django.db.transaction.TransactionManagementError: An error occurred in the current transaction. You can’t execute queries until the end of the ‘atomic’ block.

I found out I could fix this by wrapping the assertRaises

        try:
            with transaction.atomic():
                self.assertRaises(Exception, Foo.objects.create, valid=False)
        except Exception:
            pass

That’s quite verbose.

But what made me write this post is that if I want to check for another Exception in the same test, it silently does not execute the second call

# Raises Exception - transaction stopped
self.assertRaises(Exception, Foo.objects.create, valid=False) 
# The following Exception is not raised but the test passes
self.assertRaises(Exception, Foo.objects.create, valid=True) 

again wrapping the first assertRaises would fix this.

So is this a bug? It made me learn about atomic wrappers in django (which is nice), but only after I got the TransactionManagementError. I would have gone on writing multiple Exceptions inn one test wrongly assuming that they all fail.

It’s not a bug, your assertion catches any subclass of Exception so also TransactionManagementError:

TransactionManagementErrorProgrammingErrorDatabaseErrorErrorException.

@felixxm thanks again for the quick reply.

It’s not a bug, your assertion catches any subclass of Exception so also TransactionManagementError:

The assertion does not catch the exception.

def test_no_create_if_valid_False(self):
    self.assertRaises(Exception, Foo.objects.create, valid=False)
    self.assertEqual(0, Foo.objects.count())

raises the Exception. (namely the second evaluation to Foo)

I would expect the implementation in django.test.TestCase.asserEqual to wrap the ‘plain’ assertRaises with the “try - with atomic - catch” wrapper (or something more elaborate) I wrote above.

side note: I’m aware that broadly catching Exception is not a good style, I just tried to come up with a MWE

I was talking about your second example with two subsequent self.assertRaises(Exception, ...) calls.

assertRaises() is from Python’s unittest. IMO, Django shouldn’t add any implicit wrapping to it.

Thanks for the clarification.
Do I understand correctly, that you would consider the following best practice?

def test_valid_eq_False_fails_integrity__creating_nothing(self):
    try:
        with transaction.atomic():
            self.assertRaises(IntegrityError, Foo.objects.create, valid=False)
    except IntegrityError:
        pass
    self.assertEqual(0, Foo.objects.count())

Or am I missing something?

IntegrityError is caught by assertRaises() so try ... except is unnecessary:

with transaction.atomic():
    self.assertRaises(IntegrityError, Foo.objects.create, valid=False)
self.assertEqual(Foo.objects.count(), 0)

Alright that looks good.
When I was playing around the tests only passed after I added the try-except but that must have been something else.
Thanks a lot for taking the time to fill me in.

an alternative solution is to use TransactionTestCase instead of TestCase in your tests.

see this SO answer for more details