summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Chaumeny <t.chaumeny@gmail.com>2014-10-18 23:01:13 +0200
committerTim Graham <timograham@gmail.com>2014-12-03 10:37:04 -0500
commitda9fe5c717c179a9e881a40efd3bfe36a21bf4a6 (patch)
treebdf38ea36841d93fa9260daf5b8d705b377dbd40
parentdee4d23f7e703aec2d1244e4facbf7f4c88deed5 (diff)
Fixed #20392 -- Added TestCase.setUpTestData()
Each TestCase is also now wrapped in a class-wide transaction.
-rw-r--r--django/test/testcases.py74
-rw-r--r--docs/releases/1.8.txt12
-rw-r--r--docs/topics/testing/tools.txt34
-rw-r--r--tests/backends/tests.py19
-rw-r--r--tests/fixtures_migration/tests.py4
-rw-r--r--tests/introspection/tests.py7
-rw-r--r--tests/model_inheritance/tests.py21
7 files changed, 136 insertions, 35 deletions
diff --git a/django/test/testcases.py b/django/test/testcases.py
index db2187f7fd..78c185ecd8 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 4afa02dd18..d95599f613 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() <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
~~~~~~~~~~~~~
diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt
index 1d6d09c881..454da239e0 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 d47ca43d1c..db79cd0325 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 9a363385e6..1dcc443c85 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 d63358dafd..bd5024a556 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 19d7b5650f..bf76286391 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'.