Loading django/db/models/options.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 = [] Loading Loading @@ -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 Loading django/test/utils.py +68 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 docs/internals/contributing/writing-code/unit-tests.txt +114 −0 Original line number Diff line number Diff line Loading @@ -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) tests/test_utils/tests.py +41 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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
django/db/models/options.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 = [] Loading Loading @@ -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 Loading
django/test/utils.py +68 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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
docs/internals/contributing/writing-code/unit-tests.txt +114 −0 Original line number Diff line number Diff line Loading @@ -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)
tests/test_utils/tests.py +41 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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)