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 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, # If the test case has a multi_db=True flag, act on all databases,
# including mirrors or not. Otherwise, just on the default DB. # 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 return [alias for alias in connections
if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR']] if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR']]
else: else:
@ -829,6 +830,9 @@ class TransactionTestCase(SimpleTestCase):
call_command('loaddata', *self.fixtures, call_command('loaddata', *self.fixtures,
**{'verbosity': 0, 'database': db_name}) **{'verbosity': 0, 'database': db_name})
def _should_reload_connections(self):
return True
def _post_teardown(self): def _post_teardown(self):
"""Performs any post-test things. This includes: """Performs any post-test things. This includes:
@ -839,12 +843,13 @@ class TransactionTestCase(SimpleTestCase):
try: try:
self._fixture_teardown() self._fixture_teardown()
super(TransactionTestCase, self)._post_teardown() super(TransactionTestCase, self)._post_teardown()
if self._should_reload_connections():
# Some DB cursors include SQL statements as part of cursor # Some DB cursors include SQL statements as part of cursor
# creation. If you have a test that does rollback, the effect of # creation. If you have a test that does a rollback, the effect
# these statements is lost, which can effect the operation of # of these statements is lost, which can affect the operation of
# tests (e.g., losing a timezone setting causing objects to be # tests (e.g., losing a timezone setting causing objects to be
# created with the wrong time). To make sure this doesn't happen, # created with the wrong time). To make sure this doesn't
# get a clean connection at the start of every test. # happen, get a clean connection at the start of every test.
for conn in connections.all(): for conn in connections.all():
conn.close() conn.close()
finally: finally:
@ -899,15 +904,54 @@ def connections_support_transactions():
class TestCase(TransactionTestCase): class TestCase(TransactionTestCase):
""" """
Does basically the same as TransactionTestCase, but surrounds every test Similar to TransactionTestCase, but uses `transaction.atomic()` to achieve
with a transaction, monkey-patches the real transaction management routines test isolation.
to do nothing, and rollsback the test transaction at the end of the test.
You have to use TransactionTestCase, if you need transaction management In most situation, TestCase should be prefered to TransactionTestCase as
inside a test. 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): def _fixture_setup(self):
if not connections_support_transactions(): 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() return super(TestCase, self)._fixture_setup()
assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances' 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 * The :func:`~django.test.override_settings` decorator can now affect the
master router in :setting:`DATABASE_ROUTERS`. 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 Validators
^^^^^^^^^^ ^^^^^^^^^^
@ -743,6 +747,14 @@ The new package is available `on Github`_ and on PyPI.
.. _on GitHub: https://github.com/django/django-formtools/ .. _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 Miscellaneous
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -691,13 +691,45 @@ additions, including:
* Automatic loading of fixtures. * 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. * Creates a TestClient instance.
* Django-specific assertions for testing for things like redirection and form * Django-specific assertions for testing for things like redirection and form
errors. 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:: .. warning::
If you want to test some specific database transaction behavior, you should 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 # 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 # 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). # 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 """Long primary keys and model names can result in a sequence name
that exceeds the database limits, which will result in truncation that exceeds the database limits, which will result in truncation
on certain databases (e.g., Postgres). The backend needs to use on certain databases (e.g., Postgres). The backend needs to use
the correct sequence name in last_insert_id and other places, so the correct sequence name in last_insert_id and other places, so
check it is. Refs #8901. check it is. Refs #8901.
""" """
available_apps = ['backends']
def test_sequence_name_length_limits_create(self): def test_sequence_name_length_limits_create(self):
"""Test creation of model with long name and long pk name doesn't error. Ref #8901""" """Test creation of model with long name and long pk name doesn't error. Ref #8901"""
@ -465,7 +466,9 @@ class EscapingChecksDebug(EscapingChecks):
pass pass
class BackendTestCase(TestCase): class BackendTestCase(TransactionTestCase):
available_apps = ['backends']
def create_squares_with_executemany(self, args): def create_squares_with_executemany(self, args):
self.create_squares(args, 'format', True) self.create_squares(args, 'format', True)
@ -653,9 +656,8 @@ class BackendTestCase(TestCase):
""" """
Test the documented API of connection.queries. Test the documented API of connection.queries.
""" """
reset_queries()
with connection.cursor() as cursor: with connection.cursor() as cursor:
reset_queries()
cursor.execute("SELECT 1" + connection.features.bare_select_suffix) cursor.execute("SELECT 1" + connection.features.bare_select_suffix)
self.assertEqual(1, len(connection.queries)) self.assertEqual(1, len(connection.queries))
@ -823,7 +825,9 @@ class FkConstraintsTests(TransactionTestCase):
transaction.set_rollback(True) transaction.set_rollback(True)
class ThreadTests(TestCase): class ThreadTests(TransactionTestCase):
available_apps = ['backends']
def test_default_connection_thread_local(self): def test_default_connection_thread_local(self):
""" """
@ -987,9 +991,7 @@ class MySQLPKZeroTests(TestCase):
models.Square.objects.create(id=0, root=0, square=1) models.Square.objects.create(id=0, root=0, square=1)
class DBConstraintTestCase(TransactionTestCase): class DBConstraintTestCase(TestCase):
available_apps = ['backends']
def test_can_reference_existent(self): def test_can_reference_existent(self):
obj = models.Object.objects.create() obj = models.Object.objects.create()
@ -1066,6 +1068,7 @@ class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(DBTestSettingsRenamedTests, cls).setUpClass()
# Silence "UserWarning: Overriding setting DATABASES can lead to # Silence "UserWarning: Overriding setting DATABASES can lead to
# unexpected behavior." # unexpected behavior."
cls.warning_classes.append(UserWarning) 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 django.core import management
from .models import Book from .models import Book
class TestNoInitialDataLoading(TestCase): class TestNoInitialDataLoading(TransactionTestCase):
""" """
Apps with migrations should ignore initial data. This test can be removed 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 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 import connection
from django.db.utils import DatabaseError from django.db.utils import DatabaseError
from django.test import TestCase, skipUnlessDBFeature from django.test import TransactionTestCase, skipUnlessDBFeature
from .models import Reporter, Article from .models import Reporter, Article
class IntrospectionTests(TestCase): class IntrospectionTests(TransactionTestCase):
available_apps = ['introspection']
def test_table_names(self): def test_table_names(self):
tl = connection.introspection.table_names() tl = connection.introspection.table_names()
self.assertEqual(tl, sorted(tl)) self.assertEqual(tl, sorted(tl))

