Learn how to write your own framework in Python!

You'll learn how to develop your own Python web framework to see how all the magic works beneath the scenes in Flask, Django, and the other Python-based web frameworks.

Jahongir Rahmonov

I'm a Software Engineer at Delivery Hero. Avid reader. WIUT graduate. Blogger and an amateur speaker.

I write about Python, Django, Kubernetes and sometimes something non-technical.

Welcome to my corner

Sun 11 June 2017

TestCase vs TransactionTestCase in Django

Based on my observation, a lot of developers don't seem to understand the difference between TestCase and TransactionTestCase in Django and how to use them. In this post, I will try to put the puzzle pieces together and make things clear.

TestCase class

Here is what the documentation has to say about the TestCase class:

Wraps the tests within two nested atomic() blocks: one for the whole class and one for each test.

Now imagine that you have a method that must be executed inside a transaction or else it raises an error. You could write a test similar to this:

class SomeTestCase(TestCase):
    def test_your_method_raises_error_without_atomic_block(self):
        with self.assertRaises(SomeError):
            your_method()

In this test, your_method() is called without any transaction and the test is asserting that it raises SomeError because of that.

However, this test will unexpectedly fail! The reason is, you guessed it, TestCase wraps the tests with atomic() blocks ALL THE TIME. Thus, your_method() will not raise an error, which is why this test will fail.

TransactionTestCase to the rescue

This is where TransactionTestCase should be used. It does not wrap the tests with atomic() block and thus you can test your special methods that require a transaction without any problem. The above test will pass with TransactionTestCase now:

class SomeTestCase(TransactionTestCase):
    def test_your_method_raises_error_without_atomic_block(self):
        with self.assertRaises(SomeError):
            your_method()

Real Life example

Let's see a real example now. A queryset method called select_for_update() is one of those methods that require to be inside a transaction. If you call it without any transaction, it raises an error.

Let's say you have a model called Item and you are calling select_for_update():

Item.objects.select_for_update()

It will immediately raise the following error:

TransactionManagementError: select_for_update cannot be used outside of a transaction.

Now, let's try to write tests for it with both TestCase and TransactionTestCase:

class ItemTestCase(TestCase):
    def setUp(self):
        self.item = Item.objects.create(name='hat')

    def test_select_for_update_raises_an_error_without_transaction(self):
        with self.assertRaises(TransactionManagementError):
            items = Items.objects.select_for_update().filter()
            print(items)  # needed to actually execute the query because they are lazy

Try to run the test and you will get the following:

AssertionError: TransactionManagementError not raised

The reason? TestCase wraps the tests with atomic() blocks ALL THE TIME. Good. Glad you remember this.

Now, let's make this test pass with TransactionTestCase:

class ItemTestCase(TransactionTestCase):
    def setUp(self):
        self.item = Item.objects.create(name='hat')

    def test_select_for_update_raises_an_error_without_transaction(self):
        with self.assertRaises(TransactionManagementError):
            items = Items.objects.select_for_update().filter()
            print(items)  # needed to actually execute the query because they are lazy

and voila! The test passes! Great!

I hope it will clear things out for some people. Let me know in the comments if something is still not clear.

Fight on!

Send
Share

If you liked what you read, subscribe below. Once in a while, I will send you a list of my new posts.