Difference between TestCase and TransactionTestCase classes in django test

Could you please explain to someone the difference between the TestCase class and the TransactionTestCase class. I read the doc, but its only message is that TestCase runs a test on a DB transaction and uses rollback to "undo" the test in the DB, and if you need to manually manage transactions in your test, you will need to use django.test.TransactionTestCase.

Could you please help me understand the real difference with the example? I just want to know in what state TestCase fails? also is there an automatic rollback or do we need to write an instruction to rollback?

Please help me

+8


source to share


2 answers


The main difference between TestCase

and TransactionTestCase

is that it TestCase

wraps tests in blocks atomic()

ALL THE TIME. From the documentation :

Wraps tests in two nested atomic () blocks: one for the entire class and one for each test

Now, imagine you have a method that should throw an error if it is not in a block atomic()

. You are trying to write a test for this:

def test_your_method_raises_error_without_atomic_block(self):
    with self.assertRaises(SomeError):
        your_method()

      

This test will fail unexpectedly! The reason is, you guessed it, TestCase

REVERSE block tests atomic()

ALL THE TIME. This way your_method()

it won't throw an error, so this test fails. In this case, you must use TransactionTestCase to pass the test.

select_for_update () is a good example:



Evaluating a queryset with select_for_update () in auto-commit mode on backends that support SELECT ... FOR UPDATE is a TransactionManagementError

From the TransactionTestCase documentation :

with TestCase class you cannot verify that a block of code is executed in a transaction as required when using select_for_update ()

And if we look at the documentation for select_for_update()

, we see a warning:

Although select_for_update () usually fails in auto-commit mode because TestCase automatically wraps each test in a transaction, a call to select_for_update () in TestCase even outside the atomic () block will (perhaps unexpectedly) fail without a TransactionManagementError being called. To validate select_for_update () correctly, you must use TransactionTestCase.

Hope it helps!

+8


source


I would like to post an example and django code here so you can know how TransactionTestCase and TestCase work.

Both TransactionTestCase and TestCase inherit from SimpleTestCase. Difference:

  • When running the test, TestCase will check if the current function supports database transactions. If True, the transaction will be created and all test code is now in the "transaction block". And at the end of the test, TestCase will rollback all things to keep your database clean. Read the setUp and tearDown function below:

    @classmethod
    def setUpClass(cls):
            super(TestCase, cls).setUpClass()
            if not connections_support_transactions():
                return
            cls.cls_atomics = cls._enter_atomics()
    
            if cls.fixtures:
                for db_name in cls._databases_names(include_mirrors=False):
                        try:
                            call_command('loaddata', *cls.fixtures, **{
                                'verbosity': 0,
                                'commit': False,
                                'database': db_name,
                            })
                        except Exception:
                            cls._rollback_atomics(cls.cls_atomics)
                            raise
            cls.setUpTestData()
    
    
    @classmethod
    def tearDownClass(cls):
        if connections_support_transactions():
            cls._rollback_atomics(cls.cls_atomics)
            for conn in connections.all():
                conn.close()
        super(TestCase, cls).tearDownClass()
    
          

  • TransactionTestCase, however, does not start a transaction. It just cleans up the database after all tests have been run.

    def _post_teardown(self):
        try:
            self._fixture_teardown()
            super(TransactionTestCase, self)._post_teardown()
            if self._should_reload_connections():
                for conn in connections.all():
                    conn.close()
        finally:
            if self.available_apps is not None:
                apps.unset_available_apps()
                setting_changed.send(sender=settings._wrapped.__class__,
                                     setting='INSTALLED_APPS',
                                     value=settings.INSTALLED_APPS,
                                     enter=False)
    
    def _fixture_teardown(self):
        for db_name in self._databases_names(include_mirrors=False):
            call_command('flush', verbosity=0, interactive=False,
                         database=db_name, reset_sequences=False,
                         allow_cascade=self.available_apps is not None,
                         inhibit_post_migrate=self.available_apps is not None)
    
          

Now for some very simple examples of using select_for_update () mentioned in the official doc:



    class SampleTestCase(TestCase):
            def setUp(self):
                Sample.objects.create(**{'field1': 'value1', 'field2': 'value2'})

            def test_difference_testcase(self):
                sample = Sample.objects.select_for_update().filter()
                print(sample)


    class SampleTransactionTestCase(TransactionTestCase):
        def setUp(self):
            Sample.objects.create(**{'field1': 'value1', 'field2': 'value2'})

        def test_difference_transactiontestcase(self):
            sample = Sample.objects.select_for_update().filter()
            print(sample)

      

The first one will raise:

AssertionError: TransactionManagementError not raised

And the second will pass without errors.

0


source







All Articles