Fixed #15561 -- Extended test setting override code added in r16165 with a decorator and a signal for setting changes.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16237 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-05-18 12:08:53 +00:00
parent 091c9b530e
commit a3a53e0b73
7 changed files with 216 additions and 69 deletions

View File

@ -1,3 +1,5 @@
from django.dispatch import Signal
template_rendered = Signal(providing_args=["template", "context"])
setting_changed = Signal(providing_args=["setting", "value"])

View File

@ -2,7 +2,6 @@ from __future__ import with_statement
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.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
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 @@ class TransactionTestCase(ut2.TestCase):
"""
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 File

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

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

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

@ -9,48 +9,14 @@ from StringIO import StringIO
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 @@ class MailTests(TestCase):
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 @@ class BaseEmailBackendTests(object):
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 @@ class BaseEmailBackendTests(object):
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 @@ class BaseEmailBackendTests(object):
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 @@ class BaseEmailBackendTests(object):
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 File

@ -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 @@ class SettingsTests(TestCase):
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.
#