Commit 344f16e2 authored by Karen Tracey's avatar Karen Tracey
Browse files

Fixed #8138 -- Changed django.test.TestCase to rollback tests (when the...

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
parent f9f9d703
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -334,6 +334,7 @@ answer newbie questions, and generally made Django that much better:
    Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
    Brian Ray <http://brianray.chipy.org/>
    remco@diji.biz
    Marc Remolt <m.remolt@webmasters.de>
    David Reynolds <david@reynoldsfamily.org.uk>
    rhettg@gmail.com
    ricardojbarrios@gmail.com
+15 −2
Original line number Diff line number Diff line
@@ -311,6 +311,7 @@ class BaseDatabaseCreation(object):

        self.connection.close()
        settings.DATABASE_NAME = test_database_name
        settings.DATABASE_SUPPORTS_TRANSACTIONS = self._rollback_works()
        
        call_command('syncdb', verbosity=verbosity, interactive=False)

@@ -363,6 +364,18 @@ class BaseDatabaseCreation(object):

        return test_database_name
    
    def _rollback_works(self):
        cursor = self.connection.cursor()
        cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)')
        self.connection._commit()
        cursor.execute('INSERT INTO ROLLBACK_TEST (X) VALUES (8)')
        self.connection._rollback()
        cursor.execute('SELECT COUNT(X) FROM ROLLBACK_TEST')
        count, = cursor.fetchone()
        cursor.execute('DROP TABLE ROLLBACK_TEST')
        self.connection._commit()
        return count == 0
        
    def destroy_test_db(self, old_database_name, verbosity=1):
        """
        Destroy a test database, prompting the user for confirmation if the
+1 −1
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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

+40 −1
Original line number Diff line number Diff line
@@ -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)
Loading