Comment effectuer un test unitaire avec différents paramètres dans Django?

116

Existe-t-il un mécanisme simple pour remplacer les paramètres Django pour un test unitaire? J'ai un gestionnaire sur l'un de mes modèles qui renvoie un nombre spécifique des derniers objets. Le nombre d'objets qu'il renvoie est défini par un paramètre NUM_LATEST.

Cela a le potentiel de faire échouer mes tests si quelqu'un change le paramètre. Comment puis-je remplacer les paramètres setUp()et les restaurer ultérieurement tearDown()? Si ce n'est pas possible, y a-t-il un moyen pour monkey patcher la méthode ou me moquer des paramètres?

EDIT: Voici mon code de gestionnaire:

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]

Le gestionnaire utilise settings.NEWS_LATEST_MAXpour découper l'ensemble de requêtes. Le getattr()est simplement utilisé pour fournir une valeur par défaut si le paramètre n'existe pas.

Soviut
la source
@Anto - pouvez-vous expliquer pourquoi ou fournir une meilleure réponse?
utilisateur
Cela a changé entre-temps; le premier accepté était celui-ci ;)
Anto

Réponses:

163

MODIFIER: Cette réponse s'applique si vous souhaitez modifier les paramètres d'un petit nombre de tests spécifiques .

Depuis Django 1.4, il existe des moyens de remplacer les paramètres lors des tests: https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings

TestCase aura un gestionnaire de contexte self.settings, et il y aura également un décorateur @override_settings qui peut être appliqué à une méthode de test ou à une sous-classe TestCase entière.

Ces fonctionnalités n'existaient pas encore dans Django 1.3.

Si vous souhaitez modifier les paramètres de tous vos tests, vous souhaiterez créer un fichier de paramètres distinct pour le test, qui peut charger et remplacer les paramètres de votre fichier de paramètres principal. Il existe plusieurs bonnes approches à ce sujet dans les autres réponses; J'ai vu des variations réussies sur les approches de hspander et de dmitrii .

slinkp
la source
4
Je dirais que c'est la meilleure façon de faire cela maintenant dans Django 1.4+
Michael Mior
Comment accéder ultérieurement à ce paramètre à partir des tests? Le mieux que j'ai trouvé est quelque chose comme self.settings().wrapped.MEDIA_ROOT, mais c'est assez terrible.
mlissner
2
Les nouvelles versions de Django ont un gestionnaire de contexte spécifique pour cela: docs.djangoproject.com/en/1.8/topics/testing/tools
...
Mon coup de @modify_settings(MIDDLEWARE_CLASSES=...
coeur
44

Vous pouvez faire tout ce que vous voulez pour la UnitTestsous - classe, y compris la définition et la lecture des propriétés d'occurrence:

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

Étant donné que les cas de test django fonctionnent avec un seul thread, je suis curieux de savoir quoi d'autre pourrait modifier la valeur NUM_LATEST? Si ce "quelque chose d'autre" est déclenché par votre routine de test, alors je ne suis pas sûr qu'une quantité de patching de singe sauvera le test sans invalider la véracité des tests eux-mêmes.

Jarret Hardie
la source
Votre exemple a fonctionné. Cela a été une révélation en termes de portée des tests unitaires et de la façon dont les paramètres du fichier de tests se propagent dans la pile d'appels.
Soviut
Cela ne fonctionne pas avec settings.TEMPLATE_LOADERS... Donc ce n'est pas de manière générale du moins, les paramètres ou Django n'est pas rechargé ou quoi que ce soit avec cette astuce.
Ciantic
1
c'est un bon exemple pour la version Django antérieure à 1.4. Pour> = 1,4 réponse stackoverflow.com/a/6415129/190127 plus correct
Oduvan
Utilisez docs.djangoproject.com/en/dev/topics/testing/tools/... Le correctif avec setUp et tearDown comme celui-ci est un excellent moyen de faire des tests vraiment fragiles qui sont plus verbeux que nécessaire. Si vous avez besoin de patcher quelque chose comme ça, utilisez quelque chose comme flexmock.
gaufres floues
"Puisque les cas de test de django s'exécutent sur un seul thread": ce qui n'est plus le cas dans Django 1.9.
Wtower
22

Bien que la configuration des paramètres de remplacement lors de l'exécution puisse aider, à mon avis, vous devriez créer un fichier séparé pour les tests. Cela économise beaucoup de configuration pour les tests et cela garantirait que vous ne finissiez jamais par faire quelque chose d'irréversible (comme le nettoyage de la base de données de préparation).

Supposons que votre fichier de test existe dans "mon_projet / test_settings.py", ajoutez

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

dans votre manage.py. Cela garantira que lorsque vous exécutez, python manage.py testvous utilisez uniquement test_settings. Si vous utilisez un autre client de test comme pytest, vous pouvez aussi facilement l'ajouter à pytest.ini

hspandher
la source
2
Je pense que c'est une bonne solution pour moi. J'ai trop de tests et de code utilisant le cache. Il me sera difficile de remplacer les paramètres un par un. Je vais créer deux fichiers de configuration et déterminer lequel utiliser. La réponse de MicroPyramid est également disponible, mais ce sera dangereux si j'ai oublié d'ajouter les paramètres de paramètres une fois.
ramwin
22

Vous pouvez passer l' --settingsoption lors de l'exécution des tests

python manage.py test --settings=mysite.settings_local
MicroPyramide
la source
il s'est arrêté pour trouver des applications qui se trouvent dans settings.dev qui est l'extension de settings.base
holms
4
Je pense que ce sera dangereux si quelqu'un oublie d'ajouter les paramètres de paramètres une fois.
ramwin
20

Mise à jour : la solution ci-dessous n'est nécessaire que sur Django 1.3.x et versions antérieures. Pour> 1.4, voir la réponse de slinkp .

Si vous modifiez fréquemment les paramètres dans vos tests et utilisez Python ≥2.5, cela est également pratique:

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)

