Como testar a unidade com diferentes configurações no Django?

116

Existe algum mecanismo simples para substituir as configurações do Django para um teste de unidade? Tenho um gerente em um de meus modelos que retorna um número específico dos objetos mais recentes. O número de objetos que ele retorna é definido por uma configuração NUM_LATEST.

Isso tem o potencial de fazer meus testes falharem se alguém alterar a configuração. Como posso substituir as configurações setUp()e, posteriormente, restaurá-las tearDown()? Se isso não for possível, há alguma maneira de fazer o monkey patch do método ou simular as configurações?

EDIT: Aqui está meu código de gerente:

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]

O gerente usa settings.NEWS_LATEST_MAXpara fatiar o queryset. O getattr()é usado simplesmente para fornecer um padrão caso a configuração não exista.

Soviut
fonte
@Anto - você pode explicar por que ou fornecer uma resposta melhor?
usuário
Entretanto, mudou; o primeiro aceito foi este ;)
Anto

Respostas:

163

EDITAR: Esta resposta se aplica se você deseja alterar as configurações de um pequeno número de testes específicos .

Desde Django 1.4, existem maneiras de substituir as configurações durante os testes: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase terá um gerenciador de contexto self.settings e também um decorador @override_settings que pode ser aplicado a um método de teste ou a uma subclasse TestCase inteira.

Esses recursos ainda não existiam no Django 1.3.

Se você quiser alterar as configurações de todos os seus testes, deverá criar um arquivo de configurações separado para teste, que pode carregar e substituir as configurações do seu arquivo de configurações principal. Existem várias boas abordagens para isso nas outras respostas; Tenho visto variações bem-sucedidas nas abordagens de hspander e dmitrii .

slinkp
fonte
4
Eu diria que esta é a melhor maneira de fazer isso agora no Django 1.4+
Michael Mior
Como você acessa posteriormente essa configuração de dentro dos testes? O melhor que encontrei é algo assim self.settings().wrapped.MEDIA_ROOT, mas isso é terrível.
mlissner
2
Versões mais recentes do Django têm um gerenciador de contexto específico para isso: docs.djangoproject.com/en/1.8/topics/testing/tools/…
Akhorus
Meu favorito: @modify_settings(MIDDLEWARE_CLASSES=...(obrigado por esta resposta)
guettli
44

Você pode fazer o que quiser com a UnitTestsubclasse, incluindo definir e ler as propriedades da instância:

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

Como os casos de teste do django são executados em um único thread, estou curioso para saber o que mais pode estar modificando o valor NUM_LATEST? Se esse "algo mais" for acionado por sua rotina de teste, não tenho certeza se qualquer quantidade de patching do macaco salvará o teste sem invalidar a veracidade dos próprios testes.

Jarret Hardie
fonte
Seu exemplo funcionou. Isso foi revelador em termos do escopo do teste de unidade e como as configurações no arquivo de teste se propagam pela pilha de chamadas.
Soviut
Isso não funciona com settings.TEMPLATE_LOADERS... Então esse não é o jeito geral pelo menos, as configurações ou o Django não é recarregado ou qualquer coisa com esse truque.
Ciantic
1
este é um bom exemplo para a versão Django anterior à 1.4. Para> = 1,4 resposta stackoverflow.com/a/6415129/190127 mais correto
Oduvan
Use docs.djangoproject.com/en/dev/topics/testing/tools/… Corrigir com setUp e tearDown assim é uma ótima maneira de fazer testes realmente frágeis que são mais prolixos do que precisam ser. Se você precisar remendar algo assim, use algo como o flexmock.
fuzzy-waffle
"Uma vez que os casos de teste do django rodam single-threaded": o que não é mais o caso no Django 1.9.
Wtower
22

Embora a configuração das configurações de substituição no tempo de execução possa ajudar, na minha opinião, você deve criar um arquivo separado para teste. Isso economiza muitas configurações para teste e garantiria que você nunca faria algo irreversível (como limpar o banco de dados de teste).

Digamos que seu arquivo de teste exista em 'my_project / test_settings.py', adicione

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

em seu manage.py. Isso garantirá que, ao executar, python manage.py testuse apenas test_settings. Se você estiver usando algum outro cliente de teste, como o pytest, pode facilmente adicioná-lo ao pytest.ini

Hspandher
fonte
2
Acho que esta é uma boa solução para mim. Tenho muitos testes e códigos que usam cache. Será difícil para mim substituir as configurações uma por uma. Vou criar dois arquivos de configuração e determinar qual deles usar. A resposta do MicroPyramid também está disponível, mas será perigoso se eu esquecer de adicionar os parâmetros de configuração uma vez.
ramwin
22

Você pode passar na --settingsopção ao executar testes

python manage.py test --settings=mysite.settings_local
MicroPyramid
fonte
parou para encontrar aplicativos localizados em settings.dev, que é uma extensão de settings.base
holms
4
Acho que será perigoso se alguém se esquecer de adicionar os parâmetros de configuração uma vez.
ramwin
20

Atualização : a solução abaixo só é necessária no Django 1.3.x e anteriores. Para> 1,4, veja a resposta de slinkp .

Se você alterar as configurações com frequência em seus testes e usar Python ≥2,5, isso também é útil:

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)

