Просмотр исходного кода

Fixed #20392 -- Added TestCase.setUpTestData()

Each TestCase is also now wrapped in a class-wide transaction.
Thomas Chaumeny 10 лет назад
Родитель
Сommit
da9fe5c717

+ 59 - 15
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'

+ 12 - 0
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() <django.test.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
 ~~~~~~~~~~~~~
 

+ 33 - 1
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

+ 11 - 8
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)

+ 2 - 2
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

+ 5 - 2
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))

+ 14 - 7
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'.