diff --git a/django/test/testcases.py b/django/test/testcases.py index 36986185ce..f820684c87 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -4,9 +4,11 @@ import posixpath import sys import threading import unittest +import warnings from collections import Counter from contextlib import contextmanager from copy import copy +from difflib import get_close_matches from functools import wraps from unittest.util import safe_repr from urllib.parse import ( @@ -17,7 +19,7 @@ from urllib.request import url2pathname from django.apps import apps from django.conf import settings from django.core import mail -from django.core.exceptions import ValidationError +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.files import locks from django.core.handlers.wsgi import WSGIHandler, get_path_info from django.core.management import call_command @@ -36,6 +38,7 @@ from django.test.utils import ( override_settings, ) from django.utils.decorators import classproperty +from django.utils.deprecation import RemovedInDjango31Warning from django.views.static import serve __all__ = ('TestCase', 'TransactionTestCase', @@ -133,16 +136,31 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class _CursorFailure: - def __init__(self, cls_name, wrapped): - self.cls_name = cls_name + def __init__(self, wrapped, message): self.wrapped = wrapped + self.message = message def __call__(self): - raise AssertionError( - "Database queries aren't allowed in SimpleTestCase. " - "Either use TestCase or TransactionTestCase to ensure proper test isolation or " - "set %s.allow_database_queries to True to silence this failure." % self.cls_name - ) + raise AssertionError(self.message) + + +class _SimpleTestCaseDatabasesDescriptor: + """Descriptor for SimpleTestCase.allow_database_queries deprecation.""" + def __get__(self, instance, cls=None): + try: + allow_database_queries = cls.allow_database_queries + except AttributeError: + pass + else: + msg = ( + '`SimpleTestCase.allow_database_queries` is deprecated. ' + 'Restrict the databases available during the execution of ' + '%s.%s with the `databases` attribute instead.' + ) % (cls.__module__, cls.__qualname__) + warnings.warn(msg, RemovedInDjango31Warning) + if allow_database_queries: + return {DEFAULT_DB_ALIAS} + return set() class SimpleTestCase(unittest.TestCase): @@ -153,9 +171,13 @@ class SimpleTestCase(unittest.TestCase): _overridden_settings = None _modified_settings = None - # Tests shouldn't be allowed to query the database since - # this base class doesn't enforce any isolation. - allow_database_queries = False + databases = _SimpleTestCaseDatabasesDescriptor() + _disallowed_database_msg = ( + 'Database queries are not allowed in SimpleTestCase subclasses. ' + 'Either subclass TestCase or TransactionTestCase to ensure proper ' + 'test isolation or add %(alias)r to %(test)s.databases to silence ' + 'this failure.' + ) @classmethod def setUpClass(cls): @@ -166,19 +188,51 @@ class SimpleTestCase(unittest.TestCase): if cls._modified_settings: cls._cls_modified_context = modify_settings(cls._modified_settings) cls._cls_modified_context.enable() - if not cls.allow_database_queries: - for alias in connections: - connection = connections[alias] - connection.cursor = _CursorFailure(cls.__name__, connection.cursor) - connection.chunked_cursor = _CursorFailure(cls.__name__, connection.chunked_cursor) + cls._add_cursor_failures() + + @classmethod + def _validate_databases(cls): + if cls.databases == '__all__': + return frozenset(connections) + for alias in cls.databases: + if alias not in connections: + message = '%s.%s.databases refers to %r which is not defined in settings.DATABASES.' % ( + cls.__module__, + cls.__qualname__, + alias, + ) + close_matches = get_close_matches(alias, list(connections)) + if close_matches: + message += ' Did you mean %r?' % close_matches[0] + raise ImproperlyConfigured(message) + return frozenset(cls.databases) + + @classmethod + def _add_cursor_failures(cls): + cls.databases = cls._validate_databases() + for alias in connections: + if alias in cls.databases: + continue + connection = connections[alias] + message = cls._disallowed_database_msg % { + 'test': '%s.%s' % (cls.__module__, cls.__qualname__), + 'alias': alias, + } + connection.cursor = _CursorFailure(connection.cursor, message) + connection.chunked_cursor = _CursorFailure(connection.chunked_cursor, message) + + @classmethod + def _remove_cursor_failures(cls): + for alias in connections: + if alias in cls.databases: + continue + connection = connections[alias] + connection.cursor = connection.cursor.wrapped + connection.chunked_cursor = connection.chunked_cursor.wrapped @classmethod def tearDownClass(cls): - if not cls.allow_database_queries: - for alias in connections: - connection = connections[alias] - connection.cursor = connection.cursor.wrapped - connection.chunked_cursor = connection.chunked_cursor.wrapped + cls._remove_cursor_failures() if hasattr(cls, '_cls_modified_context'): cls._cls_modified_context.disable() delattr(cls, '_cls_modified_context') @@ -806,6 +860,26 @@ class SimpleTestCase(unittest.TestCase): self.fail(self._formatMessage(msg, standardMsg)) +class _TransactionTestCaseDatabasesDescriptor: + """Descriptor for TransactionTestCase.multi_db deprecation.""" + msg = ( + '`TransactionTestCase.multi_db` is deprecated. Databases available ' + 'during this test can be defined using %s.%s.databases.' + ) + + def __get__(self, instance, cls=None): + try: + multi_db = cls.multi_db + except AttributeError: + pass + else: + msg = self.msg % (cls.__module__, cls.__qualname__) + warnings.warn(msg, RemovedInDjango31Warning) + if multi_db: + return set(connections) + return {DEFAULT_DB_ALIAS} + + class TransactionTestCase(SimpleTestCase): # Subclasses can ask for resetting of auto increment sequence before each @@ -818,8 +892,12 @@ class TransactionTestCase(SimpleTestCase): # Subclasses can define fixtures which will be automatically installed. fixtures = None - # Do the tests in this class query non-default databases? - multi_db = False + databases = _TransactionTestCaseDatabasesDescriptor() + _disallowed_database_msg = ( + 'Database queries to %(alias)r are not allowed in this test. Add ' + '%(alias)r to %(test)s.databases to ensure proper test isolation ' + 'and silence this failure.' + ) # If transactions aren't available, Django will serialize the database # contents into a fixture during setup and flush and reload them @@ -827,10 +905,6 @@ class TransactionTestCase(SimpleTestCase): # This can be slow; this flag allows enabling on a per-case basis. serialized_rollback = False - # Since tests will be wrapped in a transaction, or serialized if they - # are not available, we allow queries to be run. - allow_database_queries = True - def _pre_setup(self): """ Perform pre-test setup: @@ -870,15 +944,13 @@ class TransactionTestCase(SimpleTestCase): @classmethod def _databases_names(cls, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if cls.multi_db: - return [ - alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR'] - ] - else: - return [DEFAULT_DB_ALIAS] + # Only consider allowed database aliases, including mirrors or not. + return [ + alias for alias in connections + if alias in cls.databases and ( + include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR'] + ) + ] def _reset_sequences(self, db_name): conn = connections[db_name] @@ -984,9 +1056,21 @@ class TransactionTestCase(SimpleTestCase): func(*args, **kwargs) -def connections_support_transactions(): - """Return True if all connections support transactions.""" - return all(conn.features.supports_transactions for conn in connections.all()) +def connections_support_transactions(aliases=None): + """ + Return whether or not all (or specified) connections support + transactions. + """ + conns = connections.all() if aliases is None else (connections[alias] for alias in aliases) + return all(conn.features.supports_transactions for conn in conns) + + +class _TestCaseDatabasesDescriptor(_TransactionTestCaseDatabasesDescriptor): + """Descriptor for TestCase.multi_db deprecation.""" + msg = ( + '`TestCase.multi_db` is deprecated. Databases available during this ' + 'test can be defined using %s.%s.databases.' + ) class TestCase(TransactionTestCase): @@ -1002,6 +1086,8 @@ class TestCase(TransactionTestCase): On database backends with no transaction support, TestCase behaves as TransactionTestCase. """ + databases = _TestCaseDatabasesDescriptor() + @classmethod def _enter_atomics(cls): """Open atomic blocks for multiple databases.""" @@ -1018,10 +1104,14 @@ class TestCase(TransactionTestCase): transaction.set_rollback(True, using=db_name) atomics[db_name].__exit__(None, None, None) + @classmethod + def _databases_support_transactions(cls): + return connections_support_transactions(cls.databases) + @classmethod def setUpClass(cls): super().setUpClass() - if not connections_support_transactions(): + if not cls._databases_support_transactions(): return cls.cls_atomics = cls._enter_atomics() @@ -1031,16 +1121,18 @@ class TestCase(TransactionTestCase): call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name}) except Exception: cls._rollback_atomics(cls.cls_atomics) + cls._remove_cursor_failures() raise try: cls.setUpTestData() except Exception: cls._rollback_atomics(cls.cls_atomics) + cls._remove_cursor_failures() raise @classmethod def tearDownClass(cls): - if connections_support_transactions(): + if cls._databases_support_transactions(): cls._rollback_atomics(cls.cls_atomics) for conn in connections.all(): conn.close() @@ -1052,12 +1144,12 @@ class TestCase(TransactionTestCase): pass def _should_reload_connections(self): - if connections_support_transactions(): + if self._databases_support_transactions(): return False return super()._should_reload_connections() def _fixture_setup(self): - if not connections_support_transactions(): + if not self._databases_support_transactions(): # If the backend does not support transactions, we should reload # class data before each test self.setUpTestData() @@ -1067,7 +1159,7 @@ class TestCase(TransactionTestCase): self.atomics = self._enter_atomics() def _fixture_teardown(self): - if not connections_support_transactions(): + if not self._databases_support_transactions(): return super()._fixture_teardown() try: for db_name in reversed(self._databases_names()): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 0e85e59e8c..087458fa5e 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -32,6 +32,9 @@ details on these changes. * ``RemoteUserBackend.configure_user()`` will require ``request`` as the first positional argument. +* Support for ``SimpleTestCase.allow_database_queries`` and + ``TransactionTestCase.multi_db`` will be removed. + .. _deprecation-removed-in-3.0: 3.0 diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index bc950eb88e..5c488570aa 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -513,3 +513,12 @@ Miscellaneous * :meth:`.RemoteUserBackend.configure_user` is now passed ``request`` as the first positional argument, if it accepts it. Support for overrides that don't accept it will be removed in Django 3.1. + +* The :attr:`.SimpleTestCase.allow_database_queries`, + :attr:`.TransactionTestCase.multi_db`, and :attr:`.TestCase.multi_db` + attributes are deprecated in favor of :attr:`.SimpleTestCase.databases`, + :attr:`.TransactionTestCase.databases`, and :attr:`.TestCase.databases`. + These new attributes allow databases dependencies to be declared in order to + prevent unexpected queries against non-default databases to leak state + between tests. The previous behavior of ``allow_database_queries=True`` and + ``multi_db=True`` can be achieved by setting ``databases='__all__'``. diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 38b437e18c..12f20c0144 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -722,14 +722,24 @@ A subclass of :class:`unittest.TestCase` that adds this functionality: If your tests make any database queries, use subclasses :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase`. -.. attribute:: SimpleTestCase.allow_database_queries +.. attribute:: SimpleTestCase.databases + + .. versionadded:: 2.2 :class:`~SimpleTestCase` disallows database queries by default. This helps to avoid executing write queries which will affect other tests since each ``SimpleTestCase`` test isn't run in a transaction. If you aren't concerned about this problem, you can disable this behavior by - setting the ``allow_database_queries`` class attribute to ``True`` on - your test class. + setting the ``databases`` class attribute to ``'__all__'`` on your test + class. + +.. attribute:: SimpleTestCase.allow_database_queries + + .. deprecated:: 2.2 + + This attribute is deprecated in favor of :attr:`databases`. The previous + behavior of ``allow_database_queries = True`` can be achieved by setting + ``databases = '__all__'``. .. warning:: @@ -1101,8 +1111,8 @@ you can be certain that the outcome of a test will not be affected by another test or by the order of test execution. By default, fixtures are only loaded into the ``default`` database. If you are -using multiple databases and set :attr:`multi_db=True -`, fixtures will be loaded into all databases. +using multiple databases and set :attr:`TransactionTestCase.databases`, +fixtures will be loaded into all specified databases. URLconf configuration --------------------- @@ -1119,7 +1129,9 @@ particular URL. Decorate your test class or test method with Multi-database support ---------------------- -.. attribute:: TransactionTestCase.multi_db +.. attribute:: TransactionTestCase.databases + +.. versionadded:: 2.2 Django sets up a test database corresponding to every database that is defined in the :setting:`DATABASES` definition in your settings @@ -1133,24 +1145,67 @@ 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, and you have a test that requires every database to be clean, you can -use the ``multi_db`` attribute on the test suite to request a full -flush. +use the ``databases`` attribute on the test suite to request extra databases +to be flushed. For example:: - class TestMyViews(TestCase): - multi_db = True + class TestMyViews(TransactionTestCase): + databases = {'default', 'other'} def test_index_page_view(self): call_some_test_code() -This test case will flush *all* the test databases before running -``test_index_page_view``. +This test case will flush the ``default`` and ``other`` test databases before +running ``test_index_page_view``. You can also use ``'__all__'`` to specify +that all of the test databases must be flushed. -The ``multi_db`` flag also affects into which databases the -:attr:`TransactionTestCase.fixtures` are loaded. By default (when -``multi_db=False``), fixtures are only loaded into the ``default`` database. -If ``multi_db=True``, fixtures are loaded into all databases. +The ``databases`` flag also controls which databases the +:attr:`TransactionTestCase.fixtures` are loaded into. By default, fixtures are +only loaded into the ``default`` database. + +Queries against databases not in ``databases`` will give assertion errors to +prevent state leaking between tests. + +.. attribute:: TransactionTestCase.multi_db + +.. deprecated:: 2.2 + +This attribute is deprecated in favor of :attr:`~TransactionTestCase.databases`. +The previous behavior of ``multi_db = True`` can be achieved by setting +``databases = '__all__'``. + +.. attribute:: TestCase.databases + +.. versionadded:: 2.2 + +By default, only the ``default`` database will be wrapped in a transaction +during a ``TestCase``'s execution and attempts to query other databases will +result in assertion errors to prevent state leaking between tests. + +Use the ``databases`` class attribute on the test class to request transaction +wrapping against non-``default`` databases. + +For example:: + + class OtherDBTests(TestCase): + databases = {'other'} + + def test_other_db_query(self): + ... + +This test will only allow queries against the ``other`` database. Just like for +:attr:`SimpleTestCase.databases` and :attr:`TransactionTestCase.databases`, the +``'__all__'`` constant can be used to specify that the test should allow +queries to all databases. + +.. attribute:: TestCase.multi_db + +.. deprecated:: 2.2 + +This attribute is deprecated in favor of :attr:`~TestCase.databases`. The +previous behavior of ``multi_db = True`` can be achieved by setting +``databases = '__all__'``. .. _overriding-settings: diff --git a/tests/admin_views/test_multidb.py b/tests/admin_views/test_multidb.py index ec3591d1fe..a02b637d34 100644 --- a/tests/admin_views/test_multidb.py +++ b/tests/admin_views/test_multidb.py @@ -28,7 +28,7 @@ urlpatterns = [ @override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__]) class MultiDatabaseTests(TestCase): - multi_db = True + databases = {'default', 'other'} @classmethod def setUpTestData(cls): diff --git a/tests/auth_tests/test_admin_multidb.py b/tests/auth_tests/test_admin_multidb.py index eff458de19..5849ef98e5 100644 --- a/tests/auth_tests/test_admin_multidb.py +++ b/tests/auth_tests/test_admin_multidb.py @@ -27,7 +27,7 @@ urlpatterns = [ @override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__]) class MultiDatabaseTests(TestCase): - multi_db = True + databases = {'default', 'other'} @classmethod def setUpTestData(cls): diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index b9bb092e78..87f2ae1790 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -213,7 +213,7 @@ class ChangepasswordManagementCommandTestCase(TestCase): class MultiDBChangepasswordManagementCommandTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} @mock.patch.object(changepassword.Command, '_get_pass', return_value='not qwerty') def test_that_changepassword_command_with_database_option_uses_given_db(self, mock_get_pass): @@ -906,7 +906,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): class MultiDBCreatesuperuserTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_createsuperuser_command_with_database_option(self): """ diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py index 3c1b8fae64..dd3377d7a6 100644 --- a/tests/auth_tests/test_models.py +++ b/tests/auth_tests/test_models.py @@ -47,7 +47,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase): class LoadDataWithNaturalKeysAndMultipleDatabasesTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_load_data_with_user_permissions(self): # Create test contenttypes for both databases diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 6d957c54bb..3fbd057289 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -1085,7 +1085,7 @@ class DBCacheRouter: }, ) class CreateCacheTableForDBCacheTests(TestCase): - multi_db = True + databases = {'default', 'other'} @override_settings(DATABASE_ROUTERS=[DBCacheRouter()]) def test_createcachetable_observes_database_router(self): diff --git a/tests/check_framework/test_database.py b/tests/check_framework/test_database.py index 2dff3aaca4..06baf0e38d 100644 --- a/tests/check_framework/test_database.py +++ b/tests/check_framework/test_database.py @@ -8,7 +8,7 @@ from django.test import TestCase class DatabaseCheckTests(TestCase): - multi_db = True + databases = {'default', 'other'} @property def func(self): diff --git a/tests/contenttypes_tests/test_models.py b/tests/contenttypes_tests/test_models.py index 0c263aabf0..91fdf8340f 100644 --- a/tests/contenttypes_tests/test_models.py +++ b/tests/contenttypes_tests/test_models.py @@ -214,7 +214,7 @@ class TestRouter: @override_settings(DATABASE_ROUTERS=[TestRouter()]) class ContentTypesMultidbTests(TestCase): - multi_db = True + databases = {'default', 'other'} def test_multidb(self): """ diff --git a/tests/context_processors/tests.py b/tests/context_processors/tests.py index 0baf806c1d..79b9ddef67 100644 --- a/tests/context_processors/tests.py +++ b/tests/context_processors/tests.py @@ -64,7 +64,7 @@ class DebugContextProcessorTests(TestCase): """ Tests for the ``django.template.context_processors.debug`` processor. """ - multi_db = True + databases = {'default', 'other'} def test_debug(self): url = '/debug/' diff --git a/tests/gis_tests/layermap/tests.py b/tests/gis_tests/layermap/tests.py index 460d6f6a4d..1efa643211 100644 --- a/tests/gis_tests/layermap/tests.py +++ b/tests/gis_tests/layermap/tests.py @@ -341,7 +341,7 @@ class OtherRouter: @override_settings(DATABASE_ROUTERS=[OtherRouter()]) class LayerMapRouterTest(TestCase): - multi_db = True + databases = {'default', 'other'} @unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required') def test_layermapping_default_db(self): diff --git a/tests/migrations/test_base.py b/tests/migrations/test_base.py index 7fcbaffd24..970998f562 100644 --- a/tests/migrations/test_base.py +++ b/tests/migrations/test_base.py @@ -18,7 +18,7 @@ class MigrationTestBase(TransactionTestCase): """ available_apps = ["migrations"] - multi_db = True + databases = {'default', 'other'} def tearDown(self): # Reset applied-migrations state. diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index c2891ed1a1..c231440949 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -25,7 +25,7 @@ class MigrateTests(MigrationTestBase): """ Tests running the migrate command. """ - multi_db = True + databases = {'default', 'other'} @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_migrate(self): diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py index a7666c5c2d..e3a635dc63 100644 --- a/tests/migrations/test_loader.py +++ b/tests/migrations/test_loader.py @@ -16,7 +16,7 @@ class RecorderTests(TestCase): """ Tests recording migrations as applied or not. """ - multi_db = True + databases = {'default', 'other'} def test_apply(self): """ diff --git a/tests/migrations/test_multidb.py b/tests/migrations/test_multidb.py index 0bed042964..b2c4320ad3 100644 --- a/tests/migrations/test_multidb.py +++ b/tests/migrations/test_multidb.py @@ -38,7 +38,7 @@ class MigrateWhenFooRouter: class MultiDBOperationTests(OperationTestBase): - multi_db = True + databases = {'default', 'other'} def _test_create_model(self, app_label, should_run): """ diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py index b447be575c..a403b9fd88 100644 --- a/tests/multiple_database/tests.py +++ b/tests/multiple_database/tests.py @@ -17,7 +17,7 @@ from .routers import AuthRouter, TestRouter, WriteRouter class QueryTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_db_selection(self): "Querysets will use the default database by default" @@ -998,7 +998,7 @@ class ConnectionRouterTestCase(SimpleTestCase): # Make the 'other' database appear to be a replica of the 'default' @override_settings(DATABASE_ROUTERS=[TestRouter()]) class RouterTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_db_selection(self): "Querysets obey the router for db suggestions" @@ -1526,7 +1526,7 @@ class RouterTestCase(TestCase): @override_settings(DATABASE_ROUTERS=[AuthRouter()]) class AuthTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_auth_manager(self): "The methods on the auth manager obey database hints" @@ -1589,7 +1589,7 @@ class AntiPetRouter: class FixtureTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} fixtures = ['multidb-common', 'multidb'] @override_settings(DATABASE_ROUTERS=[AntiPetRouter()]) @@ -1629,7 +1629,7 @@ class FixtureTestCase(TestCase): class PickleQuerySetTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_pickling(self): for db in connections: @@ -1655,7 +1655,7 @@ class WriteToOtherRouter: class SignalTests(TestCase): - multi_db = True + databases = {'default', 'other'} def override_router(self): return override_settings(DATABASE_ROUTERS=[WriteToOtherRouter()]) @@ -1755,7 +1755,7 @@ class AttributeErrorRouter: class RouterAttributeErrorTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def override_router(self): return override_settings(DATABASE_ROUTERS=[AttributeErrorRouter()]) @@ -1807,7 +1807,7 @@ class ModelMetaRouter: @override_settings(DATABASE_ROUTERS=[ModelMetaRouter()]) class RouterModelArgumentTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} def test_m2m_collection(self): b = Book.objects.create(title="Pro Django", @@ -1845,7 +1845,7 @@ class MigrateTestCase(TestCase): 'django.contrib.auth', 'django.contrib.contenttypes' ] - multi_db = True + databases = {'default', 'other'} def test_migrate_to_other_database(self): """Regression test for #16039: migrate with --database option.""" @@ -1879,7 +1879,7 @@ class RouterUsed(Exception): class RouteForWriteTestCase(TestCase): - multi_db = True + databases = {'default', 'other'} class WriteCheckRouter: def db_for_write(self, model, **hints): @@ -2093,7 +2093,7 @@ class NoRelationRouter: @override_settings(DATABASE_ROUTERS=[NoRelationRouter()]) class RelationAssignmentTests(SimpleTestCase): """allow_relation() is called with unsaved model instances.""" - multi_db = True + databases = {'default', 'other'} router_prevents_msg = 'the current database router prevents this relation' def test_foreign_key_relation(self): diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index 9201ff3853..24982dda14 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -1155,7 +1155,7 @@ class NullableTest(TestCase): class MultiDbTests(TestCase): - multi_db = True + databases = {'default', 'other'} def test_using_is_honored_m2m(self): B = Book.objects.using('other') diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 5917e30d24..7f75b85d6c 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -209,8 +209,7 @@ class LiveServerPort(LiveServerBase): "Acquired duplicate server addresses for server threads: %s" % self.live_server_url ) finally: - if hasattr(TestCase, 'server_thread'): - TestCase.server_thread.terminate() + TestCase.tearDownClass() def test_specified_port_bind(self): """LiveServerTestCase.port customizes the server's port.""" @@ -227,8 +226,7 @@ class LiveServerPort(LiveServerBase): 'Did not use specified port for LiveServerTestCase thread: %s' % TestCase.port ) finally: - if hasattr(TestCase, 'server_thread'): - TestCase.server_thread.terminate() + TestCase.tearDownClass() class LiverServerThreadedTests(LiveServerBase): diff --git a/tests/sites_tests/tests.py b/tests/sites_tests/tests.py index c5e20b4549..500a422b21 100644 --- a/tests/sites_tests/tests.py +++ b/tests/sites_tests/tests.py @@ -18,7 +18,7 @@ from django.test.utils import captured_stdout @modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'}) class SitesFrameworkTests(TestCase): - multi_db = True + databases = {'default', 'other'} @classmethod def setUpTestData(cls): @@ -236,7 +236,7 @@ class JustOtherRouter: @modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'}) class CreateDefaultSiteTests(TestCase): - multi_db = True + databases = {'default', 'other'} @classmethod def setUpTestData(cls): diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 477398da20..43c605eba6 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -241,8 +241,8 @@ class Ticket17477RegressionTests(AdminScriptTestCase): class SQLiteInMemoryTestDbs(TransactionTestCase): - multi_db = True available_apps = ['test_runner'] + databases = {'default', 'other'} @unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections), "This is an sqlite-specific issue") diff --git a/tests/test_utils/test_deprecated_features.py b/tests/test_utils/test_deprecated_features.py new file mode 100644 index 0000000000..fbed5e14c5 --- /dev/null +++ b/tests/test_utils/test_deprecated_features.py @@ -0,0 +1,64 @@ +from django.db import connections +from django.db.utils import DEFAULT_DB_ALIAS +from django.test import SimpleTestCase, TestCase, TransactionTestCase +from django.utils.deprecation import RemovedInDjango31Warning + + +class AllowDatabaseQueriesDeprecationTests(SimpleTestCase): + def test_enabled(self): + class AllowedDatabaseQueries(SimpleTestCase): + allow_database_queries = True + message = ( + '`SimpleTestCase.allow_database_queries` is deprecated. Restrict ' + 'the databases available during the execution of ' + 'test_utils.test_deprecated_features.AllowDatabaseQueriesDeprecationTests.' + 'test_enabled..AllowedDatabaseQueries with the ' + '`databases` attribute instead.' + ) + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(AllowedDatabaseQueries.databases, {'default'}) + + def test_explicitly_disabled(self): + class AllowedDatabaseQueries(SimpleTestCase): + allow_database_queries = False + message = ( + '`SimpleTestCase.allow_database_queries` is deprecated. Restrict ' + 'the databases available during the execution of ' + 'test_utils.test_deprecated_features.AllowDatabaseQueriesDeprecationTests.' + 'test_explicitly_disabled..AllowedDatabaseQueries with ' + 'the `databases` attribute instead.' + ) + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(AllowedDatabaseQueries.databases, set()) + + +class MultiDbDeprecationTests(SimpleTestCase): + def test_transaction_test_case(self): + class MultiDbTestCase(TransactionTestCase): + multi_db = True + message = ( + '`TransactionTestCase.multi_db` is deprecated. Databases ' + 'available during this test can be defined using ' + 'test_utils.test_deprecated_features.MultiDbDeprecationTests.' + 'test_transaction_test_case..MultiDbTestCase.databases.' + ) + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(MultiDbTestCase.databases, set(connections)) + MultiDbTestCase.multi_db = False + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(MultiDbTestCase.databases, {DEFAULT_DB_ALIAS}) + + def test_test_case(self): + class MultiDbTestCase(TestCase): + multi_db = True + message = ( + '`TestCase.multi_db` is deprecated. Databases available during ' + 'this test can be defined using ' + 'test_utils.test_deprecated_features.MultiDbDeprecationTests.' + 'test_test_case..MultiDbTestCase.databases.' + ) + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(MultiDbTestCase.databases, set(connections)) + MultiDbTestCase.multi_db = False + with self.assertWarnsMessage(RemovedInDjango31Warning, message): + self.assertEqual(MultiDbTestCase.databases, {DEFAULT_DB_ALIAS}) diff --git a/tests/test_utils/test_testcase.py b/tests/test_utils/test_testcase.py index 8a367391cb..f374549400 100644 --- a/tests/test_utils/test_testcase.py +++ b/tests/test_utils/test_testcase.py @@ -1,7 +1,7 @@ from django.db import IntegrityError, transaction from django.test import TestCase, skipUnlessDBFeature -from .models import PossessedCar +from .models import Car, PossessedCar class TestTestCase(TestCase): @@ -18,3 +18,12 @@ class TestTestCase(TestCase): car.delete() finally: self._rollback_atomics = rollback_atomics + + def test_disallowed_database_queries(self): + message = ( + "Database queries to 'other' are not allowed in this test. " + "Add 'other' to test_utils.test_testcase.TestTestCase.databases to " + "ensure proper test isolation and silence this failure." + ) + with self.assertRaisesMessage(AssertionError, message): + Car.objects.using('other').get() diff --git a/tests/test_utils/test_transactiontestcase.py b/tests/test_utils/test_transactiontestcase.py index 40c9b7576f..3a9d173138 100644 --- a/tests/test_utils/test_transactiontestcase.py +++ b/tests/test_utils/test_transactiontestcase.py @@ -3,6 +3,8 @@ from unittest import mock from django.db import connections from django.test import TestCase, TransactionTestCase, override_settings +from .models import Car + class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase): """ @@ -32,9 +34,9 @@ class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase): @override_settings(DEBUG=True) # Enable query logging for test_queries_cleared -class TransactionTestCaseMultiDbTests(TestCase): +class TransactionTestCaseDatabasesTests(TestCase): available_apps = [] - multi_db = True + databases = {'default', 'other'} def test_queries_cleared(self): """ @@ -44,3 +46,17 @@ class TransactionTestCaseMultiDbTests(TestCase): """ for alias in connections: self.assertEqual(len(connections[alias].queries_log), 0, 'Failed for alias %s' % alias) + + +class DisallowedDatabaseQueriesTests(TransactionTestCase): + available_apps = ['test_utils'] + + def test_disallowed_database_queries(self): + message = ( + "Database queries to 'other' are not allowed in this test. " + "Add 'other' to test_utils.test_transactiontestcase." + "DisallowedDatabaseQueriesTests.databases to ensure proper test " + "isolation and silence this failure." + ) + with self.assertRaisesMessage(AssertionError, message): + Car.objects.using('other').get() diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index e9aa9d9c98..c7e55e0711 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -7,8 +7,9 @@ from unittest import mock from django.conf import settings from django.contrib.staticfiles.finders import get_finder, get_finders from django.contrib.staticfiles.storage import staticfiles_storage +from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage -from django.db import connection, models, router +from django.db import connection, connections, models, router from django.forms import EmailField, IntegerField from django.http import HttpResponse from django.template.loader import render_to_string @@ -1160,32 +1161,67 @@ class TestBadSetUpTestData(TestCase): class DisallowedDatabaseQueriesTests(SimpleTestCase): def test_disallowed_database_queries(self): expected_message = ( - "Database queries aren't allowed in SimpleTestCase. " - "Either use TestCase or TransactionTestCase to ensure proper test isolation or " - "set DisallowedDatabaseQueriesTests.allow_database_queries to True to silence this failure." + "Database queries are not allowed in SimpleTestCase subclasses. " + "Either subclass TestCase or TransactionTestCase to ensure proper " + "test isolation or add 'default' to " + "test_utils.tests.DisallowedDatabaseQueriesTests.databases to " + "silence this failure." ) with self.assertRaisesMessage(AssertionError, expected_message): Car.objects.first() - -class DisallowedDatabaseQueriesChunkedCursorsTests(SimpleTestCase): - def test_disallowed_database_queries(self): + def test_disallowed_database_chunked_cursor_queries(self): expected_message = ( - "Database queries aren't allowed in SimpleTestCase. Either use " - "TestCase or TransactionTestCase to ensure proper test isolation or " - "set DisallowedDatabaseQueriesChunkedCursorsTests.allow_database_queries " - "to True to silence this failure." + "Database queries are not allowed in SimpleTestCase subclasses. " + "Either subclass TestCase or TransactionTestCase to ensure proper " + "test isolation or add 'default' to " + "test_utils.tests.DisallowedDatabaseQueriesTests.databases to " + "silence this failure." ) with self.assertRaisesMessage(AssertionError, expected_message): next(Car.objects.iterator()) class AllowedDatabaseQueriesTests(SimpleTestCase): - allow_database_queries = True + databases = {'default'} def test_allowed_database_queries(self): Car.objects.first() + def test_allowed_database_chunked_cursor_queries(self): + next(Car.objects.iterator(), None) + + +class DatabaseAliasTests(SimpleTestCase): + def setUp(self): + self.addCleanup(setattr, self.__class__, 'databases', self.databases) + + def test_no_close_match(self): + self.__class__.databases = {'void'} + message = ( + "test_utils.tests.DatabaseAliasTests.databases refers to 'void' which is not defined " + "in settings.DATABASES." + ) + with self.assertRaisesMessage(ImproperlyConfigured, message): + self._validate_databases() + + def test_close_match(self): + self.__class__.databases = {'defualt'} + message = ( + "test_utils.tests.DatabaseAliasTests.databases refers to 'defualt' which is not defined " + "in settings.DATABASES. Did you mean 'default'?" + ) + with self.assertRaisesMessage(ImproperlyConfigured, message): + self._validate_databases() + + def test_match(self): + self.__class__.databases = {'default', 'other'} + self.assertEqual(self._validate_databases(), frozenset({'default', 'other'})) + + def test_all(self): + self.__class__.databases = '__all__' + self.assertEqual(self._validate_databases(), frozenset(connections)) + @isolate_apps('test_utils', attr_name='class_apps') class IsolatedAppsTests(SimpleTestCase): diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 4b90acd05c..db23d10d32 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -225,7 +225,7 @@ class DebugViewTests(SimpleTestCase): class DebugViewQueriesAllowedTests(SimpleTestCase): # May need a query to initialize MySQL connection - allow_database_queries = True + databases = {'default'} def test_handle_db_exception(self): """