如何在Django中使用不同的设置进行单元测试?


116

是否有任何简单的机制可以覆盖Django单元测试的设置?我在一个模型上有一个经理,该经理返回特定数量的最新对象。它返回的对象数由NUM_LATEST设置定义。

如果有人更改设置,这可能会使我的测试失败。如何覆盖设置setUp()并随后恢复设置tearDown()?如果不可能,是否可以通过某种方式猴子修补方法或模拟设置?

编辑:这是我的经理代码:

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]

管理器用于settings.NEWS_LATEST_MAX切片查询集。该getattr()只是用来提供一个默认的应该设置不存在。


@Anto-您能解释原因还是提供更好的答案?
用户

与此同时,它发生了变化。前一个被接受的就是这个;)
Anto 2015年

Answers:


163

编辑:如果你想改变设置了这样的回答适用于数量的特定测试。

从Django 1.4开始,有一些方法可以在测试期间覆盖设置:https : //docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase将具有一个self.settings上下文管理器,并且还将有一个@override_settings装饰器,可将其应用于测试方法或整个TestCase子类。

这些功能在Django 1.3中尚不存在。

如果要更改所有测试的设置,则需要创建一个单独的测试设置文件,该文件可以加载和覆盖主设置文件中的设置。在其他答案中,有几种不错的方法;我看到的这两个成功的变化hspander的德米特里的方法。


4
我想说这是现在在Django 1.4+中实现此目的的最佳方法
Michael

您以后如何从测试中访问该设置?我发现的最好的东西是self.settings().wrapped.MEDIA_ROOT,但这真是太糟糕了。
mlissner 2014年

2
较新版本的Django为此提供了一个特定的上下文管理器:docs.djangoproject.com/en/1.8/topics/testing/tools/…– Akhorus
2015年

我最喜欢的:(@modify_settings(MIDDLEWARE_CLASSES=...感谢您的回答)
guettli

44

您可以UnitTest对子类执行任何您喜欢的事情,包括设置和读取实例属性:

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

由于django测试用例运行单线程,但是,我很好奇其他可能会修改NUM_LATEST值的问题?如果您的测试例程触发了“其他”事件,那么我不确定在不影响测试本身准确性的情况下,进行任何数量的猴子修补都可以保存测试。


您的示例成功了。在单元测试的范围以及测试文件中的设置如何通过调用堆栈向下传播方面,这令人大开眼界。
苏联

这不适用于settings.TEMPLATE_LOADERS...因此,至少这不是通用的方式,设置或Django无法重新加载,或者没有任何技巧。
Ciantic

1
对于早于1.4版本的Django,这是一个很好的示例。对于> = 1.4,请更正确stackoverflow.com/a/6415129/190127
Oduvan 2013年

使用docs.djangoproject.com/en/dev/topics/testing/tools/… 使用setUp和tearDown进行修补,这是进行真正脆弱的测试的好方法,该测试比实际需要的还要冗长。如果您需要打补丁,请使用flexmock。
2014年

“由于django测试用例运行单线程”:在Django 1.9中不再是这种情况。
Wtower

22

尽管在运行时重写设置配置可能会有所帮助,但我认为您应该创建一个单独的文件进行测试。这样可以节省大量测试配置,并且可以确保您永远不会做不可逆的事情(例如清理登台数据库)。

假设您的测试文件位于“ my_project / test_settings.py”中,请添加

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

在您的manage.py中。这样可以确保在运行时python manage.py test仅使用test_settings。如果您正在使用其他测试客户端(例如pytest),则可以轻松地将其添加到pytest.ini中


2
我认为这对我来说是一个很好的解决方案。我有太多使用缓存的测试和代码。对于我来说,一一覆盖设置将很困难。我将创建两个配置文件并确定要使用哪个。MicroPyramid的答案也可用,但是如果我忘记一次添加设置参数,那将很危险。
ramwin

22

您可以--settings在运行测试时通过选项

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

它停下来找到位于settings.dev中的应用程序,而settings.dev是settings.base的扩展名
holms 18-02-26

4
如果有人忘记一次添加设置参数,我认为这将很危险。
ramwin

20

更新:以下解决方案仅在Django 1.3.x及更早版本上才需要。对于> 1.4,请参见slinkp的答案

如果您在测试中经常更改设置并使用Python≥2.5,这也很方便:

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)

然后,您可以执行以下操作:

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

