Refs #25746 -- Added a test utility to isolate inlined model registration.

Thanks to Tim for the review.
This commit is contained in:
Simon Charette 2015-11-17 00:33:18 -05:00
parent b2cddeaaf4
commit 7bb373e309
4 changed files with 226 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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