mirror of
https://github.com/django/django.git
synced 2025-06-17 09:29:17 +00:00
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'}
|
'local_concrete_fields', '_forward_fields_map'}
|
||||||
REVERSE_PROPERTIES = {'related_objects', 'fields_map', '_relation_tree'}
|
REVERSE_PROPERTIES = {'related_objects', 'fields_map', '_relation_tree'}
|
||||||
|
|
||||||
|
default_apps = apps
|
||||||
|
|
||||||
def __init__(self, meta, app_label=None):
|
def __init__(self, meta, app_label=None):
|
||||||
self._get_fields_cache = {}
|
self._get_fields_cache = {}
|
||||||
self.local_fields = []
|
self.local_fields = []
|
||||||
@ -124,7 +126,7 @@ class Options(object):
|
|||||||
self.related_fkey_lookups = []
|
self.related_fkey_lookups = []
|
||||||
|
|
||||||
# A custom app registry to use, if you're making a separate model set.
|
# 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
|
self.default_related_name = None
|
||||||
|
|
||||||
|
@ -9,10 +9,12 @@ from unittest import skipIf, skipUnless
|
|||||||
from xml.dom.minidom import Node, parseString
|
from xml.dom.minidom import Node, parseString
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.apps.registry import Apps
|
||||||
from django.conf import UserSettingsHolder, settings
|
from django.conf import UserSettingsHolder, settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.signals import request_started
|
from django.core.signals import request_started
|
||||||
from django.db import reset_queries
|
from django.db import reset_queries
|
||||||
|
from django.db.models.options import Options
|
||||||
from django.http import request
|
from django.http import request
|
||||||
from django.template import Template
|
from django.template import Template
|
||||||
from django.test.signals import setting_changed, template_rendered
|
from django.test.signals import setting_changed, template_rendered
|
||||||
@ -640,3 +642,69 @@ class LoggingCaptureMixin(object):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.logger.handlers[0].stream = self.old_stream
|
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
|
Support for running tests in parallel and the ``--parallel`` option were
|
||||||
added.
|
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.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.files.storage import default_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.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
|
||||||
@ -17,7 +17,9 @@ from django.test import (
|
|||||||
skipUnlessDBFeature,
|
skipUnlessDBFeature,
|
||||||
)
|
)
|
||||||
from django.test.html import HTMLParseError, parse_html
|
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.urls import NoReverseMatch, reverse
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils._os import abspathu
|
from django.utils._os import abspathu
|
||||||
@ -1029,3 +1031,40 @@ class AllowedDatabaseQueriesTests(SimpleTestCase):
|
|||||||
|
|
||||||
def test_allowed_database_queries(self):
|
def test_allowed_database_queries(self):
|
||||||
Car.objects.first()
|
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…
x
Reference in New Issue
Block a user