¿Cómo realizar una prueba unitaria con diferentes configuraciones en Django?

116

¿Existe algún mecanismo simple para anular la configuración de Django para una prueba unitaria? Tengo un administrador en uno de mis modelos que devuelve un número específico de los últimos objetos. El número de objetos que devuelve se define mediante una configuración NUM_LATEST.

Esto tiene el potencial de hacer que mis pruebas fallen si alguien cambia la configuración. ¿Cómo puedo anular la configuración setUp()y luego restaurarla tearDown()? Si eso no es posible, ¿hay alguna forma de que pueda modificar el método o simular la configuración?

EDITAR: Aquí está mi código de administrador:

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]

El administrador utiliza settings.NEWS_LATEST_MAXpara dividir el conjunto de consultas. Se getattr()utiliza simplemente para proporcionar un valor predeterminado en caso de que la configuración no exista.

Soviut
fuente
@Anto: ¿puedes explicar por qué o dar una mejor respuesta?
usuario
Mientras tanto, cambió; el primero aceptado fue este ;)
Anto

Respuestas:

163

EDITAR: esta respuesta se aplica si desea cambiar la configuración de una pequeña cantidad de pruebas específicas .

Desde Django 1.4, hay formas de anular la configuración durante las pruebas: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase tendrá un administrador de contexto self.settings, y también habrá un decorador @override_settings que se puede aplicar a un método de prueba o una subclase completa de TestCase.

Estas características aún no existían en Django 1.3.

Si desea cambiar la configuración de todas sus pruebas, querrá crear un archivo de configuración separado para la prueba, que puede cargar y anular la configuración de su archivo de configuración principal. Hay varios buenos enfoques para esto en las otras respuestas; He visto variaciones exitosas en los enfoques de hspander y dmitrii .

deslizarse
fuente
4
Diría que esta es la mejor manera de hacer esto ahora en Django 1.4+
Michael Mior
¿Cómo accedes más tarde a esa configuración desde las pruebas? Lo mejor que he encontrado es algo así self.settings().wrapped.MEDIA_ROOT, pero eso es bastante terrible.
mlissner
2
Las versiones más nuevas de Django tienen un administrador de contexto específico para esto: docs.djangoproject.com/en/1.8/topics/testing/tools/…
Akhorus
Mi favorito: @modify_settings(MIDDLEWARE_CLASSES=...(gracias por esta respuesta)
guettli
44

Puede hacer lo que quiera con la UnitTestsubclase, incluida la configuración y lectura de las propiedades de la instancia:

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

Sin embargo, dado que los casos de prueba de django se ejecutan en un solo subproceso, tengo curiosidad por saber qué más puede estar modificando el valor NUM_LATEST. Si esa "otra cosa" es activada por su rutina de prueba, entonces no estoy seguro de que cualquier cantidad de parches de mono salvará la prueba sin invalidar la veracidad de las pruebas en sí.

Jarret Hardie
fuente
Tu ejemplo funcionó. Esto ha sido una revelación en términos del alcance de las pruebas unitarias y cómo la configuración en el archivo de pruebas se propaga a través de la pila de llamadas.
Soviut
Esto no funciona con settings.TEMPLATE_LOADERS... Entonces esta no es una forma general al menos, la configuración o Django no se recarga ni nada con este truco.
Ciantic
1
este es un buen ejemplo para la versión Django anterior a la 1.4. Para> = 1.4 respuesta stackoverflow.com/a/6415129/190127 más correcta
Oduvan
Use docs.djangoproject.com/en/dev/topics/testing/tools/… Parchear con setUp y tearDown de esta manera es una excelente manera de hacer pruebas realmente frágiles que son más detalladas de lo necesario. Si necesita parchear algo como esto, use algo como flexmock.
fuzzy-waffle
"Dado que los casos de prueba de django se ejecutan en un solo subproceso": que ya no es el caso en Django 1.9.
Wtower
22

Aunque anular la configuración de la configuración en tiempo de ejecución puede ayudar, en mi opinión, debería crear un archivo separado para probar. Esto ahorra mucha configuración para las pruebas y esto aseguraría que nunca termine haciendo algo irreversible (como limpiar la base de datos provisional).

Digamos que su archivo de prueba existe en 'my_project / test_settings.py', agregue

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

en su manage.py. Esto asegurará que cuando ejecute python manage.py testuse test_settings solamente. Si está utilizando algún otro cliente de prueba como pytest, podría agregarlo fácilmente a pytest.ini

hspandher
fuente
2
Creo que esta es una buena solución para mí. Tengo demasiadas pruebas y código que usa caché. Será difícil para mí anular los ajustes uno por uno. Crearé dos archivos de configuración y determinaré cuál usar. La respuesta de MicroPyramid también está disponible, pero será peligroso si me olvido de agregar los parámetros de configuración una vez.
ramwin
22

Puede pasar la --settingsopción al realizar pruebas

python manage.py test --settings=mysite.settings_local
MicroPirámide
fuente
se detuvo para encontrar aplicaciones que se encuentran en settings.dev, que es una extensión de settings.base
holms
4
Creo que será peligroso si alguien se olvida de agregar los parámetros de configuración una vez.
ramwin
20

Actualización : la siguiente solución solo es necesaria en Django 1.3.xy versiones anteriores. Para> 1.4, vea la respuesta de slinkp .

Si cambia la configuración con frecuencia en sus pruebas y usa Python ≥2.5, esto también es ú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)

