diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index 82be18cb65..8c9a27b8c5 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -82,8 +82,10 @@ class PostGISCreation(DatabaseCreation): self.connection.ops.quote_name(self.template_postgis),) return '' - def _create_test_db(self, verbosity, autoclobber): - test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber) + def _create_test_db(self, verbosity, autoclobber, keepdb=False): + test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber, keepdb) + if keepdb: + return test_database_name if self.template_postgis is None: # Connect to the test database in order to create the postgis extension self.connection.close() diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py index 8cadef9f9c..205e601ba3 100644 --- a/django/contrib/gis/db/backends/spatialite/creation.py +++ b/django/contrib/gis/db/backends/spatialite/creation.py @@ -7,7 +7,7 @@ from django.db.backends.sqlite3.creation import DatabaseCreation class SpatiaLiteCreation(DatabaseCreation): - def create_test_db(self, verbosity=1, autoclobber=False): + def create_test_db(self, verbosity=1, autoclobber=False, keepdb=False): """ Creates a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. @@ -22,11 +22,15 @@ class SpatiaLiteCreation(DatabaseCreation): if verbosity >= 1: test_db_repr = '' + action = 'Creating' if verbosity >= 2: test_db_repr = " ('%s')" % test_database_name - print("Creating test database for alias '%s'%s..." % (self.connection.alias, test_db_repr)) + if keepdb: + action = 'Using existing' + print("%s test database for alias '%s'%s..." % ( + action, self.connection.alias, test_db_repr)) - self._create_test_db(verbosity, autoclobber) + self._create_test_db(verbosity, autoclobber, keepdb) self.connection.close() self.connection.settings_dict["NAME"] = test_database_name diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index b2b82c13c2..72fea241dc 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -332,7 +332,7 @@ class BaseDatabaseCreation(object): ";", ] - def create_test_db(self, verbosity=1, autoclobber=False): + def create_test_db(self, verbosity=1, autoclobber=False, keepdb=False): """ Creates a test database, prompting the user for confirmation if the database already exists. Returns the name of the test database created. @@ -344,12 +344,21 @@ class BaseDatabaseCreation(object): if verbosity >= 1: test_db_repr = '' + action = 'Creating' if verbosity >= 2: test_db_repr = " ('%s')" % test_database_name - print("Creating test database for alias '%s'%s..." % ( - self.connection.alias, test_db_repr)) + if keepdb: + action = "Using existing" - self._create_test_db(verbosity, autoclobber) + print("%s test database for alias '%s'%s..." % ( + action, self.connection.alias, test_db_repr)) + + # We could skip this call if keepdb is True, but we instead + # give it the keepdb param. This is to handle the case + # where the test DB doesn't exist, in which case we need to + # create it, then just not destroy it. If we instead skip + # this, we will get an exception. + self._create_test_db(verbosity, autoclobber, keepdb) self.connection.close() settings.DATABASES[self.connection.alias]["NAME"] = test_database_name @@ -393,7 +402,7 @@ class BaseDatabaseCreation(object): return self.connection.settings_dict['TEST']['NAME'] return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME'] - def _create_test_db(self, verbosity, autoclobber): + def _create_test_db(self, verbosity, autoclobber, keepdb=False): """ Internal implementation - creates the test db tables. """ @@ -409,6 +418,11 @@ class BaseDatabaseCreation(object): cursor.execute( "CREATE DATABASE %s %s" % (qn(test_database_name), suffix)) except Exception as e: + # if we want to keep the db, then no need to do any of the below, + # just return and skip it all. + if keepdb: + return test_database_name + sys.stderr.write( "Got an error creating the test database: %s\n" % e) if not autoclobber: diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 90cc83fd58..d93aac4d77 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -56,7 +56,7 @@ class DatabaseCreation(BaseDatabaseCreation): def __init__(self, connection): super(DatabaseCreation, self).__init__(connection) - def _create_test_db(self, verbosity=1, autoclobber=False): + def _create_test_db(self, verbosity=1, autoclobber=False, keepdb=False): TEST_NAME = self._test_database_name() TEST_USER = self._test_database_user() TEST_PASSWD = self._test_database_passwd() @@ -76,6 +76,10 @@ class DatabaseCreation(BaseDatabaseCreation): try: self._execute_test_db_creation(cursor, parameters, verbosity) except Exception as e: + # if we want to keep the db, then no need to do any of the below, + # just return and skip it all. + if keepdb: + return sys.stderr.write("Got an error creating the test database: %s\n" % e) if not autoclobber: confirm = input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_NAME) diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index abcd068952..2519add07c 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -52,8 +52,10 @@ class DatabaseCreation(BaseDatabaseCreation): return test_database_name return ':memory:' - def _create_test_db(self, verbosity, autoclobber): + def _create_test_db(self, verbosity, autoclobber, keepdb=False): test_database_name = self._get_test_db_name() + if keepdb: + return test_database_name if test_database_name != ':memory:': # Erase the old test database if verbosity >= 1: diff --git a/django/test/runner.py b/django/test/runner.py index 5eafe35416..68671e6525 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -26,10 +26,13 @@ class DiscoverRunner(object): make_option('-p', '--pattern', action='store', dest='pattern', default="test*.py", help='The test matching pattern. Defaults to test*.py.'), + make_option('-k', '--keepdb', action='store_true', dest='keepdb', + default=False, + help='Preserve the test DB between runs. Defaults to False'), ) def __init__(self, pattern=None, top_level=None, - verbosity=1, interactive=True, failfast=False, + verbosity=1, interactive=True, failfast=False, keepdb=False, **kwargs): self.pattern = pattern @@ -38,6 +41,7 @@ class DiscoverRunner(object): self.verbosity = verbosity self.interactive = interactive self.failfast = failfast + self.keepdb = keepdb def setup_test_environment(self, **kwargs): setup_test_environment() @@ -106,7 +110,7 @@ class DiscoverRunner(object): return reorder_suite(suite, self.reorder_by) def setup_databases(self, **kwargs): - return setup_databases(self.verbosity, self.interactive, **kwargs) + return setup_databases(self.verbosity, self.interactive, self.keepdb, **kwargs) def run_suite(self, suite, **kwargs): return self.test_runner( @@ -120,7 +124,7 @@ class DiscoverRunner(object): """ old_names, mirrors = old_config for connection, old_name, destroy in old_names: - if destroy: + if destroy and not self.keepdb: connection.creation.destroy_test_db(old_name, self.verbosity) def teardown_test_environment(self, **kwargs): @@ -250,7 +254,7 @@ def partition_suite(suite, classes, bins): bins[-1].addTest(test) -def setup_databases(verbosity, interactive, **kwargs): +def setup_databases(verbosity, interactive, keepdb=False, **kwargs): from django.db import connections, DEFAULT_DB_ALIAS # First pass -- work out which databases actually need to be created, @@ -294,7 +298,7 @@ def setup_databases(verbosity, interactive, **kwargs): connection = connections[alias] if test_db_name is None: test_db_name = connection.creation.create_test_db( - verbosity, autoclobber=not interactive) + verbosity, autoclobber=not interactive, keepdb=keepdb) destroy = True else: connection.settings_dict['NAME'] = test_db_name diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 6bb67f01fc..9291479305 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1310,6 +1310,17 @@ The ``--liveserver`` option can be used to override the default address where the live server (used with :class:`~django.test.LiveServerTestCase`) is expected to run from. The default value is ``localhost:8081``. +.. django-admin-option:: --keepdb + +.. versionadded:: 1.8 + +The ``--keepdb`` option can be used to preserve the test database between test +runs. This has the advantage of skipping both the create and destroy actions +which greatly decreases the time to run tests, especially those in a large +test suite. If the test database does not exist, it will be created on the first +run and then preserved for each subsequent run. Any unapplied migrations will also +be applied to the test database before running the test suite. + testserver -------------------------------- diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index e0ad845a39..056ea86baf 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -193,6 +193,9 @@ Tests * The new :meth:`~django.test.SimpleTestCase.assertJSONNotEqual` assertion allows you to test that two JSON fragments are not equal. +* Added the ability to preserve the test database by adding the :djadminopt:`--keepdb` + flag. + Validators ^^^^^^^^^^ diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 10dbc70f39..2d9808f0bc 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -149,6 +149,14 @@ Tests that require a database (namely, model tests) will not use your "real" Regardless of whether the tests pass or fail, the test databases are destroyed when all the tests have been executed. +.. versionadded:: 1.8 + + You can prevent the test databases from being destroyed by adding the + :djadminopt:`--keepdb` flag to the test command. This will preserve the test + database between runs. If the database does not exist, it will first + be created. Any migrations will also be applied in order to keep it + up to date. + By default the test databases get their names by prepending ``test_`` to the value of the :setting:`NAME` settings for the databases defined in :setting:`DATABASES`. When using the SQLite database engine diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 8ec244cf8d..6de53b8283 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -310,7 +310,7 @@ class AliasedDatabaseTeardownTest(unittest.TestCase): try: destroyed_names = [] DatabaseCreation.destroy_test_db = lambda self, old_database_name, verbosity=1: destroyed_names.append(old_database_name) - DatabaseCreation.create_test_db = lambda self, verbosity=1, autoclobber=False: self._get_test_db_name() + DatabaseCreation.create_test_db = lambda self, verbosity=1, autoclobber=False, keepdb=False: self._get_test_db_name() db.connections = db.ConnectionHandler({ 'default': {