Ensuite, vous pouvez faire:

with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
    do_my_tests()
Akaihola
la source
C'est une solution vraiment cool. Pour une raison quelconque, mes paramètres ne fonctionnaient pas correctement dans les tests unitaires. Solution très élégante, merci pour le partage.
Tomas
J'utilise ce code, mais j'ai eu des problèmes avec les échecs de test en cascade, car les paramètres ne seraient pas annulés si le test en question échouait. Pour résoudre ce problème, j'ai ajouté un essai / enfin autour de l' yieldinstruction, avec la dernière partie de la fonction contenue dans le finallybloc, afin que les paramètres soient toujours annulés.
Dustin Rasener
Je vais éditer la réponse pour la postérité. J'espère que je fais ça bien! :)
Dustin Rasener
11

@override_settings est idéal si vous n'avez pas beaucoup de différences entre vos configurations d'environnement de production et de test.

Dans les autres cas, vous feriez mieux d'avoir simplement différents fichiers de paramètres. Dans ce cas, votre projet ressemblera à ceci:

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

Vous devez donc avoir la plupart de vos paramètres dans base.py, puis dans d'autres fichiers, vous devez tout importer à partir de là et remplacer certaines options. Voici à quoi ressemblera votre test.pyfichier:

from .base import *

DEBUG = False

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

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

LOGGING = {}

Et puis, vous devez soit spécifier l' --settingsoption comme dans la réponse @MicroPyramid, soit spécifier DJANGO_SETTINGS_MODULEla variable d'environnement, puis vous pouvez exécuter vos tests:

export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test 
Dmitrii Mikhailov
la source
Salut . Dmitrii, merci pour votre réponse J'ai le même cas avec cette réponse, mais je voudrais obtenir plus de conseils sur la façon dont l'application saura, l'environnement dans lequel nous sommes (test ou production) , jetez un œil sur ma branche, consultez mon repo github.com/andela/ah-backend-iroquois/tree/develop/authors , comme comment vais-je gérer cette logique?
Lutaaya Huzaifah Idris
Parce que j'utilise nosetests pour exécuter des tests, maintenant comment cela sera-t-il exécuté ?, dans l'environnement de test et non dans l' environnement de développement
Lutaaya Huzaifah Idris
3

J'ai trouvé ceci en essayant de corriger certains doctests ... Par souci d'exhaustivité, je tiens à mentionner que si vous allez modifier les paramètres lors de l'utilisation de doctests, vous devez le faire avant d'importer quoi que ce soit d'autre ...

>>> from django.conf import settings

>>> settings.SOME_SETTING = 20

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

Pour les utilisateurs de pytest .

Le plus gros problème est:

  • override_settings ne fonctionne pas avec pytest.
  • Le sous-classement de Django le TestCasefera fonctionner, mais vous ne pourrez pas utiliser les appareils pytest.

La solution est d'utiliser le settingsluminaire documenté ici .

Exemple

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

Et au cas où vous auriez besoin de mettre à jour plusieurs champs

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
la source
3

Vous pouvez annuler le réglage même pour une seule fonction de test.

from django.test import TestCase, override_settings

class SomeTestCase(TestCase):

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

ou vous pouvez remplacer le paramètre de chaque fonction de la classe.

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

    def test_some_function():
        
shivansh
la source
1

J'utilise pytest.

J'ai réussi à résoudre ce problème de la manière suivante:

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
la source
1

Vous pouvez remplacer les paramètres dans le test de cette manière:

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

Et si vous avez besoin de ces mêmes paramètres dans un autre fichier, vous pouvez simplement les importer directement test_settings.

géants
la source
0

Si vous avez plusieurs fichiers de test placés dans un sous-répertoire (package python), vous pouvez remplacer les paramètres de tous ces fichiers en fonction de la condition de présence de la chaîne 'test' dans 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)

Pas la meilleure approche. Utilisé pour changer le courtier Celery de Redis en Memory.

Ledorub
la source
0

J'ai créé un nouveau fichier settings_test.py qui importerait tout du fichier settings.py et modifierait tout ce qui est différent à des fins de test. Dans mon cas, je voulais utiliser un autre compartiment de stockage dans le cloud lors des tests. entrez la description de l'image ici

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
la source