How to Unit test with different settings in Django

2019-01-06 10:45发布

问题:

Is there any simple mechanism for overriding Django settings for a unit test? I have a manager on one of my models that returns a specific number of the latest objects. The number of objects it returns is defined by a NUM_LATEST setting.

This has the potential to make my tests fail if someone were to change the setting. How can I override the settings on setUp() and subsequently restore them on tearDown()? If that isn't possible, is there some way I can monkey patch the method or mock the settings?

EDIT: Here is my manager code:

class LatestManager(models.Manager):
    """
    Returns a specific number of the most recent public Articles as defined by 
    the NEWS_LATEST_MAX setting.
    """
    def get_query_set(self):
        num_latest = getattr(settings, 'NEWS_NUM_LATEST', 10)
        return super(LatestManager, self).get_query_set().filter(is_public=True)[:num_latest]

The manager uses settings.NEWS_LATEST_MAX to slice the queryset. The getattr() is simply used to provide a default should the setting not exist.

回答1:

EDIT: This answer applies if you want to change settings for a small number of specific tests.

Since Django 1.4, there are ways to override settings during tests: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase will have a self.settings context manager, and there will also be an @override_settings decorator that can be applied to either a test method or a whole TestCase subclass.

These features did not exist yet in Django 1.3.

If you want to change settings for all your tests, you'll want to create a separate settings file for test, which can load and override settings from your main settings file. There are several good approaches to this in the other answers; I have seen successful variations on both hspander's and dmitrii's approaches.



回答2:

You can do anything you like to the UnitTest subclass, including setting and reading instance properties:

from django.conf import settings

class MyTest(unittest.TestCase):
   def setUp(self):
       self.old_setting = settings.NUM_LATEST
       settings.NUM_LATEST = 5 # value tested against in the TestCase

   def tearDown(self):
       settings.NUM_LATEST = self.old_setting

Since the django test cases run single-threaded, however, I'm curious about what else may be modifying the NUM_LATEST value? If that "something else" is triggered by your test routine, then I'm not sure any amount of monkey patching will save the test without invalidating the veracity of the tests itself.



回答3:

Update: the solution below is only needed on Django 1.3.x and earlier. For >1.4 see slinkp's answer.

If you change settings frequently in your tests and use Python ≥2.5, this is also handy:

from contextlib import contextmanager

class SettingDoesNotExist:
    pass

@contextmanager
def patch_settings(**kwargs):
    from django.conf import settings
    old_settings = []
    for key, new_value in kwargs.items():
        old_value = getattr(settings, key, SettingDoesNotExist)
        old_settings.append((key, old_value))
        setattr(settings, key, new_value)
    yield
    for key, old_value in old_settings:
        if old_value is SettingDoesNotExist:
            delattr(settings, key)
        else:
            setattr(settings, key, old_value)

Then you can do:

with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
    do_my_tests()


回答4:

Although overriding settings configuration on runtime might help, in my opinion you should create a separate file for testing. This saves lot of configuration for testing and this would ensure that you never end up doing something irreversible (like cleaning staging database).

Say your testing file exists in 'my_project/test_settings.py', add

settings = 'my_project.test_settings' if 'test' in sys.argv else 'my_project.settings'

in your manage.py. This will ensure that when you run python manage.py test you use test_settings only. If you are using some other testing client like pytest, you could as easily add this to pytest.ini



回答5:

You can pass --settings option when running tests

python manage.py test --settings=mysite.settings_local


回答6:

@override_settings is great if you don't have many differences between your production and testing environment configurations.

In other case you'd better just have different settings files. In this case your project will look like this:

your_project
    your_app
        ...
    settings
        __init__.py
        base.py
        dev.py
        test.py
        production.py
    manage.py

So you need to have your most of your settings in base.py and then in other files you need to import all everything from there, and override some options. Here's what your test.py file will look like:

from .base import *

DEBUG = False

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'app_db_test'
    }
}

PASSWORD_HASHERS = (
    'django.contrib.auth.hashers.MD5PasswordHasher',
)

LOGGING = {}

And then you either need to specify --settings option as in @MicroPyramid answer, or specify DJANGO_SETTINGS_MODULE environment variable and then you can run your tests:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 


回答7:

Found this while trying to fix some doctests... For completeness I want to mention that if you're going to modify the settings when using doctests, you should do it before importing anything else...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

>>> # Your other imports
>>> from django.core.paginator import Paginator
>>> # etc


回答8:

I'm using pytest.

I managed to solve this the following way:

import django    
import app.setting
import modules.that.use.setting

# do some stuff with default setting
setting.VALUE = "some value"
django.setup()
import importlib
importlib.reload(app.settings)
importlib.reload(modules.that.use.setting)
# do some stuff with settings new value