Browse Source

Fixed #35660 -- Made serialized_rollback and fixture data available in TransactionTestCase.setUpClass().

Jacob Walls 6 months ago
parent
commit
a060a22ee2

+ 23 - 2
django/test/testcases.py

@@ -208,6 +208,7 @@ class SimpleTestCase(unittest.TestCase):
     async_client_class = AsyncClient
     _overridden_settings = None
     _modified_settings = None
+    _pre_setup_ran_eagerly = False
 
     databases = set()
     _disallowed_database_msg = (
@@ -360,7 +361,10 @@ class SimpleTestCase(unittest.TestCase):
 
         if not skipped:
             try:
-                self._pre_setup()
+                if self.__class__._pre_setup_ran_eagerly:
+                    self.__class__._pre_setup_ran_eagerly = False
+                else:
+                    self._pre_setup()
             except Exception:
                 if debug:
                     raise
@@ -1090,6 +1094,7 @@ class TransactionTestCase(SimpleTestCase):
 
     # Subclasses can enable only a subset of apps for faster tests
     available_apps = None
+    _available_apps_calls_balanced = 0
 
     # Subclasses can define fixtures which will be automatically installed.
     fixtures = None
@@ -1107,6 +1112,20 @@ class TransactionTestCase(SimpleTestCase):
     # This can be slow; this flag allows enabling on a per-case basis.
     serialized_rollback = False
 
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        if not issubclass(cls, TestCase):
+            cls._pre_setup()
+            cls._pre_setup_ran_eagerly = True
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        if not issubclass(cls, TestCase) and cls._available_apps_calls_balanced > 0:
+            apps.unset_available_apps()
+            cls._available_apps_calls_balanced -= 1
+
     @classmethod
     def _pre_setup(cls):
         """
@@ -1119,6 +1138,7 @@ class TransactionTestCase(SimpleTestCase):
         super()._pre_setup()
         if cls.available_apps is not None:
             apps.set_available_apps(cls.available_apps)
+            cls._available_apps_calls_balanced += 1
             setting_changed.send(
                 sender=settings._wrapped.__class__,
                 setting="INSTALLED_APPS",
@@ -1216,8 +1236,9 @@ class TransactionTestCase(SimpleTestCase):
                 for conn in connections.all(initialized_only=True):
                     conn.close()
         finally:
-            if self.available_apps is not None:
+            if self.__class__.available_apps is not None:
                 apps.unset_available_apps()
+                self.__class__._available_apps_calls_balanced -= 1
                 setting_changed.send(
                     sender=settings._wrapped.__class__,
                     setting="INSTALLED_APPS",

+ 5 - 0
docs/releases/5.2.txt

@@ -260,6 +260,11 @@ Tests
   failures easier to read and enables :option:`test --pdb` to directly enter
   into the failing test method.
 
+* Data loaded from :attr:`~django.test.TransactionTestCase.fixtures` and from
+  migrations enabled with :ref:`serialized_rollback=True
+  <test-case-serialized-rollback>` are now available during
+  ``TransactionTestCase.setUpClass()``.
+
 URLs
 ~~~~
 

+ 5 - 0
docs/topics/testing/overview.txt

@@ -280,6 +280,11 @@ To prevent serialized data from being loaded twice, setting
 :data:`~django.db.models.signals.post_migrate` signal when flushing the test
 database.
 
+.. versionchanged:: 5.2
+
+    For :class:`TransactionTestCase`, serialized migration data is made
+    available during ``setUpClass()``.
+
 Other test conditions
 ---------------------
 

+ 22 - 12
docs/topics/testing/tools.txt

@@ -1262,25 +1262,35 @@ subclass::
 
 Here's specifically what will happen:
 
-* At the start of each test, before ``setUp()`` is run, Django will flush the
-  database, returning the database to the state it was in directly after
-  :djadmin:`migrate` was called.
+* During ``setUpClass()``, all the named fixtures are installed. In this
+  example, Django will install any JSON fixture named ``mammals``, followed by
+  any fixture named ``birds``. See the :ref:`fixtures-explanation` topic for
+  more details on defining and installing fixtures.
 
-* Then, all the named fixtures are installed. In this example, Django will
-  install any JSON fixture named ``mammals``, followed by any fixture named
-  ``birds``. See the :ref:`fixtures-explanation` topic for more details on
-  defining and installing fixtures.
+For most unit tests using :class:`TestCase`, Django doesn't need to do
+anything else, because transactions are used to clean the database after each
+test for performance reasons. But for :class:`TransactionTestCase`, the
+following actions will take place:
 
-For performance reasons, :class:`TestCase` loads fixtures once for the entire
-test class, before :meth:`~TestCase.setUpTestData`, instead of before each
-test, and it uses transactions to clean the database before each test. In any case,
-you can be certain that the outcome of a test will not be affected by another
-test or by the order of test execution.
+* At the end of each test Django will flush the database, returning the
+  database to the state it was in directly after :djadmin:`migrate` was
+  called.
+
+* For each subsequent test, the fixtures will be reloaded before ``setUp()``
+  is run.
+
+In any case, you can be certain that the outcome of a test will not be
+affected by another test or by the order of test execution.
 
 By default, fixtures are only loaded into the ``default`` database. If you are
 using multiple databases and set :attr:`TransactionTestCase.databases`,
 fixtures will be loaded into all specified databases.
 
+.. versionchanged:: 5.2
+
+    For :class:`TransactionTestCase`, fixtures were made available during
+    ``setUpClass()``.
+
 URLconf configuration
 ---------------------
 

+ 21 - 0
tests/migration_test_data_persistence/tests.py

@@ -1,3 +1,4 @@
+from django.core.management import call_command
 from django.test import TestCase, TransactionTestCase
 
 from .models import Book
@@ -19,6 +20,26 @@ class MigrationDataPersistenceTestCase(TransactionTestCase):
         )
 
 
+class MigrationDataPersistenceClassSetup(TransactionTestCase):
+    """
+    Data loaded in migrations is available during class setup if
+    TransactionTestCase.serialized_rollback = True.
+    """
+
+    available_apps = ["migration_test_data_persistence"]
+    serialized_rollback = True
+
+    @classmethod
+    def setUpClass(cls):
+        # Simulate another TransactionTestCase having just torn down.
+        call_command("flush", verbosity=0, interactive=False)
+        super().setUpClass()
+        cls.book = Book.objects.first()
+
+    def test_data_available_in_class_setup(self):
+        self.assertIsInstance(self.book, Book)
+
+
 class MigrationDataNormalPersistenceTestCase(TestCase):
     """
     Data loaded in migrations is available on TestCase

+ 0 - 0
tests/test_utils/fixtures/should_not_be_loaded.json → tests/test_utils/fixtures/person.json


+ 14 - 1
tests/test_utils/test_transactiontestcase.py

@@ -4,7 +4,7 @@ from django.db import connections
 from django.test import TestCase, TransactionTestCase, override_settings
 from django.test.testcases import DatabaseOperationForbidden
 
-from .models import Car
+from .models import Car, Person
 
 
 class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase):
@@ -68,3 +68,16 @@ class DisallowedDatabaseQueriesTests(TransactionTestCase):
         )
         with self.assertRaisesMessage(DatabaseOperationForbidden, message):
             Car.objects.using("other").get()
+
+
+class FixtureAvailableInSetUpClassTest(TransactionTestCase):
+    available_apps = ["test_utils"]
+    fixtures = ["person.json"]
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.elvis = Person.objects.get(name="Elvis Presley")
+
+    def test_fixture_loaded_during_class_setup(self):
+        self.assertIsInstance(self.elvis, Person)

+ 1 - 1
tests/test_utils/tests.py

@@ -1214,7 +1214,7 @@ class XMLEqualTests(SimpleTestCase):
 
 
 class SkippingExtraTests(TestCase):
-    fixtures = ["should_not_be_loaded.json"]
+    fixtures = ["person.json"]
 
     # HACK: This depends on internals of our TestCase subclasses
     def __call__(self, result=None):