mirror of
https://github.com/django/django.git
synced 2025-10-24 06:06:09 +00:00
Fixed #8138 -- Changed django.test.TestCase to rollback tests (when the database supports it) instead of flushing and reloading the database. This can substantially reduce the time it takes to run large test suites.
This change may be slightly backwards incompatible, if existing tests need to test transactional behavior, or if they rely on invalid assumptions or a specific test case ordering. For the first case, django.test.TransactionTestCase should be used. TransactionTestCase is also a quick fix to get around test case errors revealed by the new rollback approach, but a better long-term fix is to correct the test case. See the testing doc for full details. Many thanks to: * Marc Remolt for the initial proposal and implementation. * Luke Plant for initial testing and improving the implementation. * Ramiro Morales for feedback and help with tracking down a mysterious PostgreSQL issue. * Eric Holscher for feedback regarding the effect of the change on the Ellington testsuite. * Russell Keith-Magee for guidance and feedback from beginning to end. git-svn-id: http://code.djangoproject.com/svn/django/trunk@9756 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
@@ -3,4 +3,4 @@ Django Unit Test and Doctest framework.
|
||||
"""
|
||||
|
||||
from django.test.client import Client
|
||||
from django.test.testcases import TestCase
|
||||
from django.test.testcases import TestCase, TransactionTestCase
|
||||
|
||||
@@ -19,6 +19,7 @@ from django.utils.functional import curry
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.itercompat import is_iterable
|
||||
from django.db import transaction, close_connection
|
||||
|
||||
BOUNDARY = 'BoUnDaRyStRiNg'
|
||||
MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
@@ -69,7 +70,9 @@ class ClientHandler(BaseHandler):
|
||||
response = middleware_method(request, response)
|
||||
response = self.apply_response_fixes(request, response)
|
||||
finally:
|
||||
signals.request_finished.disconnect(close_connection)
|
||||
signals.request_finished.send(sender=self.__class__)
|
||||
signals.request_finished.connect(close_connection)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.conf import settings
|
||||
from django.db.models import get_app, get_apps
|
||||
from django.test import _doctest as doctest
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
from django.test.testcases import OutputChecker, DocTestRunner
|
||||
from django.test.testcases import OutputChecker, DocTestRunner, TestCase
|
||||
|
||||
# The module name for tests outside models.py
|
||||
TEST_MODULE = 'tests'
|
||||
@@ -99,6 +99,43 @@ def build_test(label):
|
||||
else: # label is app.TestClass.test_method
|
||||
return TestClass(parts[2])
|
||||
|
||||
def partition_suite(suite, classes, bins):
|
||||
"""
|
||||
Partitions a test suite by test type.
|
||||
|
||||
classes is a sequence of types
|
||||
bins is a sequence of TestSuites, one more than classes
|
||||
|
||||
Tests of type classes[i] are added to bins[i],
|
||||
tests with no match found in classes are place in bins[-1]
|
||||
"""
|
||||
for test in suite:
|
||||
if isinstance(test, unittest.TestSuite):
|
||||
partition_suite(test, classes, bins)
|
||||
else:
|
||||
for i in range(len(classes)):
|
||||
if isinstance(test, classes[i]):
|
||||
bins[i].addTest(test)
|
||||
break
|
||||
else:
|
||||
bins[-1].addTest(test)
|
||||
|
||||
def reorder_suite(suite, classes):
|
||||
"""
|
||||
Reorders a test suite by test type.
|
||||
|
||||
classes is a sequence of types
|
||||
|
||||
All tests of type clases[0] are placed first, then tests of type classes[1], etc.
|
||||
Tests with no match in classes are placed last.
|
||||
"""
|
||||
class_count = len(classes)
|
||||
bins = [unittest.TestSuite() for i in range(class_count+1)]
|
||||
partition_suite(suite, classes, bins)
|
||||
for i in range(class_count):
|
||||
bins[0].addTests(bins[i+1])
|
||||
return bins[0]
|
||||
|
||||
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
|
||||
"""
|
||||
Run the unit tests for all the test labels in the provided list.
|
||||
@@ -137,6 +174,8 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
|
||||
for test in extra_tests:
|
||||
suite.addTest(test)
|
||||
|
||||
suite = reorder_suite(suite, (TestCase,))
|
||||
|
||||
old_name = settings.DATABASE_NAME
|
||||
from django.db import connection
|
||||
connection.creation.create_test_db(verbosity, autoclobber=not interactive)
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import clear_url_caches
|
||||
from django.db import transaction
|
||||
from django.db import transaction, connection
|
||||
from django.http import QueryDict
|
||||
from django.test import _doctest as doctest
|
||||
from django.test.client import Client
|
||||
@@ -27,6 +27,31 @@ def to_list(value):
|
||||
value = [value]
|
||||
return value
|
||||
|
||||
real_commit = transaction.commit
|
||||
real_rollback = transaction.rollback
|
||||
real_enter_transaction_management = transaction.enter_transaction_management
|
||||
real_leave_transaction_management = transaction.leave_transaction_management
|
||||
real_savepoint_commit = transaction.savepoint_commit
|
||||
real_savepoint_rollback = transaction.savepoint_rollback
|
||||
|
||||
def nop(x=None):
|
||||
return
|
||||
|
||||
def disable_transaction_methods():
|
||||
transaction.commit = nop
|
||||
transaction.rollback = nop
|
||||
transaction.savepoint_commit = nop
|
||||
transaction.savepoint_rollback = nop
|
||||
transaction.enter_transaction_management = nop
|
||||
transaction.leave_transaction_management = nop
|
||||
|
||||
def restore_transaction_methods():
|
||||
transaction.commit = real_commit
|
||||
transaction.rollback = real_rollback
|
||||
transaction.savepoint_commit = real_savepoint_commit
|
||||
transaction.savepoint_rollback = real_savepoint_rollback
|
||||
transaction.enter_transaction_management = real_enter_transaction_management
|
||||
transaction.leave_transaction_management = real_leave_transaction_management
|
||||
|
||||
class OutputChecker(doctest.OutputChecker):
|
||||
def check_output(self, want, got, optionflags):
|
||||
@@ -173,8 +198,8 @@ class DocTestRunner(doctest.DocTestRunner):
|
||||
# Rollback, in case of database errors. Otherwise they'd have
|
||||
# side effects on other tests.
|
||||
transaction.rollback_unless_managed()
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
|
||||
class TransactionTestCase(unittest.TestCase):
|
||||
def _pre_setup(self):
|
||||
"""Performs any pre-test setup. This includes:
|
||||
|
||||
@@ -185,16 +210,22 @@ class TestCase(unittest.TestCase):
|
||||
ROOT_URLCONF with it.
|
||||
* Clearing the mail test outbox.
|
||||
"""
|
||||
self._fixture_setup()
|
||||
self._urlconf_setup()
|
||||
mail.outbox = []
|
||||
|
||||
def _fixture_setup(self):
|
||||
call_command('flush', verbosity=0, interactive=False)
|
||||
if hasattr(self, 'fixtures'):
|
||||
# We have to use this slightly awkward syntax due to the fact
|
||||
# that we're using *args and **kwargs together.
|
||||
call_command('loaddata', *self.fixtures, **{'verbosity': 0})
|
||||
|
||||
def _urlconf_setup(self):
|
||||
if hasattr(self, 'urls'):
|
||||
self._old_root_urlconf = settings.ROOT_URLCONF
|
||||
settings.ROOT_URLCONF = self.urls
|
||||
clear_url_caches()
|
||||
mail.outbox = []
|
||||
|
||||
def __call__(self, result=None):
|
||||
"""
|
||||
@@ -211,7 +242,7 @@ class TestCase(unittest.TestCase):
|
||||
import sys
|
||||
result.addError(self, sys.exc_info())
|
||||
return
|
||||
super(TestCase, self).__call__(result)
|
||||
super(TransactionTestCase, self).__call__(result)
|
||||
try:
|
||||
self._post_teardown()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
@@ -226,6 +257,13 @@ class TestCase(unittest.TestCase):
|
||||
|
||||
* Putting back the original ROOT_URLCONF if it was changed.
|
||||
"""
|
||||
self._fixture_teardown()
|
||||
self._urlconf_teardown()
|
||||
|
||||
def _fixture_teardown(self):
|
||||
pass
|
||||
|
||||
def _urlconf_teardown(self):
|
||||
if hasattr(self, '_old_root_urlconf'):
|
||||
settings.ROOT_URLCONF = self._old_root_urlconf
|
||||
clear_url_caches()
|
||||
@@ -359,3 +397,37 @@ class TestCase(unittest.TestCase):
|
||||
self.failIf(template_name in template_names,
|
||||
(u"Template '%s' was used unexpectedly in rendering the"
|
||||
u" response") % template_name)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def _fixture_setup(self):
|
||||
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
||||
return super(TestCase, self)._fixture_setup()
|
||||
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
disable_transaction_methods()
|
||||
|
||||
from django.contrib.sites.models import Site
|
||||
Site.objects.clear_cache()
|
||||
|
||||
if hasattr(self, 'fixtures'):
|
||||
call_command('loaddata', *self.fixtures, **{
|
||||
'verbosity': 0,
|
||||
'commit': False
|
||||
})
|
||||
|
||||
def _fixture_teardown(self):
|
||||
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
||||
return super(TestCase, self)._fixture_teardown()
|
||||
|
||||
restore_transaction_methods()
|
||||
transaction.rollback()
|
||||
transaction.leave_transaction_management()
|
||||
connection.close()
|
||||
Reference in New Issue
Block a user