Fixed #20392 -- Added TestCase.setUpTestData()

Each TestCase is also now wrapped in a class-wide transaction.
This commit is contained in:
Thomas Chaumeny 2014-10-18 23:01:13 +02:00 committed by Tim Graham
parent dee4d23f7e
commit da9fe5c717
7 changed files with 136 additions and 35 deletions

View File

@ -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'

View File

@ -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
~~~~~~~~~~~~~

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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'.