Então você pode fazer:

with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
    do_my_tests()
Akaihola
fonte
Esta é uma solução muito legal. Por algum motivo, minhas configurações não estavam funcionando corretamente nos testes de unidade. Solução muito elegante, obrigado por compartilhar.
Tomas
Estou usando este código, mas tive problemas com falhas de teste em cascata, porque as configurações não seriam revertidas se o teste em questão falhasse. Para resolver isso, adicionei um try / finally em torno da yieldinstrução, com a parte final da função contida no finallybloco, para que as configurações sejam sempre revertidas.
Dustin Rasener
Vou editar a resposta para a posteridade. Espero estar fazendo certo! :)
Dustin Rasener
11

@override_settings é ótimo se você não tiver muitas diferenças entre as configurações do ambiente de produção e teste.

Em outro caso, é melhor você apenas ter arquivos de configurações diferentes. Neste caso, seu projeto será semelhante a este:

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

Portanto, você precisa ter a maior parte de suas configurações em base.pye, em seguida, em outros arquivos, você precisa importar tudo de lá e substituir algumas opções. Esta test.pyserá a aparência do seu arquivo:

from .base import *

DEBUG = False

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

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

LOGGING = {}

E então você precisa especificar a --settingsopção como na resposta @MicroPyramid ou especificar DJANGO_SETTINGS_MODULEa variável de ambiente e então você pode executar seus testes:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 
Dmitrii Mikhailov
fonte
Olá . Dmitrii, obrigado pela sua resposta, estou tendo o mesmo caso com esta resposta, mas gostaria de obter mais orientações sobre como o aplicativo saberá, o ambiente em que estamos (teste ou produção) , dê uma olhada no meu branch, confira meu repo github.com/andela/ah-backend-iroquois/tree/develop/authors , por exemplo , como vou lidar com essa lógica?
Lutaaya Huzaifah Idris
Porque eu uso nosetests para executar testes, agora como isso será executado ?, no ambiente de teste, não no ambiente de desenvolvimento
Lutaaya Huzaifah Idris
3

Encontrei isto ao tentar corrigir alguns doctests ... Para completar, quero mencionar que se você for modificar as configurações ao usar doctests, você deve fazer isso antes de importar qualquer outra coisa ...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

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

Para usuários pytest .

O maior problema é:

  • override_settings não funciona com pytest.
  • Subclassificar Django TestCasefará com que funcione, mas você não pode usar fixtures pytest.

A solução é usar o settingsacessório documentado aqui .

Exemplo

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

E no caso de você precisar atualizar vários campos

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)
Pithikos
fonte
3

Você pode substituir a configuração mesmo para uma única função de teste.

from django.test import TestCase, override_settings

class SomeTestCase(TestCase):

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

ou você pode substituir a configuração de cada função na classe.

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

    def test_some_function():
        
shivansh
fonte
1

Estou usando o pytest.

Consegui resolver isso da seguinte maneira:

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
Brontes
fonte
1

Você pode substituir as configurações no teste desta maneira:

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

E se você precisar dessas mesmas configurações em outro arquivo, basta importar diretamente test_settings.

giantas
fonte
0

Se você tiver vários arquivos de teste colocados em um subdiretório (pacote python), você pode substituir as configurações de todos esses arquivos com base na condição de presença da string de 'teste' em 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)

Não é a melhor abordagem. Usei-o para mudar o corretor de Celery de Redis para Memória.

Ledorub
fonte
0

Eu criei um novo arquivo settings_test.py que importaria tudo do arquivo settings.py e modificaria o que fosse diferente para fins de teste. No meu caso, eu queria usar um intervalo de armazenamento em nuvem diferente ao testar. insira a descrição da imagem aqui

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