Opened 3 months ago
Last modified 3 months ago
#36429 assigned Bug
IntegrityError on contentype creation when using transactional test with AND without serialized_rollback
Reported by: | Julie Rymer | Owned by: | Clifford Gama |
---|---|---|---|
Component: | Testing framework | Version: | 5.2 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Accepted | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
Related to #23727
When using TransactionTestCase
, if you use rollback emulation with serialized_rollback=True
for some test but not all, you'll get a db error because of a duplicate on ContentType.
django.db.utils.IntegrityError: UNIQUE constraint failed: django_content_type.app_label, django_content_type.model Traceback (most recent call last): File "[...]/site-packages/django/db/backends/base/creation.py", line 163, in deserialize_db_from_string obj.save()
Problem analysis
When using serialized_rollback=True
, on test setup the content of the database that was previously serialised will be applied.
On teardown, the data will be flushed and the post_migrate signal won't be emitted because of inhibit_post_migrate
argument.
When using serialized_rollback=False
no database content is loaded on setup and on teardown the database is flushed and post_migrate signal will be emitted.
ContentType objects get created from a post_migrate handler. They also get serialised from database content.
So, what happens if a test with serialized_rollback=True
get run after a test with serialized_rollback=False
? You get an IntegrityError because the ContentType object are loaded from connection._test_serialized_contents
after they already were created from the post_migrate handler emitted when flushing.
I think this is what is happening, please correct me if my assumptions are wrong.
How to reproduce
Using the "Writing your first Django app" tutorial, you can reproduce by adding the following tests to the polls app:
from django.test import TransactionTestCase class QuestionModel1Tests(TransactionTestCase): serialized_rollback = False def test_was_published_recently_with_future_question(self): self.assertTrue(True) class QuestionModel2Tests(TransactionTestCase): serialized_rollback = True def test_was_published_recently_with_future_question(self): self.assertTrue(True)
This error fully depends on the order of the test execution. If you don't get the error the first time, just add some more tests alternating serialized_rollback
True and False until you get the error. you'll get it as soon as a serialised rollback test execute after a non-serialised one.
Change History (4)
comment:1 by , 3 months ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:2 by , 3 months ago
Also for those affected, I found a workaround I'm using with pytest.
You can use the pytest_collection_modifyitems
hook to change the order of the tests and have all transactional tests with serialized_rollback=False
all run at the end. This way the post_migrate won't affect any test with serialized_rollback=True
.
def pytest_collection_modifyitems(config, items): def transactional_attr_order(item): marker = item.get_closest_marker("django_db") if ( marker and marker.kwargs.get("transaction", False) and not marker.kwargs.get("serialized_rollback", False) ): return 1 return 0 items.sort(key=transactional_attr_order)
comment:3 by , 3 months ago
A similar re-ordering strategy could be employed in this case. Something like
+class _SerializedRollbackTransactionTestCase: + def __instancecheck__(self, instance): + return ( + isinstance(instance, TransactionTestCase) and instance.serialized_rollback + ) + + class DiscoverRunner: """A Django test runner that uses unittest2 test discovery.""" @@ -663,7 +670,7 @@ class DiscoverRunner: parallel_test_suite = ParallelTestSuite test_runner = unittest.TextTestRunner test_loader = unittest.defaultTestLoader - reorder_by = (TestCase, SimpleTestCase) + reorder_by = (TestCase, _SerializedRollbackTransactionTestCase, SimpleTestCase) def __init__( self,
comment:4 by , 3 months ago
Owner: | set to |
---|---|
Status: | new → assigned |
Thank you, replicated. Also see this behavior on 5.1, 5.0, 4.2