From a060a22ee2dde7aa29a5a29120087c4864887325 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 4 Sep 2024 09:33:44 -0400 Subject: [PATCH] Fixed #35660 -- Made serialized_rollback and fixture data available in TransactionTestCase.setUpClass(). --- django/test/testcases.py | 25 ++++++++++++-- docs/releases/5.2.txt | 5 +++ docs/topics/testing/overview.txt | 5 +++ docs/topics/testing/tools.txt | 34 ++++++++++++------- .../migration_test_data_persistence/tests.py | 21 ++++++++++++ ...{should_not_be_loaded.json => person.json} | 0 tests/test_utils/test_transactiontestcase.py | 15 +++++++- tests/test_utils/tests.py | 2 +- 8 files changed, 91 insertions(+), 16 deletions(-) rename tests/test_utils/fixtures/{should_not_be_loaded.json => person.json} (100%) diff --git a/django/test/testcases.py b/django/test/testcases.py index 13ceb4a8ea..36366bd777 100644 --- a/django/test/testcases.py +++ b/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", diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 8f969a4b85..901475a7b4 100644 --- a/docs/releases/5.2.txt +++ b/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 + ` are now available during + ``TransactionTestCase.setUpClass()``. + URLs ~~~~ diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 6caa1ab131..a975ec099d 100644 --- a/docs/topics/testing/overview.txt +++ b/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 --------------------- diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 363505a0fc..7830f5e575 100644 --- a/docs/topics/testing/tools.txt +++ b/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 --------------------- diff --git a/tests/migration_test_data_persistence/tests.py b/tests/migration_test_data_persistence/tests.py index 862a06c4a5..a04259bba1 100644 --- a/tests/migration_test_data_persistence/tests.py +++ b/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 diff --git a/tests/test_utils/fixtures/should_not_be_loaded.json b/tests/test_utils/fixtures/person.json similarity index 100% rename from tests/test_utils/fixtures/should_not_be_loaded.json rename to tests/test_utils/fixtures/person.json diff --git a/tests/test_utils/test_transactiontestcase.py b/tests/test_utils/test_transactiontestcase.py index 0032e2ee0c..12ef4c9a1c 100644 --- a/tests/test_utils/test_transactiontestcase.py +++ b/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) diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 1110466fcf..4fd9267429 100644 --- a/tests/test_utils/tests.py +++ b/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):