24π
The main difference between TestCase
and TransactionTestCase
is that TestCase
wraps the tests with atomic()
blocks ALL THE TIME. From the documentation:
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 should raise an error if it is not wrapped inside atomic()
block. You are trying to write a test for that:
def test_your_method_raises_error_without_atomic_block(self):
with self.assertRaises(SomeError):
your_method()
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. In this case, you should use TransactionTestCase to make your test pass.
select_for_update() is a clear case in point:
Evaluating a queryset with select_for_update() in autocommit mode on backends which support SELECT β¦ FOR UPDATE is a TransactionManagementError error
From the TransactionTestCase documentation:
with TestCase class, you cannot test that a block of code is executing within a transaction, as is required when using select_for_update()
And if we take a look at the documentation of select_for_update()
, we see a warning:
Although select_for_update() normally fails in autocommit mode, since TestCase automatically wraps each test in a transaction, calling select_for_update() in a TestCase even outside an atomic() block will (perhaps unexpectedly) pass without raising a TransactionManagementError. To properly test select_for_update() you should use TransactionTestCase.
Hope it helps!
5π
I would like to post some example and django code here so that you can know how TransactionTestCase
and TestCase
work.
Both TransactionTestCase
and TestCase
are inherit from SimpleTestCase
. Difference:
-
When runing the test,
TestCase
will check if the current DB support transaction feature. If true, a transaction will be created and all test code are now under a "transaction block". And at the end of the test,TestCase
will rollback all things to keep your DB clean. Read thesetUp()
andtearDown()
functions 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 simply flushes the DB after all tests done.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 some very simple example using select_for_update()
mentioned in official docs:
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 one will pass without an error.
- [Django]-Non-global middleware in Django
- [Django]-How to Create a form from a json-schema?
- [Django]-How do I add a "class" to a ChoiceField in Django?