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

This commit is contained in:
Simon Charette 2018-07-12 00:12:20 -04:00 committed by Tim Graham
parent 647be06538
commit 8c775391b7
27 changed files with 391 additions and 109 deletions

View File

@ -4,9 +4,11 @@ import posixpath
import sys import sys
import threading import threading
import unittest import unittest
import warnings
from collections import Counter from collections import Counter
from contextlib import contextmanager from contextlib import contextmanager
from copy import copy from copy import copy
from difflib import get_close_matches
from functools import wraps from functools import wraps
from unittest.util import safe_repr from unittest.util import safe_repr
from urllib.parse import ( from urllib.parse import (
@ -17,7 +19,7 @@ from urllib.request import url2pathname
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core import mail 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.files import locks
from django.core.handlers.wsgi import WSGIHandler, get_path_info from django.core.handlers.wsgi import WSGIHandler, get_path_info
from django.core.management import call_command from django.core.management import call_command
@ -36,6 +38,7 @@ from django.test.utils import (
override_settings, override_settings,
) )
from django.utils.decorators import classproperty from django.utils.decorators import classproperty
from django.utils.deprecation import RemovedInDjango31Warning
from django.views.static import serve from django.views.static import serve
__all__ = ('TestCase', 'TransactionTestCase', __all__ = ('TestCase', 'TransactionTestCase',
@ -133,16 +136,31 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext):
class _CursorFailure: class _CursorFailure:
def __init__(self, cls_name, wrapped): def __init__(self, wrapped, message):
self.cls_name = cls_name
self.wrapped = wrapped self.wrapped = wrapped
self.message = message
def __call__(self): def __call__(self):
raise AssertionError( raise AssertionError(self.message)
"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 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): class SimpleTestCase(unittest.TestCase):
@ -153,9 +171,13 @@ class SimpleTestCase(unittest.TestCase):
_overridden_settings = None _overridden_settings = None
_modified_settings = None _modified_settings = None
# Tests shouldn't be allowed to query the database since databases = _SimpleTestCaseDatabasesDescriptor()
# this base class doesn't enforce any isolation. _disallowed_database_msg = (
allow_database_queries = False '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 @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -166,19 +188,51 @@ class SimpleTestCase(unittest.TestCase):
if cls._modified_settings: if cls._modified_settings:
cls._cls_modified_context = modify_settings(cls._modified_settings) cls._cls_modified_context = modify_settings(cls._modified_settings)
cls._cls_modified_context.enable() cls._cls_modified_context.enable()
if not cls.allow_database_queries: cls._add_cursor_failures()
for alias in connections:
connection = connections[alias]
connection.cursor = _CursorFailure(cls.__name__, connection.cursor)
connection.chunked_cursor = _CursorFailure(cls.__name__, connection.chunked_cursor)
@classmethod @classmethod
def tearDownClass(cls): def _validate_databases(cls):
if not cls.allow_database_queries: 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: 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 = connections[alias]
connection.cursor = connection.cursor.wrapped connection.cursor = connection.cursor.wrapped
connection.chunked_cursor = connection.chunked_cursor.wrapped connection.chunked_cursor = connection.chunked_cursor.wrapped
@classmethod
def tearDownClass(cls):
cls._remove_cursor_failures()
if hasattr(cls, '_cls_modified_context'): if hasattr(cls, '_cls_modified_context'):
cls._cls_modified_context.disable() cls._cls_modified_context.disable()
delattr(cls, '_cls_modified_context') delattr(cls, '_cls_modified_context')
@ -806,6 +860,26 @@ class SimpleTestCase(unittest.TestCase):
self.fail(self._formatMessage(msg, standardMsg)) 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): class TransactionTestCase(SimpleTestCase):
# Subclasses can ask for resetting of auto increment sequence before each # 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. # Subclasses can define fixtures which will be automatically installed.
fixtures = None fixtures = None
# Do the tests in this class query non-default databases? databases = _TransactionTestCaseDatabasesDescriptor()
multi_db = False _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 # If transactions aren't available, Django will serialize the database
# contents into a fixture during setup and flush and reload them # 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. # This can be slow; this flag allows enabling on a per-case basis.
serialized_rollback = False 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): def _pre_setup(self):
""" """
Perform pre-test setup: Perform pre-test setup:
@ -870,15 +944,13 @@ class TransactionTestCase(SimpleTestCase):
@classmethod @classmethod
def _databases_names(cls, include_mirrors=True): def _databases_names(cls, include_mirrors=True):
# If the test case has a multi_db=True flag, act on all databases, # Only consider allowed database aliases, including mirrors or not.
# including mirrors or not. Otherwise, just on the default DB.
if cls.multi_db:
return [ return [
alias for alias in connections alias for alias in connections
if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR'] if alias in cls.databases and (
include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR']
)
] ]
else:
return [DEFAULT_DB_ALIAS]
def _reset_sequences(self, db_name): def _reset_sequences(self, db_name):
conn = connections[db_name] conn = connections[db_name]
@ -984,9 +1056,21 @@ class TransactionTestCase(SimpleTestCase):
func(*args, **kwargs) func(*args, **kwargs)
def connections_support_transactions(): def connections_support_transactions(aliases=None):
"""Return True if all connections support transactions.""" """
return all(conn.features.supports_transactions for conn in connections.all()) 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): class TestCase(TransactionTestCase):
@ -1002,6 +1086,8 @@ class TestCase(TransactionTestCase):
On database backends with no transaction support, TestCase behaves as On database backends with no transaction support, TestCase behaves as
TransactionTestCase. TransactionTestCase.
""" """
databases = _TestCaseDatabasesDescriptor()
@classmethod @classmethod
def _enter_atomics(cls): def _enter_atomics(cls):
"""Open atomic blocks for multiple databases.""" """Open atomic blocks for multiple databases."""
@ -1018,10 +1104,14 @@ class TestCase(TransactionTestCase):
transaction.set_rollback(True, using=db_name) transaction.set_rollback(True, using=db_name)
atomics[db_name].__exit__(None, None, None) atomics[db_name].__exit__(None, None, None)
@classmethod
def _databases_support_transactions(cls):
return connections_support_transactions(cls.databases)
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
if not connections_support_transactions(): if not cls._databases_support_transactions():
return return
cls.cls_atomics = cls._enter_atomics() cls.cls_atomics = cls._enter_atomics()
@ -1031,16 +1121,18 @@ class TestCase(TransactionTestCase):
call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name}) call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})
except Exception: except Exception:
cls._rollback_atomics(cls.cls_atomics) cls._rollback_atomics(cls.cls_atomics)
cls._remove_cursor_failures()
raise raise
try: try:
cls.setUpTestData() cls.setUpTestData()
except Exception: except Exception:
cls._rollback_atomics(cls.cls_atomics) cls._rollback_atomics(cls.cls_atomics)
cls._remove_cursor_failures()
raise raise
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
if connections_support_transactions(): if cls._databases_support_transactions():
cls._rollback_atomics(cls.cls_atomics) cls._rollback_atomics(cls.cls_atomics)
for conn in connections.all(): for conn in connections.all():
conn.close() conn.close()
@ -1052,12 +1144,12 @@ class TestCase(TransactionTestCase):
pass pass
def _should_reload_connections(self): def _should_reload_connections(self):
if connections_support_transactions(): if self._databases_support_transactions():
return False return False
return super()._should_reload_connections() return super()._should_reload_connections()
def _fixture_setup(self): 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 # If the backend does not support transactions, we should reload
# class data before each test # class data before each test
self.setUpTestData() self.setUpTestData()
@ -1067,7 +1159,7 @@ class TestCase(TransactionTestCase):
self.atomics = self._enter_atomics() self.atomics = self._enter_atomics()
def _fixture_teardown(self): def _fixture_teardown(self):
if not connections_support_transactions(): if not self._databases_support_transactions():
return super()._fixture_teardown() return super()._fixture_teardown()
try: try:
for db_name in reversed(self._databases_names()): for db_name in reversed(self._databases_names()):

View File

@ -32,6 +32,9 @@ details on these changes.
* ``RemoteUserBackend.configure_user()`` will require ``request`` as the first * ``RemoteUserBackend.configure_user()`` will require ``request`` as the first
positional argument. positional argument.
* Support for ``SimpleTestCase.allow_database_queries`` and
``TransactionTestCase.multi_db`` will be removed.
.. _deprecation-removed-in-3.0: .. _deprecation-removed-in-3.0:
3.0 3.0

View File

@ -513,3 +513,12 @@ Miscellaneous
* :meth:`.RemoteUserBackend.configure_user` is now passed ``request`` as the * :meth:`.RemoteUserBackend.configure_user` is now passed ``request`` as the
first positional argument, if it accepts it. Support for overrides that don't first positional argument, if it accepts it. Support for overrides that don't
accept it will be removed in Django 3.1. 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__'``.

View File

@ -722,14 +722,24 @@ A subclass of :class:`unittest.TestCase` that adds this functionality:
If your tests make any database queries, use subclasses If your tests make any database queries, use subclasses
:class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase`. :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 :class:`~SimpleTestCase` disallows database queries by default. This
helps to avoid executing write queries which will affect other tests helps to avoid executing write queries which will affect other tests
since each ``SimpleTestCase`` test isn't run in a transaction. If you since each ``SimpleTestCase`` test isn't run in a transaction. If you
aren't concerned about this problem, you can disable this behavior by aren't concerned about this problem, you can disable this behavior by
setting the ``allow_database_queries`` class attribute to ``True`` on setting the ``databases`` class attribute to ``'__all__'`` on your test
your test class. 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:: .. 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. test or by the order of test execution.
By default, fixtures are only loaded into the ``default`` database. If you are By default, fixtures are only loaded into the ``default`` database. If you are
using multiple databases and set :attr:`multi_db=True using multiple databases and set :attr:`TransactionTestCase.databases`,
<TransactionTestCase.multi_db>`, fixtures will be loaded into all databases. fixtures will be loaded into all specified databases.
URLconf configuration URLconf configuration
--------------------- ---------------------
@ -1119,7 +1129,9 @@ particular URL. Decorate your test class or test method with
Multi-database support 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 Django sets up a test database corresponding to every database that is
defined in the :setting:`DATABASES` definition in your settings 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 As an optimization, Django only flushes the ``default`` database at
the start of each test run. If your setup contains multiple databases, 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 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 use the ``databases`` attribute on the test suite to request extra databases
flush. to be flushed.
For example:: For example::
class TestMyViews(TestCase): class TestMyViews(TransactionTestCase):
multi_db = True databases = {'default', 'other'}
def test_index_page_view(self): def test_index_page_view(self):
call_some_test_code() call_some_test_code()
This test case will flush *all* the test databases before running This test case will flush the ``default`` and ``other`` test databases before
``test_index_page_view``. 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 The ``databases`` flag also controls which databases the
:attr:`TransactionTestCase.fixtures` are loaded. By default (when :attr:`TransactionTestCase.fixtures` are loaded into. By default, fixtures are
``multi_db=False``), fixtures are only loaded into the ``default`` database. only loaded into the ``default`` database.
If ``multi_db=True``, fixtures are loaded into all databases.
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: .. _overriding-settings:

View File

@ -28,7 +28,7 @@ urlpatterns = [
@override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__]) @override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__])
class MultiDatabaseTests(TestCase): class MultiDatabaseTests(TestCase):
multi_db = True databases = {'default', 'other'}
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -27,7 +27,7 @@ urlpatterns = [
@override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__]) @override_settings(ROOT_URLCONF=__name__, DATABASE_ROUTERS=['%s.Router' % __name__])
class MultiDatabaseTests(TestCase): class MultiDatabaseTests(TestCase):
multi_db = True databases = {'default', 'other'}
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -213,7 +213,7 @@ class ChangepasswordManagementCommandTestCase(TestCase):
class MultiDBChangepasswordManagementCommandTestCase(TestCase): class MultiDBChangepasswordManagementCommandTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
@mock.patch.object(changepassword.Command, '_get_pass', return_value='not qwerty') @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): 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): class MultiDBCreatesuperuserTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_createsuperuser_command_with_database_option(self): def test_createsuperuser_command_with_database_option(self):
""" """

View File

@ -47,7 +47,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase):
class LoadDataWithNaturalKeysAndMultipleDatabasesTestCase(TestCase): class LoadDataWithNaturalKeysAndMultipleDatabasesTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_load_data_with_user_permissions(self): def test_load_data_with_user_permissions(self):
# Create test contenttypes for both databases # Create test contenttypes for both databases

View File

@ -1085,7 +1085,7 @@ class DBCacheRouter:
}, },
) )
class CreateCacheTableForDBCacheTests(TestCase): class CreateCacheTableForDBCacheTests(TestCase):
multi_db = True databases = {'default', 'other'}
@override_settings(DATABASE_ROUTERS=[DBCacheRouter()]) @override_settings(DATABASE_ROUTERS=[DBCacheRouter()])
def test_createcachetable_observes_database_router(self): def test_createcachetable_observes_database_router(self):

View File

@ -8,7 +8,7 @@ from django.test import TestCase
class DatabaseCheckTests(TestCase): class DatabaseCheckTests(TestCase):
multi_db = True databases = {'default', 'other'}
@property @property
def func(self): def func(self):

View File

@ -214,7 +214,7 @@ class TestRouter:
@override_settings(DATABASE_ROUTERS=[TestRouter()]) @override_settings(DATABASE_ROUTERS=[TestRouter()])
class ContentTypesMultidbTests(TestCase): class ContentTypesMultidbTests(TestCase):
multi_db = True databases = {'default', 'other'}
def test_multidb(self): def test_multidb(self):
""" """

View File

@ -64,7 +64,7 @@ class DebugContextProcessorTests(TestCase):
""" """
Tests for the ``django.template.context_processors.debug`` processor. Tests for the ``django.template.context_processors.debug`` processor.
""" """
multi_db = True databases = {'default', 'other'}
def test_debug(self): def test_debug(self):
url = '/debug/' url = '/debug/'

View File

@ -341,7 +341,7 @@ class OtherRouter:
@override_settings(DATABASE_ROUTERS=[OtherRouter()]) @override_settings(DATABASE_ROUTERS=[OtherRouter()])
class LayerMapRouterTest(TestCase): class LayerMapRouterTest(TestCase):
multi_db = True databases = {'default', 'other'}
@unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required') @unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required')
def test_layermapping_default_db(self): def test_layermapping_default_db(self):

View File

@ -18,7 +18,7 @@ class MigrationTestBase(TransactionTestCase):
""" """
available_apps = ["migrations"] available_apps = ["migrations"]
multi_db = True databases = {'default', 'other'}
def tearDown(self): def tearDown(self):
# Reset applied-migrations state. # Reset applied-migrations state.

View File

@ -25,7 +25,7 @@ class MigrateTests(MigrationTestBase):
""" """
Tests running the migrate command. Tests running the migrate command.
""" """
multi_db = True databases = {'default', 'other'}
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_migrate(self): def test_migrate(self):

View File

@ -16,7 +16,7 @@ class RecorderTests(TestCase):
""" """
Tests recording migrations as applied or not. Tests recording migrations as applied or not.
""" """
multi_db = True databases = {'default', 'other'}
def test_apply(self): def test_apply(self):
""" """

View File

@ -38,7 +38,7 @@ class MigrateWhenFooRouter:
class MultiDBOperationTests(OperationTestBase): class MultiDBOperationTests(OperationTestBase):
multi_db = True databases = {'default', 'other'}
def _test_create_model(self, app_label, should_run): def _test_create_model(self, app_label, should_run):
""" """

View File

@ -17,7 +17,7 @@ from .routers import AuthRouter, TestRouter, WriteRouter
class QueryTestCase(TestCase): class QueryTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_db_selection(self): def test_db_selection(self):
"Querysets will use the default database by default" "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' # Make the 'other' database appear to be a replica of the 'default'
@override_settings(DATABASE_ROUTERS=[TestRouter()]) @override_settings(DATABASE_ROUTERS=[TestRouter()])
class RouterTestCase(TestCase): class RouterTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_db_selection(self): def test_db_selection(self):
"Querysets obey the router for db suggestions" "Querysets obey the router for db suggestions"
@ -1526,7 +1526,7 @@ class RouterTestCase(TestCase):
@override_settings(DATABASE_ROUTERS=[AuthRouter()]) @override_settings(DATABASE_ROUTERS=[AuthRouter()])
class AuthTestCase(TestCase): class AuthTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_auth_manager(self): def test_auth_manager(self):
"The methods on the auth manager obey database hints" "The methods on the auth manager obey database hints"
@ -1589,7 +1589,7 @@ class AntiPetRouter:
class FixtureTestCase(TestCase): class FixtureTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
fixtures = ['multidb-common', 'multidb'] fixtures = ['multidb-common', 'multidb']
@override_settings(DATABASE_ROUTERS=[AntiPetRouter()]) @override_settings(DATABASE_ROUTERS=[AntiPetRouter()])
@ -1629,7 +1629,7 @@ class FixtureTestCase(TestCase):
class PickleQuerySetTestCase(TestCase): class PickleQuerySetTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_pickling(self): def test_pickling(self):
for db in connections: for db in connections:
@ -1655,7 +1655,7 @@ class WriteToOtherRouter:
class SignalTests(TestCase): class SignalTests(TestCase):
multi_db = True databases = {'default', 'other'}
def override_router(self): def override_router(self):
return override_settings(DATABASE_ROUTERS=[WriteToOtherRouter()]) return override_settings(DATABASE_ROUTERS=[WriteToOtherRouter()])
@ -1755,7 +1755,7 @@ class AttributeErrorRouter:
class RouterAttributeErrorTestCase(TestCase): class RouterAttributeErrorTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def override_router(self): def override_router(self):
return override_settings(DATABASE_ROUTERS=[AttributeErrorRouter()]) return override_settings(DATABASE_ROUTERS=[AttributeErrorRouter()])
@ -1807,7 +1807,7 @@ class ModelMetaRouter:
@override_settings(DATABASE_ROUTERS=[ModelMetaRouter()]) @override_settings(DATABASE_ROUTERS=[ModelMetaRouter()])
class RouterModelArgumentTestCase(TestCase): class RouterModelArgumentTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
def test_m2m_collection(self): def test_m2m_collection(self):
b = Book.objects.create(title="Pro Django", b = Book.objects.create(title="Pro Django",
@ -1845,7 +1845,7 @@ class MigrateTestCase(TestCase):
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes' 'django.contrib.contenttypes'
] ]
multi_db = True databases = {'default', 'other'}
def test_migrate_to_other_database(self): def test_migrate_to_other_database(self):
"""Regression test for #16039: migrate with --database option.""" """Regression test for #16039: migrate with --database option."""
@ -1879,7 +1879,7 @@ class RouterUsed(Exception):
class RouteForWriteTestCase(TestCase): class RouteForWriteTestCase(TestCase):
multi_db = True databases = {'default', 'other'}
class WriteCheckRouter: class WriteCheckRouter:
def db_for_write(self, model, **hints): def db_for_write(self, model, **hints):
@ -2093,7 +2093,7 @@ class NoRelationRouter:
@override_settings(DATABASE_ROUTERS=[NoRelationRouter()]) @override_settings(DATABASE_ROUTERS=[NoRelationRouter()])
class RelationAssignmentTests(SimpleTestCase): class RelationAssignmentTests(SimpleTestCase):
"""allow_relation() is called with unsaved model instances.""" """allow_relation() is called with unsaved model instances."""
multi_db = True databases = {'default', 'other'}
router_prevents_msg = 'the current database router prevents this relation' router_prevents_msg = 'the current database router prevents this relation'
def test_foreign_key_relation(self): def test_foreign_key_relation(self):