Entonces puedes hacer:

with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
    do_my_tests()
akaihola
fuente
Esta es una solución realmente genial. Por alguna razón, mi configuración no funcionaba correctamente en las pruebas unitarias. Solución muy elegante, gracias por compartir.
Tomas
Estoy usando este código, pero tuve problemas con las fallas de prueba en cascada, porque la configuración no se revertiría si la prueba en cuestión fallaba. Para abordar esto, agregué un try / finalmente alrededor de la yielddeclaración, con la parte final de la función contenida en el finallybloque, para que la configuración siempre se revierte.
Dustin Rasener
Editaré la respuesta para la posteridad. ¡Espero estar haciendo esto bien! :)
Dustin Rasener
11

@override_settings es excelente si no tiene muchas diferencias entre las configuraciones de su entorno de producción y de prueba.

En otro caso, será mejor que tenga diferentes archivos de configuración. En este caso, su proyecto se verá así:

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

Por lo tanto, debe tener la mayor parte de su configuración base.pyy luego en otros archivos, debe importar todo desde allí y anular algunas opciones. Así es como test.pyse verá su archivo:

from .base import *

DEBUG = False

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

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

LOGGING = {}

Y luego debe especificar la --settingsopción como en la respuesta de @MicroPyramid, o especificar DJANGO_SETTINGS_MODULEla variable de entorno y luego puede ejecutar sus pruebas:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 
Dmitrii Mikhailov
fuente
Hola . Dmitrii, gracias por su respuesta, estoy teniendo el mismo caso con esta respuesta, pero me gustaría obtener más orientación sobre cómo la aplicación sabrá, el entorno en el que estamos (pruebas o producción) , eche un vistazo a mi sucursal, consulte mi repositorio github.com/andela/ah-backend-iroquois/tree/develop/authors , como ¿cómo manejaré esa lógica?
Lutaaya Huzaifah Idris
Debido a que uso nosetests para ejecutar pruebas, ahora, ¿cómo se ejecutará? En el entorno de prueba, no en el entorno de desarrollo
Lutaaya Huzaifah Idris
3

Encontré esto mientras intentaba arreglar algunas pruebas de documentación ... Para completar, quiero mencionar que si va a modificar la configuración al usar pruebas de documentación, debe hacerlo antes de importar cualquier otra cosa ...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

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

Para usuarios de Pytest .

El mayor problema es:

  • override_settings no funciona con pytest.
  • Subclasificar Django TestCaselo hará funcionar, pero luego no puede usar los accesorios de pytest.

La solución es utilizar el settingsaccesorio documentado aquí .

Ejemplo

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

Y en caso de que necesite actualizar varios 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
fuente
3

Puede anular la configuración incluso para una única función de prueba.

from django.test import TestCase, override_settings

class SomeTestCase(TestCase):

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

o puede anular la configuración de cada función en la clase.

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

    def test_some_function():
        
shivansh
fuente
1

Estoy usando pytest.

Logré resolver esto de la siguiente manera:

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
fuente
1

Puede anular la configuración en la prueba de esta manera:

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

Y si necesita estas mismas configuraciones en otro archivo, puede importarlas directamente test_settings.

gigantes
fuente
0

Si tiene varios archivos de prueba colocados en un subdirectorio (paquete de Python), puede anular la configuración de todos estos archivos en función de la condición de presencia de la cadena de 'prueba' en 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)

No es el mejor enfoque. Lo usé para cambiar el corredor de apio de Redis a Memory.

Ledorub
fuente
0

Creé un nuevo archivo settings_test.py que importaría todo del archivo settings.py y modificaría lo que sea diferente para fines de prueba. En mi caso, quería usar un depósito de almacenamiento en la nube diferente al realizar la prueba. ingrese la descripción de la imagen aquí

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
fuente