From da9fe5c717c179a9e881a40efd3bfe36a21bf4a6 Mon Sep 17 00:00:00 2001 From: Thomas Chaumeny Date: Sat, 18 Oct 2014 23:01:13 +0200 Subject: [PATCH] Fixed #20392 -- Added TestCase.setUpTestData() Each TestCase is also now wrapped in a class-wide transaction. --- django/test/testcases.py | 74 ++++++++++++++++++++++++------- docs/releases/1.8.txt | 12 +++++ docs/topics/testing/tools.txt | 34 +++++++++++++- tests/backends/tests.py | 19 ++++---- tests/fixtures_migration/tests.py | 4 +- tests/introspection/tests.py | 7 ++- tests/model_inheritance/tests.py | 21 ++++++--- 7 files changed, 136 insertions(+), 35 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index db2187f7fd1..78c185ecd83 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -786,10 +786,11 @@ class TransactionTestCase(SimpleTestCase): raise - def _databases_names(self, include_mirrors=True): + @classmethod + def _databases_names(cls, include_mirrors=True): # If the test case has a multi_db=True flag, act on all databases, # including mirrors or not. Otherwise, just on the default DB. - if getattr(self, 'multi_db', False): + if getattr(cls, 'multi_db', False): return [alias for alias in connections if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR']] else: @@ -829,6 +830,9 @@ class TransactionTestCase(SimpleTestCase): call_command('loaddata', *self.fixtures, **{'verbosity': 0, 'database': db_name}) + def _should_reload_connections(self): + return True + def _post_teardown(self): """Performs any post-test things. This includes: @@ -839,14 +843,15 @@ class TransactionTestCase(SimpleTestCase): try: self._fixture_teardown() super(TransactionTestCase, self)._post_teardown() - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does rollback, the effect of - # these statements is lost, which can effect the operation of - # tests (e.g., losing a timezone setting causing objects to be - # created with the wrong time). To make sure this doesn't happen, - # get a clean connection at the start of every test. - for conn in connections.all(): - conn.close() + if self._should_reload_connections(): + # Some DB cursors include SQL statements as part of cursor + # creation. If you have a test that does a rollback, the effect + # of these statements is lost, which can affect the operation of + # tests (e.g., losing a timezone setting causing objects to be + # created with the wrong time). To make sure this doesn't + # happen, get a clean connection at the start of every test. + for conn in connections.all(): + conn.close() finally: if self.available_apps is not None: apps.unset_available_apps() @@ -899,15 +904,54 @@ def connections_support_transactions(): class TestCase(TransactionTestCase): """ - Does basically the same as TransactionTestCase, but surrounds every test - with a transaction, monkey-patches the real transaction management routines - to do nothing, and rollsback the test transaction at the end of the test. - You have to use TransactionTestCase, if you need transaction management - inside a test. + Similar to TransactionTestCase, but uses `transaction.atomic()` to achieve + test isolation. + + In most situation, TestCase should be prefered to TransactionTestCase as + it allows faster execution. However, there are some situations where using + TransactionTestCase might be necessary (e.g. testing some transactional + behavior). + + On database backends with no transaction support, TestCase behaves as + TransactionTestCase. """ + @classmethod + def setUpClass(cls): + super(TestCase, cls).setUpClass() + if not connections_support_transactions(): + return + cls.cls_atomics = {} + for db_name in cls._databases_names(): + cls.cls_atomics[db_name] = transaction.atomic(using=db_name) + cls.cls_atomics[db_name].__enter__() + cls.setUpTestData() + + @classmethod + def tearDownClass(cls): + if connections_support_transactions(): + for db_name in reversed(cls._databases_names()): + transaction.set_rollback(True, using=db_name) + cls.cls_atomics[db_name].__exit__(None, None, None) + for conn in connections.all(): + conn.close() + super(TestCase, cls).tearDownClass() + + @classmethod + def setUpTestData(cls): + """Load initial data for the TestCase""" + pass + + def _should_reload_connections(self): + if connections_support_transactions(): + return False + return super(TestCase, self)._should_reload_connections() + def _fixture_setup(self): if not connections_support_transactions(): + # If the backend does not support transactions, we should reload + # class data before each test + self.setUpTestData() return super(TestCase, self)._fixture_setup() assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances' diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 4afa02dd18a..d95599f6138 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -507,6 +507,10 @@ Tests * The :func:`~django.test.override_settings` decorator can now affect the master router in :setting:`DATABASE_ROUTERS`. +* Added the ability to setup test data at the class level using + :meth:`TestCase.setUpTestData() `. Using + this technique can speed up the tests as compared to using ``setUp()``. + Validators ^^^^^^^^^^ @@ -743,6 +747,14 @@ The new package is available `on Github`_ and on PyPI. .. _on GitHub: https://github.com/django/django-formtools/ +Database connection reloading between tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django previously closed database connections between each test within a +``TestCase``. This is no longer the case as Django now wraps the whole +``TestCase`` within a transaction. If some of your tests relied on the old +behavior, you should have them inherit from ``TransactionTestCase`` instead. + Miscellaneous ~~~~~~~~~~~~~ diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 1d6d09c8815..454da239e0e 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -691,13 +691,45 @@ additions, including: * Automatic loading of fixtures. -* Wraps each test in a transaction. +* Wraps the tests within two nested ``atomic`` blocks: one for the whole class + and one for each test. * Creates a TestClient instance. * Django-specific assertions for testing for things like redirection and form errors. +.. classmethod:: TestCase.setUpTestData() + + .. versionadded:: 1.8 + + The class-level ``atomic`` block described above allows the creation of + initial data at the class level, once for the whole ``TestCase``. This + technique allows for faster tests as compared to using ``setUp()``. + + For example:: + + from django.test import TestCase + + class MyTests(TestCase): + @classmethod + def setUpTestData(cls): + # Set up data for the whole TestCase + cls.foo = Foo.objects.create(bar="Test") + ... + + def test1(self): + # Some test using self.foo + ... + + def test2(self): + # Some other test using self.foo + ... + + Note that if the tests are run on a database with no transaction support + (for instance, MySQL with the MyISAM engine), ``setUpTestData()`` will be + called before each test, negating the speed benefits. + .. warning:: If you want to test some specific database transaction behavior, you should diff --git a/tests/backends/tests.py b/tests/backends/tests.py index d47ca43d1c5..db79cd0325c 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -345,13 +345,14 @@ class ParameterHandlingTest(TestCase): # Unfortunately, the following tests would be a good test to run on all # backends, but it breaks MySQL hard. Until #13711 is fixed, it can't be run # everywhere (although it would be an effective test of #13711). -class LongNameTest(TestCase): +class LongNameTest(TransactionTestCase): """Long primary keys and model names can result in a sequence name that exceeds the database limits, which will result in truncation on certain databases (e.g., Postgres). The backend needs to use the correct sequence name in last_insert_id and other places, so check it is. Refs #8901. """ + available_apps = ['backends'] def test_sequence_name_length_limits_create(self): """Test creation of model with long name and long pk name doesn't error. Ref #8901""" @@ -465,7 +466,9 @@ class EscapingChecksDebug(EscapingChecks): pass -class BackendTestCase(TestCase): +class BackendTestCase(TransactionTestCase): + + available_apps = ['backends'] def create_squares_with_executemany(self, args): self.create_squares(args, 'format', True) @@ -653,9 +656,8 @@ class BackendTestCase(TestCase): """ Test the documented API of connection.queries. """ - reset_queries() - with connection.cursor() as cursor: + reset_queries() cursor.execute("SELECT 1" + connection.features.bare_select_suffix) self.assertEqual(1, len(connection.queries)) @@ -823,7 +825,9 @@ class FkConstraintsTests(TransactionTestCase): transaction.set_rollback(True) -class ThreadTests(TestCase): +class ThreadTests(TransactionTestCase): + + available_apps = ['backends'] def test_default_connection_thread_local(self): """ @@ -987,9 +991,7 @@ class MySQLPKZeroTests(TestCase): models.Square.objects.create(id=0, root=0, square=1) -class DBConstraintTestCase(TransactionTestCase): - - available_apps = ['backends'] +class DBConstraintTestCase(TestCase): def test_can_reference_existent(self): obj = models.Object.objects.create() @@ -1066,6 +1068,7 @@ class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase): @classmethod def setUpClass(cls): + super(DBTestSettingsRenamedTests, cls).setUpClass() # Silence "UserWarning: Overriding setting DATABASES can lead to # unexpected behavior." cls.warning_classes.append(UserWarning) diff --git a/tests/fixtures_migration/tests.py b/tests/fixtures_migration/tests.py index 9a363385e69..1dcc443c850 100644 --- a/tests/fixtures_migration/tests.py +++ b/tests/fixtures_migration/tests.py @@ -1,10 +1,10 @@ -from django.test import TestCase +from django.test import TransactionTestCase from django.core import management from .models import Book -class TestNoInitialDataLoading(TestCase): +class TestNoInitialDataLoading(TransactionTestCase): """ Apps with migrations should ignore initial data. This test can be removed in Django 1.9 when migrations become required and initial data is no longer diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index d63358dafd6..bd5024a556d 100644 --- a/tests/introspection/tests.py +++ b/tests/introspection/tests.py @@ -2,12 +2,15 @@ from __future__ import unicode_literals from django.db import connection from django.db.utils import DatabaseError -from django.test import TestCase, skipUnlessDBFeature +from django.test import TransactionTestCase, skipUnlessDBFeature from .models import Reporter, Article -class IntrospectionTests(TestCase): +class IntrospectionTests(TransactionTestCase): + + available_apps = ['introspection'] + def test_table_names(self): tl = connection.introspection.table_names() self.assertEqual(tl, sorted(tl)) diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 19d7b5650f2..bf762863914 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -5,7 +5,7 @@ from operator import attrgetter from django.core.exceptions import FieldError from django.core.management import call_command from django.db import connection -from django.test import TestCase +from django.test import TestCase, TransactionTestCase from django.test.utils import CaptureQueriesContext from django.utils import six @@ -379,7 +379,9 @@ class ModelInheritanceTests(TestCase): s.titles.all(), []) -class InheritanceSameModelNameTests(TestCase): +class InheritanceSameModelNameTests(TransactionTestCase): + + available_apps = ['model_inheritance'] def setUp(self): # The Title model has distinct accessors for both @@ -402,14 +404,19 @@ class InheritanceSameModelNameTests(TestCase): INSTALLED_APPS={'append': ['model_inheritance.same_model_name']}): call_command('migrate', verbosity=0) from .same_model_name.models import Copy + copy = self.title.attached_same_model_name_copy_set.create( + content='The Web framework for perfectionists with deadlines.', + url='http://www.djangoproject.com/', + title='Django Rocks' + ) self.assertEqual( - self.title.attached_same_model_name_copy_set.create( - content='The Web framework for perfectionists with deadlines.', - url='http://www.djangoproject.com/', - title='Django Rocks' - ), Copy.objects.get( + copy, + Copy.objects.get( content='The Web framework for perfectionists with deadlines.', )) + # We delete the copy manually so that it doesn't block the flush + # command under Oracle (which does not cascade deletions). + copy.delete() def test_related_name_attribute_exists(self): # The Post model doesn't have an attribute called 'attached_%(app_label)s_%(class)s_set'.