Как провести модульное тестирование с разными настройками в Django?


117

Есть ли простой механизм для переопределения настроек 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

Ответы:


164

РЕДАКТИРОВАТЬ: этот ответ применим, если вы хотите изменить настройки для небольшого количества конкретных тестов.

Начиная с Django 1.4, есть способы переопределить настройки во время тестов: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase будет иметь диспетчер контекста self.settings, а также декоратор @override_settings, который можно применить либо к методу тестирования, либо ко всему подклассу TestCase.

Этих функций еще не было в Django 1.3.

Если вы хотите изменить настройки для всех ваших тестов, вам нужно создать отдельный файл настроек для теста, который может загружать и отменять настройки из вашего основного файла настроек. В других ответах есть несколько хороших подходов к этому; Я видел успешные варианты подходов как hspander, так и dmitrii .


4
Я бы сказал, что это лучший способ сделать это сейчас в Django 1.4+
Майкл Майор

Как вы позже получите доступ к этой настройке из тестов? Лучшее, что я нашел, это что-то вроде self.settings().wrapped.MEDIA_ROOT, но это довольно ужасно.
mlissner

2
В новых версиях Django для этого есть специальный диспетчер контекста: docs.djangoproject.com/en/1.8/topics/testing/tools/…
Akhorus

Мой фаворит: @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? Если это «что-то еще» вызвано вашей программой тестирования, то я не уверен, что любое количество обезьяньих исправлений спасет тест, не сделав недействительной достоверность самих тестов.


Ваш пример сработал. Это стало откровением с точки зрения объема модульного тестирования и того, как настройки в файле тестов распространяются вниз по стеку вызовов.
Soviut

Это не работает с settings.TEMPLATE_LOADERS... Так что это не общий способ, по крайней мере, настройки или Django не перезагружаются или что-то еще с этим трюком.
Ciantic

1
это хороший пример для версии Django старше 1.4. Для> = 1,4 ответьте stackoverflow.com/a/6415129/190127 более правильно
Одуван, 09

Используйте docs.djangoproject.com/en/dev/topics/testing/tools/… Исправление с помощью setUp и tearDown, как это, - отличный способ сделать действительно хрупкие тесты, которые более подробны, чем они должны быть. Если вам нужно исправить что-то подобное, используйте что-то вроде flexmock.
fuzzy-waffle

«Поскольку тестовые примеры 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.base
Holms

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

Я использую этот код, но у меня были проблемы с каскадными сбоями тестов, потому что настройки не восстанавливались, если рассматриваемый тест не прошел. Чтобы решить эту проблему, я добавил try / finally вокруг yieldоператора с последней частью функции, содержащейся в finallyблоке, так что настройки всегда возвращаются.
Dustin Rasener

Отредактирую ответ для потомков. Надеюсь, я все делаю правильно! :)
Dustin Rasener

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 

Привет . Дмитрий, спасибо за ваш ответ, у меня тот же случай с этим ответом, но я хотел бы получить больше рекомендаций о том, как приложение будет знать, в какой среде мы находимся (тестирование или производство) , посмотрите мою ветку, проверьте мое репо github.com/andela/ah-backend-iroquois/tree/develop/authors , например, как я буду обрабатывать эту логику?
Lutaaya Huzaifah Idris

Поскольку я использую носовые тесты для запуска тестов, как теперь это будет выполняться? В среде тестирования, а не в среде разработки
Лутаая Хузайфа Идрис

3

Обнаружил это при попытке исправить некоторые доктесты ... Для полноты картины я хочу упомянуть, что если вы собираетесь изменить настройки при использовании доктестов, вам следует сделать это перед импортом чего-либо еще ...

>>> 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)

3

Вы можете изменить настройку даже для одной тестовой функции.

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():
        

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.


0

Если у вас есть несколько тестовых файлов, помещенных в подкаталог (пакет python), вы можете переопределить настройки для всех этих файлов на основе условия наличия строки test в sys.argv

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)
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.