Russ and I discussed having a decorator/context manager for monkey
patching settings:
@swap_settings(TEMPLATE_DIRS=[fuzzy], DEBUG=True)
def test_who_cares(self):
self.assertTrue(settings.DEBUG)
or
with swap_settings(DEBUG=True):
self.assertTrue(settings.DEBUG)
Alex
--
"I disapprove of what you say, but I will defend to the death your
right to say it." -- Voltaire
"The people's good is the highest law." -- Cicero
"Code can always be simpler than you think, but never as simple as you
want" -- Me
Pedant: there's a small bug above which has bitten me before doing a similar thing - settings.AWESOME ends up set to None after the test has run if it didn't exist before.
> Anyways, I'd love to hear how others have dealt with this and any
> other possible solutions.
I've used Michael Foord's Mock library to patch a setting for the duration of a test case. Chris Withers' testfixtures library also has some sugar to provide a context manager approach, though I haven't used that in a little while.
Cheers,
Dan
--
Dan Fairs | dan....@gmail.com | www.fezconsulting.com
(This is dry code)
def with_settings(**overrides):
"""Allows you to define settings that are required for this
function to work"""
NotDefined = object()
def wrapped(func):
@wraps(func)
def _with_settings(*args, **kwargs):
_orig = {}
for k, v in overrides.iteritems():
_orig[k] = getattr(settings, k, NotDefined)
try:
func(*args, **kwargs)
finally:
for k, v in _orig.iteritems():
if v is NotDefined:
delattr(settings, k)
else:
setattr(settings, k, v)
return _with_settings
return wrapped
I'm not familiar with the context managers, but I imagine those would
solve things like adjusting CONTEXT_PROCESSORS.
Currently, I'm using a pair of save/restore functions: save() monkey
patches the settings module and returns a dictionary of old values,
restore() puts back the old values based on the dictionary. I usually
put this in setUp/tearDown so I don't have to repeat in every test. I
was about to propose that
Django's TestCase should do something similar by default.
Both the decorator and context processor are very useful, but having
something to set values for the whole test case instead of a single
test or a block of code would be great too. I was thinking about
something in line of:
class EmailTestCase(TestCase):
settings = dict(DEFAULT_FROM_EMAIL="webm...@example.com")
--
Łukasz Rekucki
Well, there's no reason the decorator couldn't be used as a class
decorator (on 2.6 and above). I'll admit that the settings attribute
on TestCase is more consistant with how we've handled other things
(urls, fixtures), however, for whatever reason I'm not a fan, as it
forces you to split up tests that should logically be grouped on a
single class.
Here's a working (we're now using it) version of the previous decorator:
def with_settings(**overrides):
"""Allows you to define settings that are required for this
function to work"""
NotDefined = object()
def wrapped(func):
@wraps(func)
def _with_settings(*args, **kwargs):
_orig = {}
for k, v in overrides.iteritems():
_orig[k] = getattr(settings, k, NotDefined)
setattr(settings, k, v)
try:
func(*args, **kwargs)
finally:
for k, v in _orig.iteritems():
if v is NotDefined:
delattr(settings, k)
else:
setattr(settings, k, v)
return _with_settings
return wrapped
--
David Cramer
http://www.davidcramer.net
What exactly do you mean by "decorating views" in this context ?
> On Thu, Nov 4, 2010 at 2:26 PM, Alex Gaynor <alex....@gmail.com> wrote:
>>
>> Well, there's no reason the decorator couldn't be used as a class
>> decorator (on 2.6 and above). I'll admit that the settings attribute
>> on TestCase is more consistant with how we've handled other things
>> (urls, fixtures), however, for whatever reason I'm not a fan, as it
>> forces you to split up tests that should logically be grouped on a
>> single class.
You can alter the setting both with a class attribute/decorator and
then alter it some more for a particular test. For example (excuse the
horrible naming):
@alter_class_settings(USE_L10N=True, OTHER_SETTING=False):
class MyTest(TestCase):
# 20 tests that rely on L10N
@alter_test_settings(USE_L10N=False)
def test_no_localization(self): pass
I meant the class-level decorator/attribute as a shortcut for a common
case. I agree that splitting tests just because of settings is bad,
but repeating the same decorator all over the test case is bad too,
imho.
Here[1] is an implementation of a context manager and a class
decorator based on the function decorator that David provided. Does it
look reasonable ?
[1]: https://gist.github.com/663367
--
Łukasz Rekucki
Love the idea in general. Django's own test suite is full of this
pattern (or buggy partial implementations of it), which is a prime
indication that we should providing something at the framework level
to make this easy to do.
The devil is in the detail. If you look at the ways Django uses this
pattern, there are lots of edge cases that need to be handled. For
example:
* Some settings are lists, and the test case needs to
append/prepend/insert into that existing list, rather than overwriting
a list. An example of this is adding an extra context processor for
test purposes. Yes, this could be handled by manually specifying the
full list, but it's a fairly common pattern, so it would be nice to
have a quick way to represent the pattern.
* Settings that are internally cached. For example, anything that
modifies INSTALLED_APPS.
* Settings that need to make call to reset state affected by loading
new new group of settings. For example, if you change TZ, you need to
call "time.tzset()" on teardown to reset the timezone. Similarly for
deactivating translations if you change USE_I18N.
* Settings that need to be removed, rather that set to None. Again,
TZ is an example here -- there is a difference between "TZ exists and
is None" and "TZ doesn't exist".
I've probably missed a couple of other edge cases; it would be worth
doing an audit of Django's test suite to see all the places that we've
used this pattern, and the workarounds we've had to use to clean up
after it.
There's also the matter of making this system easy to use in the
practical sense. The context manager approach that have been given so
far in this thread is a nice syntactic fits, but miss one big use case
-- modifying settings for *every* test in a TestCase. This is the way
that settings changes are used right now in Django's test suite. The
provided decorator has the same limitation, but it shouldn't be too
hard to modify it to be a class-or-function decorator.
Lastly, a persistent source of bugs in Django's own test suite is
having complete settings isolation. If you're doing true unit tests,
it's not enough to just replace one or two settings -- you have to
clear the decks to make sure that the user's settings file doesn't
provide an environment that breaks your test. This manifests itself as
tests for contrib apps that pass fine in Django's own suite, but fail
when an end user deployes the app -- for example, you deploy
contrib.auth in an environment that doesn't have the login URLs
deployed, and your tests fail; or you deploy contrib.flatpages but
don't deploy contrib.sites, and the tests fail.
Over time, we've cleaned up these issues as we've found them, but the
real fix is to make sure a test runs in a completely clean settings
environment. That is, we reset settings to a baseline
(global_settings.py would be a an obvious candidate) rather than just
tinkering with the one or two settings that we think are important.
The "Clear the decks" approach is a different use case to "just change
these three settings", but it's closely related, and worth tackling at
the same time, IMHO.
So - tl;dr. love the idea. However, we need to handle the edge cases
if it's going to be added to Django trunk, and replacing Django's own
usage of the ad-hoc pattern is as good a test as any that we've
tackled the edge cases.
Yours,
Russ Magee %-)
I think it's a little cavalier to see what we do is ad-hoc. Sure
there are a bunch of tests with:
def setUp(self):
self.old_SETTING = getattr(settings, "SETING", _missing)
def tearDown(self):
if self.old_SETTING is _missing:
del settings.SETTING"
else:
settings.SETTING = self.old_SETTING
but how else would you write that? That's the whole point of setUp
and tearDown, and I can't think of a more succinct formulation of that
that covers all of the cases (assign/reset attribute, append/insert,
set env variable, etc.).
Ad hoc is from the Latin "For this". Ad-hoc doens't mean the pattern
is bad or wrong, it just means it isn't generalized. We manually
reproduce variations of the pattern you describe every time we need
it, rather than having a generalized framework level tool for handling
settings. I think "ad hoc" is a pretty apt description of what
Django's test suite does.
Yours,
Russ Magee %-)
* Settings that are internally cached. For example, anything that
modifies INSTALLED_APPS.
* Settings that need to make call to reset state affected by loading
new new group of settings. For example, if you change TZ, you need to
call "time.tzset()" on teardown to reset the timezone. Similarly for
deactivating translations if you change USE_I18N.
* Settings that need to be removed, rather that set to None. Again,
TZ is an example here -- there is a difference between "TZ exists and
is None" and "TZ doesn't exist".