Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #15561 -- Extended test setting override code added in r16165 w…

…ith a decorator and a signal for setting changes.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16237 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit a3a53e0b7364c7f9bf3a2f1551dc527505051974 1 parent 091c9b5
@jezdez jezdez authored
View
2  django/test/signals.py
@@ -1,3 +1,5 @@
from django.dispatch import Signal
template_rendered = Signal(providing_args=["template", "context"])
+
+setting_changed = Signal(providing_args=["setting", "value"])
View
16 django/test/testcases.py
@@ -2,7 +2,6 @@
import re
import sys
-from contextlib import contextmanager
from functools import wraps
from urlparse import urlsplit, urlunsplit
from xml.dom.minidom import parseString, Node
@@ -17,7 +16,7 @@
from django.http import QueryDict
from django.test import _doctest as doctest
from django.test.client import Client
-from django.test.utils import get_warnings_state, restore_warnings_state
+from django.test.utils import get_warnings_state, restore_warnings_state, override_settings
from django.utils import simplejson, unittest as ut2
from django.utils.encoding import smart_str
@@ -342,21 +341,12 @@ def restore_warnings_state(self):
"""
restore_warnings_state(self._warnings_state)
- @contextmanager
- def settings(self, **options):
+ def settings(self, **kwargs):
"""
A context manager that temporarily sets a setting and reverts
back to the original value when exiting the context.
"""
- old_wrapped = settings._wrapped
- override = UserSettingsHolder(settings._wrapped)
- try:
- for key, new_value in options.items():
- setattr(override, key, new_value)
- settings._wrapped = override
- yield
- finally:
- settings._wrapped = old_wrapped
+ return override_settings(**kwargs)
def assertRedirects(self, response, expected_url, status_code=302,
target_status_code=200, host=None, msg_prefix=''):
View
59 django/test/utils.py
@@ -1,17 +1,23 @@
+from __future__ import with_statement
+
import sys
import time
import os
import warnings
-from django.conf import settings
+from django.conf import settings, UserSettingsHolder
from django.core import mail
from django.core.mail.backends import locmem
-from django.test import signals
+from django.test.signals import template_rendered, setting_changed
from django.template import Template, loader, TemplateDoesNotExist
from django.template.loaders import cached
from django.utils.translation import deactivate
+from django.utils.functional import wraps
+
-__all__ = ('Approximate', 'ContextList', 'setup_test_environment',
- 'teardown_test_environment', 'get_runner')
+__all__ = (
+ 'Approximate', 'ContextList', 'get_runner', 'override_settings',
+ 'setup_test_environment', 'teardown_test_environment',
+)
RESTORE_LOADERS_ATTR = '_original_template_source_loaders'
@@ -56,7 +62,7 @@ def instrumented_test_render(self, context):
An instrumented Template render method, providing a signal
that can be intercepted by the test system Client
"""
- signals.template_rendered.send(sender=self, template=self, context=context)
+ template_rendered.send(sender=self, template=self, context=context)
return self.nodelist.render(context)
@@ -160,3 +166,46 @@ def restore_template_loaders():
"""
loader.template_source_loaders = getattr(loader, RESTORE_LOADERS_ATTR)
delattr(loader, RESTORE_LOADERS_ATTR)
+
+
+class OverrideSettingsHolder(UserSettingsHolder):
+ """
+ A custom setting holder that sends a signal upon change.
+ """
+ def __setattr__(self, name, value):
+ UserSettingsHolder.__setattr__(self, name, value)
+ setting_changed.send(sender=name, setting=name, value=value)
+
+
+class override_settings(object):
+ """
+ Acts as either a decorator, or a context manager. If it's a decorator it
+ takes a function and returns a wrapped function. If it's a contextmanager
+ it's used with the ``with`` statement. In either event entering/exiting
+ are called before and after, respectively, the function/block is executed.
+ """
+ def __init__(self, **kwargs):
+ self.options = kwargs
+ self.wrapped = settings._wrapped
+
+ def __enter__(self):
+ self.enable()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.disable()
+
+ def __call__(self, func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+ return inner
+
+ def enable(self):
+ override = OverrideSettingsHolder(settings._wrapped)
+ for key, new_value in self.options.items():
+ setattr(override, key, new_value)
+ settings._wrapped = override
+
+ def disable(self):
+ settings._wrapped = self.wrapped
View
23 docs/ref/signals.txt
@@ -460,6 +460,29 @@ Test signals
Signals only sent when :doc:`running tests </topics/testing>`.
+setting_changed
+---------------
+
+.. versionadded:: 1.4
+
+.. data:: django.test.signals.setting_changed
+ :module:
+
+Sent when some :ref:`settings are overridden <overriding-setting>` with the
+:meth:`django.test.TestCase.setting` context manager or the
+:func:`django.test.utils.override_settings` decorator/context manager.
+
+Arguments sent with this signal:
+
+``sender``
+ The setting name (string).
+
+``setting``
+ Same as sender
+
+``value``
+ The new setting value.
+
template_rendered
-----------------
View
62 docs/topics/testing.txt
@@ -1361,6 +1361,8 @@ For example::
This test case will flush *all* the test databases before running
``testIndexPageView``.
+.. _overriding-setting:
+
Overriding settings
~~~~~~~~~~~~~~~~~~~
@@ -1376,7 +1378,14 @@ this use case Django provides a standard `Python context manager`_
from django.test import TestCase
class LoginTestCase(TestCase):
- def test_overriding_settings(self):
+
+ def test_login(self):
+
+ # First check for the default behavior
+ response = self.client.get('/sekrit/')
+ self.assertRedirects(response, '/accounts/login/?next=/sekrit/')
+
+ # Then override the LOGING_URL setting
with self.settings(LOGIN_URL='/other/login/'):
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/other/login/?next=/sekrit/')
@@ -1384,7 +1393,58 @@ this use case Django provides a standard `Python context manager`_
This example will override the :setting:`LOGIN_URL` setting for the code
in the ``with`` block and reset its value to the previous state afterwards.
+.. function:: utils.override_settings
+
+In case you want to override a setting for just one test method or even the
+whole TestCase class, Django provides the
+:func:`django.test.utils.override_settings` decorator_. It's used like this::
+
+ from django.test import TestCase
+ from django.test.utils import override_settings
+
+ class LoginTestCase(TestCase):
+
+ @override_settings(LOGIN_URL='/other/login/')
+ def test_login(self):
+ response = self.client.get('/sekrit/')
+ self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+The decorator can also be applied to test case classes::
+
+ from django.test import TestCase
+ from django.test.utils import override_settings
+
+ class LoginTestCase(TestCase):
+
+ def test_login(self):
+ response = self.client.get('/sekrit/')
+ self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+ LoginTestCase = override_settings(LOGIN_URL='/other/login/')(LoginTestCase)
+
+On Python 2.6 and higher you can also use the well known decorator syntax to
+decorate the class::
+
+ from django.test import TestCase
+ from django.test.utils import override_settings
+
+ @override_settings(LOGIN_URL='/other/login/')
+ class LoginTestCase(TestCase):
+
+ def test_login(self):
+ response = self.client.get('/sekrit/')
+ self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+.. note::
+
+ When overriding settings make sure to also handle the cases in which
+ Django or your app's code use a cache or another feature that retain
+ state even if the setting is changed. Django provides the
+ :data:`django.test.signals.setting_changed` signal to connect cleanup
+ and other state resetting callbacks to.
+
.. _`Python context manager`: http://www.python.org/dev/peps/pep-0343/
+.. _`decorator`: http://www.python.org/dev/peps/pep-0318/
Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
View
67 tests/regressiontests/mail/tests.py
@@ -9,48 +9,14 @@
import tempfile
import threading
-from django.conf import settings
from django.core import mail
from django.core.mail import (EmailMessage, mail_admins, mail_managers,
EmailMultiAlternatives, send_mail, send_mass_mail)
from django.core.mail.backends import console, dummy, locmem, filebased, smtp
from django.core.mail.message import BadHeaderError
from django.test import TestCase
+from django.test.utils import override_settings
from django.utils.translation import ugettext_lazy
-from django.utils.functional import wraps
-
-
-def alter_django_settings(**kwargs):
- oldvalues = {}
- nonexistant = []
- for setting, newvalue in kwargs.iteritems():
- try:
- oldvalues[setting] = getattr(settings, setting)
- except AttributeError:
- nonexistant.append(setting)
- setattr(settings, setting, newvalue)
- return oldvalues, nonexistant
-
-
-def restore_django_settings(state):
- oldvalues, nonexistant = state
- for setting, oldvalue in oldvalues.iteritems():
- setattr(settings, setting, oldvalue)
- for setting in nonexistant:
- delattr(settings, setting)
-
-
-def with_django_settings(**kwargs):
- def decorator(test):
- @wraps(test)
- def decorated_test(self):
- state = alter_django_settings(**kwargs)
- try:
- return test(self)
- finally:
- restore_django_settings(state)
- return decorated_test
- return decorator
class MailTests(TestCase):
@@ -251,7 +217,7 @@ def test_backend_arg(self):
shutil.rmtree(tmp_dir)
self.assertTrue(isinstance(mail.get_connection(), locmem.EmailBackend))
- @with_django_settings(
+ @override_settings(
EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',
ADMINS=[('nobody', 'nobody@example.com')],
MANAGERS=[('nobody', 'nobody@example.com')])
@@ -323,10 +289,11 @@ class BaseEmailBackendTests(object):
email_backend = None
def setUp(self):
- self.__settings_state = alter_django_settings(EMAIL_BACKEND=self.email_backend)
+ self.settings_override = override_settings(EMAIL_BACKEND=self.email_backend)
+ self.settings_override.enable()
def tearDown(self):
- restore_django_settings(self.__settings_state)
+ self.settings_override.disable()
def assertStartsWith(self, first, second):
if not first.startswith(second):
@@ -375,7 +342,7 @@ def test_send_verbose_name(self):
self.assertEqual(message.get_payload(), "Content")
self.assertEqual(message["from"], "=?utf-8?q?Firstname_S=C3=BCrname?= <from@example.com>")
- @with_django_settings(MANAGERS=[('nobody', 'nobody@example.com')])
+ @override_settings(MANAGERS=[('nobody', 'nobody@example.com')])
def test_html_mail_managers(self):
"""Test html_message argument to mail_managers"""
mail_managers('Subject', 'Content', html_message='HTML Content')
@@ -390,7 +357,7 @@ def test_html_mail_managers(self):
self.assertEqual(message.get_payload(1).get_payload(), 'HTML Content')
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
- @with_django_settings(ADMINS=[('nobody', 'nobody@example.com')])
+ @override_settings(ADMINS=[('nobody', 'nobody@example.com')])
def test_html_mail_admins(self):
"""Test html_message argument to mail_admins """
mail_admins('Subject', 'Content', html_message='HTML Content')
@@ -405,8 +372,9 @@ def test_html_mail_admins(self):
self.assertEqual(message.get_payload(1).get_payload(), 'HTML Content')
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
- @with_django_settings(ADMINS=[('nobody', 'nobody+admin@example.com')],
- MANAGERS=[('nobody', 'nobody+manager@example.com')])
+ @override_settings(
+ ADMINS=[('nobody', 'nobody+admin@example.com')],
+ MANAGERS=[('nobody', 'nobody+manager@example.com')])
def test_manager_and_admin_mail_prefix(self):
"""
String prefix + lazy translated subject = bad output
@@ -421,7 +389,7 @@ def test_manager_and_admin_mail_prefix(self):
message = self.get_the_message()
self.assertEqual(message.get('subject'), '[Django] Subject')
- @with_django_settings(ADMINS=(), MANAGERS=())
+ @override_settings(ADMINS=(), MANAGERS=())
def test_empty_admins(self):
"""
Test that mail_admins/mail_managers doesn't connect to the mail server
@@ -501,13 +469,14 @@ class FileBackendTests(BaseEmailBackendTests, TestCase):
email_backend = 'django.core.mail.backends.filebased.EmailBackend'
def setUp(self):
- super(FileBackendTests, self).setUp()
self.tmp_dir = tempfile.mkdtemp()
- self.__settings_state = alter_django_settings(EMAIL_FILE_PATH=self.tmp_dir)
+ self.addCleanup(shutil.rmtree, self.tmp_dir)
+ self.settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir)
+ self.settings_override.enable()
+ super(FileBackendTests, self).setUp()
def tearDown(self):
- restore_django_settings(self.__settings_state)
- shutil.rmtree(self.tmp_dir)
+ self.settings_override.disable()
super(FileBackendTests, self).tearDown()
def flush_mailbox(self):
@@ -642,13 +611,15 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase):
@classmethod
def setUpClass(cls):
cls.server = FakeSMTPServer(('127.0.0.1', 0), None)
- cls.settings = alter_django_settings(
+ cls.settings_override = override_settings(
EMAIL_HOST="127.0.0.1",
EMAIL_PORT=cls.server.socket.getsockname()[1])
+ cls.settings_override.enable()
cls.server.start()
@classmethod
def tearDownClass(cls):
+ cls.settings_override.disable()
cls.server.stop()
def setUp(self):
View
56 tests/regressiontests/settings_tests/tests.py
@@ -1,7 +1,22 @@
from __future__ import with_statement
-import os
+import os, sys
from django.conf import settings, global_settings
-from django.test import TestCase
+from django.test import TestCase, signals
+from django.test.utils import override_settings
+from django.utils.unittest import skipIf
+
+
+class SettingGetter(object):
+ def __init__(self):
+ self.test = getattr(settings, 'TEST', 'undefined')
+
+testvalue = None
+
+def signal_callback(sender, setting, value, **kwargs):
+ global testvalue
+ testvalue = value
+
+signals.setting_changed.connect(signal_callback, sender='TEST')
class SettingsTests(TestCase):
@@ -29,6 +44,43 @@ def test_override_doesnt_leak(self):
settings.TEST = 'test'
self.assertRaises(AttributeError, getattr, settings, 'TEST')
+ @override_settings(TEST='override')
+ def test_decorator(self):
+ self.assertEqual('override', settings.TEST)
+
+ def test_context_manager(self):
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+ override = override_settings(TEST='override')
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+ override.enable()
+ self.assertEqual('override', settings.TEST)
+ override.disable()
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+ def test_class_decorator(self):
+ self.assertEqual(SettingGetter().test, 'undefined')
+ DecoratedSettingGetter = override_settings(TEST='override')(SettingGetter)
+ self.assertEqual(DecoratedSettingGetter().test, 'override')
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+ @skipIf(sys.version_info[:2] < (2, 6), "Python version is lower than 2.6")
+ def test_new_class_decorator(self):
+ self.assertEqual(SettingGetter().test, 'undefined')
+ @override_settings(TEST='override')
+ class DecoratedSettingGetter(SettingGetter):
+ pass
+ self.assertEqual(DecoratedSettingGetter().test, 'override')
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+ def test_signal_callback_context_manager(self):
+ self.assertRaises(AttributeError, getattr, settings, 'TEST')
+ with self.settings(TEST='override'):
+ self.assertEqual(testvalue, 'override')
+
+ @override_settings(TEST='override')
+ def test_signal_callback_decorator(self):
+ self.assertEqual(testvalue, 'override')
+
#
# Regression tests for #10130: deleting settings.
#

0 comments on commit a3a53e0

Please sign in to comment.
Something went wrong with that request. Please try again.