diff --git a/django/test/runner.py b/django/test/runner.py index ed969cc42f..180b6911fb 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -391,6 +391,9 @@ class ParallelTestSuite(unittest.TestSuite): return result + def __iter__(self): + return iter(self.subsuites) + class DiscoverRunner: """A Django test runner that uses unittest2 test discovery.""" @@ -587,6 +590,27 @@ class DiscoverRunner: def suite_result(self, suite, result, **kwargs): return len(result.failures) + len(result.errors) + def _get_databases(self, suite): + databases = set() + for test in suite: + if isinstance(test, unittest.TestCase): + test_databases = getattr(test, 'databases', None) + if test_databases == '__all__': + return set(connections) + if test_databases: + databases.update(test_databases) + else: + databases.update(self._get_databases(test)) + return databases + + def get_databases(self, suite): + databases = self._get_databases(suite) + if self.verbosity >= 2: + unused_databases = [alias for alias in connections if alias not in databases] + if unused_databases: + print('Skipping setup of unused database(s): %s.' % ', '.join(sorted(unused_databases))) + return databases + def run_tests(self, test_labels, extra_tests=None, **kwargs): """ Run the unit tests for all the test labels in the provided list. @@ -601,7 +625,8 @@ class DiscoverRunner: """ self.setup_test_environment() suite = self.build_suite(test_labels, extra_tests) - old_config = self.setup_databases() + databases = self.get_databases(suite) + old_config = self.setup_databases(aliases=databases) run_failed = False try: self.run_checks() diff --git a/django/test/utils.py b/django/test/utils.py index c8965cdbb8..057969503d 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -152,9 +152,9 @@ def teardown_test_environment(): del mail.outbox -def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, **kwargs): +def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs): """Create the test databases.""" - test_databases, mirrored_aliases = get_unique_databases_and_mirrors() + test_databases, mirrored_aliases = get_unique_databases_and_mirrors(aliases) old_names = [] @@ -238,7 +238,7 @@ def dependency_ordered(test_databases, dependencies): return ordered_test_databases -def get_unique_databases_and_mirrors(): +def get_unique_databases_and_mirrors(aliases=None): """ Figure out which databases actually need to be created. @@ -250,6 +250,8 @@ def get_unique_databases_and_mirrors(): where all aliases share the same underlying database. - mirrored_aliases: mapping of mirror aliases to original aliases. """ + if aliases is None: + aliases = connections mirrored_aliases = {} test_databases = {} dependencies = {} @@ -262,7 +264,7 @@ def get_unique_databases_and_mirrors(): if test_settings['MIRROR']: # If the database is marked as a test mirror, save the alias. mirrored_aliases[alias] = test_settings['MIRROR'] - else: + elif alias in aliases: # Store a tuple with DB parameters that uniquely identify it. # If we have two aliases with the same values for that tuple, # we only need to create the test database once. diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 5c488570aa..4a529f30da 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -293,6 +293,9 @@ Tests for older versions of SQLite because they would require expensive table introspection there. +* :class:`~django.test.runner.DiscoverRunner` now skips the setup of databases + not :ref:`referenced by tests`. + URLs ~~~~ diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index a3112fb367..0a228fc4ef 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -614,7 +614,7 @@ utility methods in the ``django.test.utils`` module. Performs global post-test teardown, such as removing instrumentation from the template system and restoring normal email services. -.. function:: setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, **kwargs) +.. function:: setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs) Creates the test databases. @@ -622,6 +622,14 @@ utility methods in the ``django.test.utils`` module. that have been made. This data will be provided to the :func:`teardown_databases` function at the conclusion of testing. + The ``aliases`` argument determines which :setting:`DATABASES` aliases test + databases should be setup for. If it's not provided, it defaults to all of + :setting:`DATABASES` aliases. + + .. versionadded:: 2.2 + + The ``aliases`` argument was added. + .. function:: teardown_databases(old_config, parallel=0, keepdb=False) Destroys the test databases, restoring pre-test conditions. diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 12f20c0144..e656f89124 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -1134,13 +1134,14 @@ Multi-database support .. versionadded:: 2.2 Django sets up a test database corresponding to every database that is -defined in the :setting:`DATABASES` definition in your settings -file. However, a big part of the time taken to run a Django TestCase -is consumed by the call to ``flush`` that ensures that you have a -clean database at the start of each test run. If you have multiple -databases, multiple flushes are required (one for each database), -which can be a time consuming activity -- especially if your tests -don't need to test multi-database activity. +defined in the :setting:`DATABASES` definition in your settings and referred to +by at least one test through ``databases``. + +However, a big part of the time taken to run a Django ``TestCase`` is consumed +by the call to ``flush`` that ensures that you have a clean database at the +start of each test run. If you have multiple databases, multiple flushes are +required (one for each database), which can be a time consuming activity -- +especially if your tests don't need to test multi-database activity. As an optimization, Django only flushes the ``default`` database at the start of each test run. If your setup contains multiple databases, diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index d7569fc111..caa48a852d 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -3,6 +3,7 @@ from argparse import ArgumentParser from contextlib import contextmanager from unittest import TestSuite, TextTestRunner, defaultTestLoader +from django.db import connections from django.test import SimpleTestCase from django.test.runner import DiscoverRunner from django.test.utils import captured_stdout @@ -223,3 +224,47 @@ class DiscoverRunnerTests(SimpleTestCase): with captured_stdout() as stdout: runner.build_suite(['test_runner_apps.tagged.tests']) self.assertIn('Excluding test tag(s): bar, foo.\n', stdout.getvalue()) + + +class DiscoverRunnerGetDatabasesTests(SimpleTestCase): + runner = DiscoverRunner(verbosity=2) + skip_msg = 'Skipping setup of unused database(s): ' + + def get_databases(self, test_labels): + suite = self.runner.build_suite(test_labels) + with captured_stdout() as stdout: + databases = self.runner.get_databases(suite) + return databases, stdout.getvalue() + + def test_mixed(self): + databases, output = self.get_databases(['test_runner_apps.databases.tests']) + self.assertEqual(databases, set(connections)) + self.assertNotIn(self.skip_msg, output) + + def test_all(self): + databases, output = self.get_databases(['test_runner_apps.databases.tests.AllDatabasesTests']) + self.assertEqual(databases, set(connections)) + self.assertNotIn(self.skip_msg, output) + + def test_default_and_other(self): + databases, output = self.get_databases([ + 'test_runner_apps.databases.tests.DefaultDatabaseTests', + 'test_runner_apps.databases.tests.OtherDatabaseTests', + ]) + self.assertEqual(databases, set(connections)) + self.assertNotIn(self.skip_msg, output) + + def test_default_only(self): + databases, output = self.get_databases(['test_runner_apps.databases.tests.DefaultDatabaseTests']) + self.assertEqual(databases, {'default'}) + self.assertIn(self.skip_msg + 'other', output) + + def test_other_only(self): + databases, output = self.get_databases(['test_runner_apps.databases.tests.OtherDatabaseTests']) + self.assertEqual(databases, {'other'}) + self.assertIn(self.skip_msg + 'default', output) + + def test_no_databases_required(self): + databases, output = self.get_databases(['test_runner_apps.databases.tests.NoDatabaseTests']) + self.assertEqual(databases, set()) + self.assertIn(self.skip_msg + 'default, other', output) diff --git a/tests/test_runner_apps/databases/__init__.py b/tests/test_runner_apps/databases/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_runner_apps/databases/tests.py b/tests/test_runner_apps/databases/tests.py new file mode 100644 index 0000000000..4be260e689 --- /dev/null +++ b/tests/test_runner_apps/databases/tests.py @@ -0,0 +1,18 @@ +import unittest + + +class NoDatabaseTests(unittest.TestCase): + def test_nothing(self): + pass + + +class DefaultDatabaseTests(NoDatabaseTests): + databases = {'default'} + + +class OtherDatabaseTests(NoDatabaseTests): + databases = {'other'} + + +class AllDatabasesTests(NoDatabaseTests): + databases = '__all__'