2
0
Эх сурвалжийг харах

Refs #28478 -- Deprecated TestCase's allow_database_queries and multi_db in favor of databases.

Simon Charette 6 жил өмнө
parent
commit
8c775391b7

+ 136 - 44
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()):

+ 3 - 0
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

+ 9 - 0
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__'``.

+ 71 - 16
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
-<TransactionTestCase.multi_db>`, 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 ``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
 
-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.
+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:
 

+ 1 - 1
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):

+ 1 - 1
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):

+ 2 - 2
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):
         """

+ 1 - 1
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

+ 1 - 1
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):

+ 1 - 1
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):

+ 1 - 1
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):
         """

+ 1 - 1
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/'

+ 1 - 1
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):

+ 1 - 1
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.

+ 1 - 1
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):

+ 1 - 1
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):
         """

+ 1 - 1
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):
         """

+ 11 - 11
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):

+ 1 - 1
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')

+ 2 - 4
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):

+ 2 - 2
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):

+ 1 - 1
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")

+ 64 - 0
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.<locals>.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.<locals>.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.<locals>.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.<locals>.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})

+ 10 - 1
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()

+ 18 - 2
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()

+ 48 - 12
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):

+ 1 - 1
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):
         """