View File

@ -5,7 +5,7 @@ from operator import attrgetter
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.core.management import call_command from django.core.management import call_command
from django.db import connection from django.db import connection
from django.test import TestCase from django.test import TestCase, TransactionTestCase
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.utils import six from django.utils import six
@ -379,7 +379,9 @@ class ModelInheritanceTests(TestCase):
s.titles.all(), []) s.titles.all(), [])
class InheritanceSameModelNameTests(TestCase): class InheritanceSameModelNameTests(TransactionTestCase):
available_apps = ['model_inheritance']
def setUp(self): def setUp(self):
# The Title model has distinct accessors for both # The Title model has distinct accessors for both
@ -402,14 +404,19 @@ class InheritanceSameModelNameTests(TestCase):
INSTALLED_APPS={'append': ['model_inheritance.same_model_name']}): INSTALLED_APPS={'append': ['model_inheritance.same_model_name']}):
call_command('migrate', verbosity=0) call_command('migrate', verbosity=0)
from .same_model_name.models import Copy from .same_model_name.models import Copy
self.assertEqual( copy = self.title.attached_same_model_name_copy_set.create(
self.title.attached_same_model_name_copy_set.create(
content='The Web framework for perfectionists with deadlines.', content='The Web framework for perfectionists with deadlines.',
url='http://www.djangoproject.com/', url='http://www.djangoproject.com/',
title='Django Rocks' title='Django Rocks'
), Copy.objects.get( )
self.assertEqual(
copy,
Copy.objects.get(
content='The Web framework for perfectionists with deadlines.', 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): def test_related_name_attribute_exists(self):
# The Post model doesn't have an attribute called 'attached_%(app_label)s_%(class)s_set'. # The Post model doesn't have an attribute called 'attached_%(app_label)s_%(class)s_set'.