Difference between TestCase and TransactionTestCase classes in django test

后端 未结 2 1494
遥遥无期
遥遥无期 2021-02-19 04:32

Please explain the difference between the TestCase class and TransactionTestCase class. I have read the documentation but it\'s only saying that

相关标签:
2条回答
  • 2021-02-19 04:48

    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!

    0 讨论(0)
  • 2021-02-19 04:55

    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 func 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 simple flush 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 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 2rd one will pass without error.

    0 讨论(0)
提交回复
热议问题