这真的是很酷的解决方案。由于某些原因,我的设置在单元测试中无法正常工作。非常优雅的解决方案,谢谢分享。
Tomas 2012年

我正在使用此代码,但是我遇到级联测试失败的问题,因为如果相关测试失败,设置将不会恢复。为了解决这个问题,我在该yield语句周围添加了一个try / final方法,该函数的最后部分包含在该finally块中,以便始终还原设置。
达斯汀·拉森纳

我将为后代编辑答案。我希望我做对了!:)
达斯汀·拉森纳

11

@override_settings 如果您的生产环境和测试环境配置之间没有太多差异,那将是很好的选择。

在其他情况下,最好只使用不同的设置文件。在这种情况下,您的项目将如下所示:

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

因此,您需要拥有大部分设置base.py,然后在其他文件中需要从那里导入所有内容,并覆盖某些选项。您的test.py文件如下所示:

from .base import *

DEBUG = False

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

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

LOGGING = {}

然后,您需要--settings像@MicroPyramid答案中那样指定选项,或者指定DJANGO_SETTINGS_MODULE环境变量,然后就可以运行测试:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 

你好 。德米特里(Dmitrii),谢谢你的回答,这个答案与我的答案相同。但是,我想获得有关应用程序如何知道,我们所处的环境(测试或生产中)的更多指导,请查看我的分支机构,然后进行结帐。我的仓库github.com/andela/ah-backend-iroquois/tree/develop/authors,例如我将如何处理该逻辑?
Lutaaya Huzaifah Idris

因为我使用鼻子测试运行测试,现在将如何运行它,在测试环境中而不在开发环境中运行
Lutaaya Huzaifah Idris 18/08/30

3

在尝试修复某些doctest时发现了这一点...为了完整起见,我想提一下,如果您要在使用doctest时修改设置,则应在导入其他任何东西之前先进行设置...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

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

3

对于pytest用户。

最大的问题是:

  • override_settings 不适用于pytest。
  • 子类化Django TestCase可以使其工作,但是您不能使用pytest固定装置。

解决方案是使用此处settings记录的灯具。

def test_with_specific_settings(settings):
    settings.DEBUG = False
    settings.MIDDLEWARE = []
    ..

如果您需要更新多个字段

def override_settings(settings, kwargs):
    for k, v in kwargs.items():
        setattr(settings, k, v)


new_settings = dict(
    DEBUG=True,
    INSTALLED_APPS=[],
)


def test_with_specific_settings(settings):
    override_settings(settings, new_settings)

1

我正在使用pytest。

我设法通过以下方式解决此问题:

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

1

您可以通过以下方式覆盖测试中的设置:

from django.test import TestCase, override_settings

test_settings = override_settings(
    DEFAULT_FILE_STORAGE='django.core.files.storage.FileSystemStorage',
    PASSWORD_HASHERS=(
        'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    )
)


@test_settings
class SomeTestCase(TestCase):
    """Your test cases in this class"""

如果您在另一个文件中需要这些相同的设置,则可以直接导入test_settings


1

您甚至可以覆盖单个测试功能的设置。

from django.test import TestCase, override_settings

class SomeTestCase(TestCase):

    @override_settings(SOME_SETTING="some_value")
    def test_some_function():
        

或者您可以覆盖类中每个函数的设置。

@override_settings(SOME_SETTING="some_value")
class SomeTestCase(TestCase):

    def test_some_function():
        

0

如果您在子目录(python软件包)中放置了多个测试文件,则可以基于sys.argv中'test'字符串的存在条件来覆盖所有这些文件的设置。

app
  tests
    __init__.py
    test_forms.py
    test_models.py

__init__.py:

import sys
from project import settings

if 'test' in sys.argv:
    NEW_SETTINGS = {
        'setting_name': value,
        'another_setting_name': another_value
    }
    settings.__dict__.update(NEW_SETTINGS)

不是最好的方法。用它将Celery代理从Redis更改为Memory。


0

我创建了一个新的settings_test.py文件,该文件将从settings.py文件中导入所有内容,并为测试目的修改任何其他内容。就我而言,我想在测试时使用其他云存储桶。 在此处输入图片说明

settings_test.py:

from project1.settings import *
import os

CLOUD_STORAGE_BUCKET = 'bucket_name_for_testing'

manage.py:

def main():

    # use seperate settings.py for tests
    if 'test' in sys.argv:
        print('using settings_test.py')
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.settings_test')
    else:
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.settings')

    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.