Reputation: 867
Please explain the difference between the TestCase
class and TransactionTestCase
class. I have read the documentation but it's only saying that TestCase
runs tests in a database transaction and uses rollback to "undo" the test in the database, and if you need to manually manage transactions within your test, you would need to use django.test.TransactionTestCase
.
Please help me understand the actual difference with an example.
In what condition will TestCase
fail? Do rollbacks happen automatically or do we have to write code to do the rollback?
Upvotes: 30
Views: 17954
Reputation: 13733
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!
Upvotes: 27
Reputation: 61
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 the setUp()
and tearDown()
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.
Upvotes: 6