View File

@ -1155,7 +1155,7 @@ class NullableTest(TestCase):
class MultiDbTests(TestCase): class MultiDbTests(TestCase):
multi_db = True databases = {'default', 'other'}
def test_using_is_honored_m2m(self): def test_using_is_honored_m2m(self):
B = Book.objects.using('other') B = Book.objects.using('other')

View File

@ -209,8 +209,7 @@ class LiveServerPort(LiveServerBase):
"Acquired duplicate server addresses for server threads: %s" % self.live_server_url "Acquired duplicate server addresses for server threads: %s" % self.live_server_url
) )
finally: finally:
if hasattr(TestCase, 'server_thread'): TestCase.tearDownClass()
TestCase.server_thread.terminate()
def test_specified_port_bind(self): def test_specified_port_bind(self):
"""LiveServerTestCase.port customizes the server's port.""" """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 'Did not use specified port for LiveServerTestCase thread: %s' % TestCase.port
) )
finally: finally:
if hasattr(TestCase, 'server_thread'): TestCase.tearDownClass()
TestCase.server_thread.terminate()
class LiverServerThreadedTests(LiveServerBase): class LiverServerThreadedTests(LiveServerBase):

View File

@ -18,7 +18,7 @@ from django.test.utils import captured_stdout
@modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'}) @modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'})
class SitesFrameworkTests(TestCase): class SitesFrameworkTests(TestCase):
multi_db = True databases = {'default', 'other'}
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -236,7 +236,7 @@ class JustOtherRouter:
@modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'}) @modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'})
class CreateDefaultSiteTests(TestCase): class CreateDefaultSiteTests(TestCase):
multi_db = True databases = {'default', 'other'}
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -241,8 +241,8 @@ class Ticket17477RegressionTests(AdminScriptTestCase):
class SQLiteInMemoryTestDbs(TransactionTestCase): class SQLiteInMemoryTestDbs(TransactionTestCase):
multi_db = True
available_apps = ['test_runner'] available_apps = ['test_runner']
databases = {'default', 'other'}
@unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections), @unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections),
"This is an sqlite-specific issue") "This is an sqlite-specific issue")

View File

@ -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})

View File

@ -1,7 +1,7 @@
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.test import TestCase, skipUnlessDBFeature from django.test import TestCase, skipUnlessDBFeature
from .models import PossessedCar from .models import Car, PossessedCar
class TestTestCase(TestCase): class TestTestCase(TestCase):
@ -18,3 +18,12 @@ class TestTestCase(TestCase):
car.delete() car.delete()
finally: finally:
self._rollback_atomics = rollback_atomics 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()

View File

@ -3,6 +3,8 @@ from unittest import mock
from django.db import connections from django.db import connections
from django.test import TestCase, TransactionTestCase, override_settings from django.test import TestCase, TransactionTestCase, override_settings
from .models import Car
class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase): class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase):
""" """
@ -32,9 +34,9 @@ class TestSerializedRollbackInhibitsPostMigrate(TransactionTestCase):
@override_settings(DEBUG=True) # Enable query logging for test_queries_cleared @override_settings(DEBUG=True) # Enable query logging for test_queries_cleared
class TransactionTestCaseMultiDbTests(TestCase): class TransactionTestCaseDatabasesTests(TestCase):
available_apps = [] available_apps = []
multi_db = True databases = {'default', 'other'}
def test_queries_cleared(self): def test_queries_cleared(self):
""" """
@ -44,3 +46,17 @@ class TransactionTestCaseMultiDbTests(TestCase):
""" """
for alias in connections: for alias in connections:
self.assertEqual(len(connections[alias].queries_log), 0, 'Failed for alias %s' % alias) 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()

View File

@ -7,8 +7,9 @@ from unittest import mock
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles.finders import get_finder, get_finders from django.contrib.staticfiles.finders import get_finder, get_finders
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage 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.forms import EmailField, IntegerField
from django.http import HttpResponse from django.http import HttpResponse
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -1160,32 +1161,67 @@ class TestBadSetUpTestData(TestCase):
class DisallowedDatabaseQueriesTests(SimpleTestCase): class DisallowedDatabaseQueriesTests(SimpleTestCase):
def test_disallowed_database_queries(self): def test_disallowed_database_queries(self):
expected_message = ( expected_message = (
"Database queries aren't allowed in SimpleTestCase. " "Database queries are not allowed in SimpleTestCase subclasses. "
"Either use TestCase or TransactionTestCase to ensure proper test isolation or " "Either subclass TestCase or TransactionTestCase to ensure proper "
"set DisallowedDatabaseQueriesTests.allow_database_queries to True to silence this failure." "test isolation or add 'default' to "
"test_utils.tests.DisallowedDatabaseQueriesTests.databases to "
"silence this failure."
) )
with self.assertRaisesMessage(AssertionError, expected_message): with self.assertRaisesMessage(AssertionError, expected_message):
Car.objects.first() Car.objects.first()
def test_disallowed_database_chunked_cursor_queries(self):
class DisallowedDatabaseQueriesChunkedCursorsTests(SimpleTestCase):
def test_disallowed_database_queries(self):
expected_message = ( expected_message = (
"Database queries aren't allowed in SimpleTestCase. Either use " "Database queries are not allowed in SimpleTestCase subclasses. "
"TestCase or TransactionTestCase to ensure proper test isolation or " "Either subclass TestCase or TransactionTestCase to ensure proper "
"set DisallowedDatabaseQueriesChunkedCursorsTests.allow_database_queries " "test isolation or add 'default' to "
"to True to silence this failure." "test_utils.tests.DisallowedDatabaseQueriesTests.databases to "
"silence this failure."
) )
with self.assertRaisesMessage(AssertionError, expected_message): with self.assertRaisesMessage(AssertionError, expected_message):
next(Car.objects.iterator()) next(Car.objects.iterator())
class AllowedDatabaseQueriesTests(SimpleTestCase): class AllowedDatabaseQueriesTests(SimpleTestCase):
allow_database_queries = True databases = {'default'}
def test_allowed_database_queries(self): def test_allowed_database_queries(self):
Car.objects.first() 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') @isolate_apps('test_utils', attr_name='class_apps')
class IsolatedAppsTests(SimpleTestCase): class IsolatedAppsTests(SimpleTestCase):

View File

@ -225,7 +225,7 @@ class DebugViewTests(SimpleTestCase):
class DebugViewQueriesAllowedTests(SimpleTestCase): class DebugViewQueriesAllowedTests(SimpleTestCase):
# May need a query to initialize MySQL connection # May need a query to initialize MySQL connection
allow_database_queries = True databases = {'default'}
def test_handle_db_exception(self): def test_handle_db_exception(self):
""" """