Refs #25746 -- Added a test utility to isolate inlined model registration.
Thanks to Tim for the review.
This commit is contained in:
parent
b2cddeaaf4
commit
7bb373e309
|
@ -69,6 +69,8 @@ class Options(object):
|
|||
'local_concrete_fields', '_forward_fields_map'}
|
||||
REVERSE_PROPERTIES = {'related_objects', 'fields_map', '_relation_tree'}
|
||||
|
||||
default_apps = apps
|
||||
|
||||
def __init__(self, meta, app_label=None):
|
||||
self._get_fields_cache = {}
|
||||
self.local_fields = []
|
||||
|
@ -124,7 +126,7 @@ class Options(object):
|
|||
self.related_fkey_lookups = []
|
||||
|
||||
# A custom app registry to use, if you're making a separate model set.
|
||||
self.apps = apps
|
||||
self.apps = self.default_apps
|
||||
|
||||
self.default_related_name = None
|
||||
|
||||
|
|
|
@ -9,10 +9,12 @@ from unittest import skipIf, skipUnless
|
|||
from xml.dom.minidom import Node, parseString
|
||||
|
||||
from django.apps import apps
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import UserSettingsHolder, settings
|
||||
from django.core import mail
|
||||
from django.core.signals import request_started
|
||||
from django.db import reset_queries
|
||||
from django.db.models.options import Options
|
||||
from django.http import request
|
||||
from django.template import Template
|
||||
from django.test.signals import setting_changed, template_rendered
|
||||
|
@ -640,3 +642,69 @@ class LoggingCaptureMixin(object):
|
|||
|
||||
def tearDown(self):
|
||||
self.logger.handlers[0].stream = self.old_stream
|
||||
|
||||
|
||||
class isolate_apps(object):
|
||||
"""
|
||||
Act as either a decorator or a context manager to register models defined
|
||||
in its wrapped context to an isolated registry.
|
||||
|
||||
The list of installed apps the isolated registry should contain must be
|
||||
passed as arguments.
|
||||
|
||||
Two optional keyword arguments can be specified:
|
||||
|
||||
`attr_name`: attribute assigned the isolated registry if used as a class
|
||||
decorator.
|
||||
|
||||
`kwarg_name`: keyword argument passing the isolated registry to the
|
||||
decorated method.
|
||||
"""
|
||||
|
||||
def __init__(self, *installed_apps, **kwargs):
|
||||
self.installed_apps = installed_apps
|
||||
self.attr_name = kwargs.pop('attr_name', None)
|
||||
self.kwarg_name = kwargs.pop('kwarg_name', None)
|
||||
|
||||
def enable(self):
|
||||
self.old_apps = Options.default_apps
|
||||
apps = Apps(self.installed_apps)
|
||||
setattr(Options, 'default_apps', apps)
|
||||
return apps
|
||||
|
||||
def disable(self):
|
||||
setattr(Options, 'default_apps', self.old_apps)
|
||||
|
||||
def __enter__(self):
|
||||
return self.enable()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.disable()
|
||||
|
||||
def __call__(self, decorated):
|
||||
if isinstance(decorated, type):
|
||||
# A class is decorated
|
||||
decorated_setUp = decorated.setUp
|
||||
decorated_tearDown = decorated.tearDown
|
||||
|
||||
def setUp(inner_self):
|
||||
apps = self.enable()
|
||||
if self.attr_name:
|
||||
setattr(inner_self, self.attr_name, apps)
|
||||
decorated_setUp(inner_self)
|
||||
|
||||
def tearDown(inner_self):
|
||||
decorated_tearDown(inner_self)
|
||||
self.disable()
|
||||
|
||||
decorated.setUp = setUp
|
||||
decorated.tearDown = tearDown
|
||||
return decorated
|
||||
else:
|
||||
@wraps(decorated)
|
||||
def inner(*args, **kwargs):
|
||||
with self as apps:
|
||||
if self.kwarg_name:
|
||||
kwargs[self.kwarg_name] = apps
|
||||
return decorated(*args, **kwargs)
|
||||
return inner
|
||||
|
|
|
@ -303,3 +303,117 @@ purpose.
|
|||
|
||||
Support for running tests in parallel and the ``--parallel`` option were
|
||||
added.
|
||||
|
||||
Tips for writing tests
|
||||
----------------------
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
Isolating model registration
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To avoid polluting the global :attr:`~django.apps.apps` registry and prevent
|
||||
unnecessary table creation, models defined in a test method should be bound to
|
||||
a temporary ``Apps`` instance::
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
def test_model_definition(self):
|
||||
test_apps = Apps(['app_label'])
|
||||
|
||||
class TestModel(models.Model):
|
||||
class Meta:
|
||||
apps = test_apps
|
||||
...
|
||||
|
||||
.. function:: django.test.utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)
|
||||
|
||||
.. versionadded:: 1.10
|
||||
|
||||
Since this pattern involves a lot of boilerplate, Django provides the
|
||||
:func:`~django.test.utils.isolate_apps` decorator. It's used like this::
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps
|
||||
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
@isolate_apps('app_label')
|
||||
def test_model_definition(self):
|
||||
class TestModel(models.Model):
|
||||
pass
|
||||
...
|
||||
|
||||
.. admonition:: Setting ``app_label``
|
||||
|
||||
Models defined in a test method with no explicit
|
||||
:attr:`~django.db.models.Options.app_label` are automatically assigned the
|
||||
label of the app in which their test class is located.
|
||||
|
||||
In order to make sure the models defined within the context of
|
||||
:func:`~django.test.utils.isolate_apps` instances are correctly
|
||||
installed, you should pass the set of targeted ``app_label`` as arguments:
|
||||
|
||||
.. snippet::
|
||||
:filename: tests/app_label/tests.py
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps
|
||||
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
@isolate_apps('app_label', 'other_app_label')
|
||||
def test_model_definition(self):
|
||||
# This model automatically receives app_label='app_label'
|
||||
class TestModel(models.Model):
|
||||
pass
|
||||
|
||||
class OtherAppModel(models.Model):
|
||||
class Meta:
|
||||
app_label = 'other_app_label'
|
||||
...
|
||||
|
||||
The decorator can also be applied to classes::
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps
|
||||
|
||||
@isolate_apps('app_label')
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
def test_model_definition(self):
|
||||
class TestModel(models.Model):
|
||||
pass
|
||||
...
|
||||
|
||||
The temporary ``Apps`` instance used to isolate model registration can be
|
||||
retrieved as an attribute when used as a class decorator by using the
|
||||
``attr_name`` parameter::
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps
|
||||
|
||||
@isolate_apps('app_label', attr_name='apps')
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
def test_model_definition(self):
|
||||
class TestModel(models.Model):
|
||||
pass
|
||||
self.assertIs(self.apps.get_model('app_label', 'TestModel'), TestModel)
|
||||
|
||||
Or as an argument on the test method when used as a method decorator by using
|
||||
the ``kwarg_name`` parameter::
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import isolate_apps
|
||||
|
||||
class TestModelDefinition(SimpleTestCase):
|
||||
@isolate_apps('app_label', kwarg_name='apps')
|
||||
def test_model_definition(self, apps):
|
||||
class TestModel(models.Model):
|
||||
pass
|
||||
self.assertIs(apps.get_model('app_label', 'TestModel'), TestModel)
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import url
|
|||
from django.contrib.staticfiles.finders import get_finder, get_finders
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import connection, router
|
||||
from django.db import connection, models, router
|
||||
from django.forms import EmailField, IntegerField
|
||||
from django.http import HttpResponse
|
||||
from django.template.loader import render_to_string
|
||||
|
@ -17,7 +17,9 @@ from django.test import (
|
|||
skipUnlessDBFeature,
|
||||
)
|
||||
from django.test.html import HTMLParseError, parse_html
|
||||
from django.test.utils import CaptureQueriesContext, override_settings
|
||||
from django.test.utils import (
|
||||
CaptureQueriesContext, isolate_apps, override_settings,
|
||||
)
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils import six
|
||||
from django.utils._os import abspathu
|
||||
|
@ -1029,3 +1031,40 @@ class AllowedDatabaseQueriesTests(SimpleTestCase):
|
|||
|
||||
def test_allowed_database_queries(self):
|
||||
Car.objects.first()
|
||||
|
||||
|
||||
@isolate_apps('test_utils', attr_name='class_apps')
|
||||
class IsolatedAppsTests(SimpleTestCase):
|
||||
def test_installed_apps(self):
|
||||
self.assertEqual([app_config.label for app_config in self.class_apps.get_app_configs()], ['test_utils'])
|
||||
|
||||
def test_class_decoration(self):
|
||||
class ClassDecoration(models.Model):
|
||||
pass
|
||||
self.assertEqual(ClassDecoration._meta.apps, self.class_apps)
|
||||
|
||||
@isolate_apps('test_utils', kwarg_name='method_apps')
|
||||
def test_method_decoration(self, method_apps):
|
||||
class MethodDecoration(models.Model):
|
||||
pass
|
||||
self.assertEqual(MethodDecoration._meta.apps, method_apps)
|
||||
|
||||
def test_context_manager(self):
|
||||
with isolate_apps('test_utils') as context_apps:
|
||||
class ContextManager(models.Model):
|
||||
pass
|
||||
self.assertEqual(ContextManager._meta.apps, context_apps)
|
||||
|
||||
@isolate_apps('test_utils', kwarg_name='method_apps')
|
||||
def test_nested(self, method_apps):
|
||||
class MethodDecoration(models.Model):
|
||||
pass
|
||||
with isolate_apps('test_utils') as context_apps:
|
||||
class ContextManager(models.Model):
|
||||
pass
|
||||
with isolate_apps('test_utils') as nested_context_apps:
|
||||
class NestedContextManager(models.Model):
|
||||
pass
|
||||
self.assertEqual(MethodDecoration._meta.apps, method_apps)
|
||||
self.assertEqual(ContextManager._meta.apps, context_apps)
|
||||
self.assertEqual(NestedContextManager._meta.apps, nested_context_apps)
|
||||
|
|
Loading…
Reference in New Issue