From fcd08c175787e909b3eb98f756317a07741c48dd Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 12 Feb 2016 16:41:31 -0800 Subject: [PATCH] Fixed #11665 -- Made TestCase check deferrable constraints after each test. --- django/test/testcases.py | 13 ++++++++++++- docs/releases/1.10.txt | 6 +++++- docs/topics/testing/tools.txt | 7 +++++++ tests/auth_tests/test_views.py | 6 ++++++ tests/test_utils/test_testcase.py | 20 ++++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/test_utils/test_testcase.py diff --git a/django/test/testcases.py b/django/test/testcases.py index 34c94b82cc4..9de29fff683 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1044,7 +1044,18 @@ class TestCase(TransactionTestCase): def _fixture_teardown(self): if not connections_support_transactions(): return super(TestCase, self)._fixture_teardown() - self._rollback_atomics(self.atomics) + try: + for db_name in reversed(self._databases_names()): + if self._should_check_constraints(connections[db_name]): + connections[db_name].check_constraints() + finally: + self._rollback_atomics(self.atomics) + + def _should_check_constraints(self, connection): + return ( + connection.features.can_defer_constraint_checks and + not connection.needs_rollback and connection.is_usable() + ) class CheckCondition(object): diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index db4f7d4632d..b3bd4b8afd8 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -333,7 +333,8 @@ Templates Tests ~~~~~ -* ... +* To better catch bugs, :class:`~django.test.TestCase` now checks deferrable + database constraints at the end of each test. URLs ~~~~ @@ -541,6 +542,9 @@ Miscellaneous aggregate function now returns a ``float`` instead of ``decimal.Decimal``. (It's still wrapped in a measure of square meters.) +* Tests that violate deferrable database constraints will now error when run on + a database that supports deferrable constraints. + .. _deprecated-features-1.10: Features deprecated in 1.10 diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 5b8908c4855..2bb53454cf3 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -761,11 +761,18 @@ additions, including: * Wraps the tests within two nested ``atomic`` blocks: one for the whole class and one for each test. +* Checks deferrable database constraints at the end of each test. + * Creates a TestClient instance. * Django-specific assertions for testing for things like redirection and form errors. +.. versionchanged:: 1.10 + + The check for deferrable database constraints at the end of each test was + added. + .. classmethod:: TestCase.setUpTestData() The class-level ``atomic`` block described above allows the creation of diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index 7fd493ca34a..d8325d8b93f 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -991,3 +991,9 @@ class UUIDUserTests(TestCase): self.assertEqual(row.user_id, 1) # hardcoded in CustomUserAdmin.log_change() self.assertEqual(row.object_id, str(u.pk)) self.assertEqual(row.get_change_message(), 'Changed password.') + + # The LogEntry.user column isn't altered to a UUID type so it's set to + # an integer manually in CustomUserAdmin to avoid an error. To avoid a + # constraint error, delete the entry before constraints are checked + # after the test. + row.delete() diff --git a/tests/test_utils/test_testcase.py b/tests/test_utils/test_testcase.py new file mode 100644 index 00000000000..8a367391cbf --- /dev/null +++ b/tests/test_utils/test_testcase.py @@ -0,0 +1,20 @@ +from django.db import IntegrityError, transaction +from django.test import TestCase, skipUnlessDBFeature + +from .models import PossessedCar + + +class TestTestCase(TestCase): + + @skipUnlessDBFeature('can_defer_constraint_checks') + @skipUnlessDBFeature('supports_foreign_keys') + def test_fixture_teardown_checks_constraints(self): + rollback_atomics = self._rollback_atomics + self._rollback_atomics = lambda connection: None # noop + try: + car = PossessedCar.objects.create(car_id=1, belongs_to_id=1) + with self.assertRaises(IntegrityError), transaction.atomic(): + self._fixture_teardown() + car.delete() + finally: + self._rollback_atomics = rollback_atomics