Comment désactiver la journalisation lors de l'exécution de tests unitaires dans Python Django?

168

J'utilise un simple testeur basé sur un test unitaire pour tester mon application Django.

Mon application elle-même est configurée pour utiliser un enregistreur de base dans settings.py en utilisant:

logging.basicConfig(level=logging.DEBUG)

Et dans mon code d'application en utilisant:

logger = logging.getLogger(__name__)
logger.setLevel(getattr(settings, 'LOG_LEVEL', logging.DEBUG))

Cependant, lors de l'exécution de tests unitaires, j'aimerais désactiver la journalisation afin qu'elle n'encombre pas la sortie de mes résultats de test. Existe-t-il un moyen simple de désactiver la journalisation de manière globale, de sorte que les enregistreurs spécifiques à l'application n'écrivent pas de données sur la console lorsque j'exécute des tests?

shreddd
la source
Comment avez-vous activé la journalisation lors de l'exécution des tests? et pourquoi n'utilisez-vous pas django LOGGING?
dalore

Réponses:

249
logging.disable(logging.CRITICAL)

désactivera tous les appels de journalisation avec des niveaux inférieurs ou égaux à CRITICAL. La journalisation peut être réactivée avec

logging.disable(logging.NOTSET)
unutbu
la source
42
Cela peut être évident, mais je trouve utile d'indiquer parfois l'évidence pour le bénéfice d'autres lecteurs: vous mettriez l'appel à logging.disable(à partir de la réponse acceptée) en haut de tests.pyvotre application qui effectue la journalisation.
CJ Gaconnet
7
J'ai fini par mettre l'appel dans setUp () mais votre point est bien pris.
shreddd
dans la méthode setUp () de votre test, ou dans le test réel qui génère les messages de journal que vous souhaitez masquer.
qris
10
Et dans votre tearDown()méthode: logging.disable(logging.NOTSET)remet la journalisation en place proprement.
mlissner
34
Le mettre dans le fichier init .py du testsmodule est très utile.
toabi
46

Puisque vous êtes dans Django, vous pouvez ajouter ces lignes à votre settings.py:

import sys
import logging

if len(sys.argv) > 1 and sys.argv[1] == 'test':
    logging.disable(logging.CRITICAL)

De cette façon, vous n'avez pas à ajouter cette ligne dans tous setUp()vos tests.

Vous pouvez également faire quelques modifications pratiques pour vos besoins de test de cette façon.

Il existe une autre façon "plus agréable" ou "plus propre" d'ajouter des détails à vos tests et c'est de créer votre propre testeur.

Créez simplement une classe comme celle-ci:

import logging

from django.test.simple import DjangoTestSuiteRunner
from django.conf import settings

class MyOwnTestRunner(DjangoTestSuiteRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):

        # Don't show logging messages while testing
        logging.disable(logging.CRITICAL)

        return super(MyOwnTestRunner, self).run_tests(test_labels, extra_tests, **kwargs)

Et maintenant, ajoutez à votre fichier settings.py:

TEST_RUNNER = "PATH.TO.PYFILE.MyOwnTestRunner"
#(for example, 'utils.mytest_runner.MyOwnTestRunner')

Cela vous permet de faire une modification vraiment pratique que l'autre approche ne fait pas, c'est-à-dire que Django teste simplement les applications que vous voulez. Vous pouvez le faire en modifiant l' test_labelsajout de cette ligne au testeur:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
Hassek
la source
Bien sûr, le mettre dans settings.py le rendrait global.
shreddd
7
pour Django 1.6+, veuillez vérifier la réponse @alukach.
Hassek
2
Parfois, dans les tests unitaires, je veux affirmer qu'une erreur a été enregistrée, donc cette méthode n'est pas idéale. Pourtant, il est une bonne réponse.
Sardathrion - contre les abus SE
23

Existe-t-il un moyen simple de désactiver la journalisation de manière globale, de sorte que les enregistreurs spécifiques à l'application n'écrivent pas de données sur la console lorsque j'exécute des tests?

Les autres réponses empêchent «d'écrire des trucs sur la console» en configurant globalement l'infrastructure de journalisation pour qu'elle ignore quoi que ce soit. Cela fonctionne mais je trouve que c'est une approche trop directe. Mon approche consiste à effectuer un changement de configuration qui ne fait que ce qui est nécessaire pour empêcher les journaux de sortir sur la console. J'ajoute donc un filtre de journalisation personnalisé à mon settings.py:

from logging import Filter

class NotInTestingFilter(Filter):

    def filter(self, record):
        # Although I normally just put this class in the settings.py
        # file, I have my reasons to load settings here. In many
        # cases, you could skip the import and just read the setting
        # from the local symbol space.
        from django.conf import settings

        # TESTING_MODE is some settings variable that tells my code
        # whether the code is running in a testing environment or
        # not. Any test runner I use will load the Django code in a
        # way that makes it True.
        return not settings.TESTING_MODE

Et je configure la journalisation Django pour utiliser le filtre:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'testing': {
            '()': NotInTestingFilter
        }
    },
    'formatters': {
        'verbose': {
            'format': ('%(levelname)s %(asctime)s %(module)s '
                       '%(process)d %(thread)d %(message)s')
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'filters': ['testing'],
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'foo': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        },
    }
}

Résultat final: lorsque je teste, rien ne va à la console, mais tout le reste reste le même.

Pourquoi faire ceci?

Je conçois un code qui contient des instructions de journalisation qui ne sont déclenchées que dans des circonstances spécifiques et qui doivent générer les données exactes dont j'ai besoin pour le diagnostic en cas de problème. Par conséquent, je teste qu'ils font ce qu'ils sont censés faire et ainsi désactiver complètement la journalisation n'est pas viable pour moi. Je ne veux pas trouver une fois que le logiciel est en production que ce que je pensais être enregistré ne l'est pas.

De plus, certains testeurs (Nose, par exemple) captureront les journaux pendant les tests et afficheront la partie pertinente du journal avec un échec du test. Il est utile pour comprendre pourquoi un test a échoué. Si la journalisation est complètement désactivée, alors rien ne peut être capturé.

Louis
la source
"Tout testeur que j'utilise chargera le code Django d'une manière qui le rendra vrai." Intéressant ... Comment?
webtweakers
J'ai un test_settings.pyfichier qui se trouve à côté de celui de mon projet settings.py. Il est configuré pour se charger settings.pyet apporter des modifications comme défini TESTING_MODEsur True. Mes testeurs sont organisés de manière à ce que test_settingsle module soit chargé pour les paramètres du projet Django. Cela peut être fait de plusieurs façons. Je vais généralement définir la variable d'environnement DJANGO_SETTINGS_MODULEsur proj.test_settings.
Louis
C'est génial et fait exactement ce que je veux. Cache la journalisation pendant les tests unitaires jusqu'à ce que quelque chose échoue - alors Django Nose récupère la sortie et l'imprime avec l'échec. Parfait. Combinez-le avec ceci pour déterminer si le test unitaire est actif.
rrauenza
21

J'aime l'idée de testeur personnalisé de Hassek. Il convient de noter que ce DjangoTestSuiteRunnern'est plus le lanceur de test par défaut dans Django 1.6+, il a été remplacé par le DiscoverRunner. Pour le comportement par défaut, le lanceur de test devrait ressembler davantage à:

import logging

from django.test.runner import DiscoverRunner

class NoLoggingTestRunner(DiscoverRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):

        # disable logging below CRITICAL while testing
        logging.disable(logging.CRITICAL)

        return super(NoLoggingTestRunner, self).run_tests(test_labels, extra_tests, **kwargs)
Alukach
la source
J'ai trouvé votre solution après avoir essayé beaucoup de choses. Cependant, je ne suis pas en mesure de définir la variable TEST_RUNNER dans les paramètres car il ne peut pas importer le module où se trouve le fichier test_runner.
Bunny Rabbit
Cela ressemble à un problème d'importation. Définissez-vous TEST_RUNNER sur un chemin de chaîne vers le runner (pas le module Python réel)? De plus, où se trouve votre coureur? J'ai le mien dans une application distincte nommée helpers, qui n'a que des utilitaires qui ne sont importés de nulle part ailleurs dans le projet.
alukach
5

J'ai constaté que pour les tests dans unittestou dans un cadre similaire, le moyen le plus efficace de désactiver en toute sécurité la journalisation indésirable dans les tests unitaires est d'activer / de désactiver les méthodes setUp/ tearDownd'un cas de test particulier. Cela permet à une cible spécifiquement où les journaux doivent être désactivés. Vous pouvez également le faire explicitement sur le logger de la classe que vous testez.

import unittest
import logging

class TestMyUnitTest(unittest.TestCase):
    def setUp(self):
        logging.disable(logging.CRITICAL)

    def tearDown(self):
        logging.disable(logging.NOTSET)
mcguip
la source
4

J'utilise un décorateur de méthode simple pour désactiver la journalisation uniquement dans une méthode de test particulière.

def disable_logging(f):

    def wrapper(*args):
        logging.disable(logging.CRITICAL)
        result = f(*args)
        logging.disable(logging.NOTSET)

        return result

    return wrapper

Et puis je l'utilise comme dans l'exemple suivant:

class ScenarioTestCase(TestCase):

    @disable_logging
    test_scenario(self):
        pass
Eduard Mukans
la source
3

Il existe une méthode jolie et propre pour suspendre la connexion aux tests avec unittest.mock.patchmethod.

foo.py :

import logging


logger = logging.getLogger(__name__)

def bar():
    logger.error('There is some error output here!')
    return True

tests.py :

from unittest import mock, TestCase
from foo import bar


class FooBarTestCase(TestCase):
    @mock.patch('foo.logger', mock.Mock())
    def test_bar(self):
        self.assertTrue(bar())

Et python3 -m unittest testsne produira aucune sortie de journalisation.

Valex
la source
1

Parfois, vous voulez les journaux et parfois non. J'ai ce code dans monsettings.py

import sys

if '--no-logs' in sys.argv:
    print('> Disabling logging levels of CRITICAL and below.')
    sys.argv.remove('--no-logs')
    logging.disable(logging.CRITICAL)

Donc, si vous exécutez votre test avec les --no-logsoptions, vous n'obtiendrez que les criticaljournaux:

$ python ./manage.py tests --no-logs
> Disabling logging levels of CRITICAL and below.

C'est très utile si vous souhaitez accélérer les tests sur votre flux d'intégration continue.

Karim N Gorjux
la source
1

Si vous ne voulez pas l'activer / le désactiver à plusieurs reprises dans setUp () et tearDown () pour unittest (ne voyez pas la raison de cela), vous pouvez le faire une fois par classe:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)
l'oreiller
la source
1

Dans les cas où je souhaite supprimer temporairement un enregistreur spécifique, j'ai écrit un petit gestionnaire de contexte que j'ai trouvé utile:

from contextlib import contextmanager
import logging

@contextmanager
def disable_logger(name):
    """Temporarily disable a specific logger."""
    logger = logging.getLogger(name)
    old_value = logger.disabled
    logger.disabled = True
    try:
        yield
    finally:
        logger.disabled = old_value

Vous l'utilisez ensuite comme:

class MyTestCase(TestCase):
    def test_something(self):
        with disable_logger('<logger name>'):
            # code that causes the logger to fire

Cela présente l'avantage que l'enregistreur est réactivé (ou remis à son état antérieur) une fois l'opération withterminée.

Nathan Villaescusa
la source
1

Vous pouvez le mettre dans le répertoire de niveau supérieur pour le __init__.pyfichier de tests unitaires . Cela désactivera la journalisation globalement dans la suite de tests unitaires.

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)
Aaron Lelevier
la source
0

Dans mon cas, j'ai un fichier de paramètres settings/test.pycréé spécifiquement à des fins de test, voici à quoi il ressemble:

from .base import *

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

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

LOGGING = {}

J'ai mis une variable d'environnement DJANGO_SETTINGS_MODULE=settings.testsur /etc/environment.

Dmitrii Mikhailov
la source
0

Si vous avez différents modules d'initialisation pour le test, le développement et la production, vous pouvez désactiver n'importe quoi ou le rediriger dans l'initialser. J'ai local.py, test.py et production.py qui héritent tous de common.y

common.py fait toute la configuration principale, y compris cet extrait:

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
    'django.server': {
        '()': 'django.utils.log.ServerFormatter',
        'format': '[%(server_time)s] %(message)s',
    },
    'verbose': {
        'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
    },
    'simple': {
        'format': '%(levelname)s %(message)s'
    },
},
'filters': {
    'require_debug_true': {
        '()': 'django.utils.log.RequireDebugTrue',
    },
},
'handlers': {
    'django.server': {
        'level': 'INFO',
        'class': 'logging.StreamHandler',
        'formatter': 'django.server',
    },
    'console': {
        'level': 'DEBUG',
        'class': 'logging.StreamHandler',
        'formatter': 'simple'
    },
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler'
    }
},
'loggers': {
    'django': {
        'handlers': ['console'],
        'level': 'INFO',
        'propagate': True,
    },
    'celery.tasks': {
        'handlers': ['console'],
        'level': 'DEBUG',
        'propagate': True,
    },
    'django.server': {
        'handlers': ['django.server'],
        'level': 'INFO',
        'propagate': False,
    },
}

Ensuite, dans test.py j'ai ceci:

console_logger = Common.LOGGING.get('handlers').get('console')
console_logger['class'] = 'logging.FileHandler
console_logger['filename'] = './unitest.log

Cela remplace le gestionnaire de console par un FileHandler et signifie toujours obtenir la journalisation, mais je n'ai pas à toucher la base de code de production.

Christopher Broderick
la source
0

Si vous utilisez pytest:

Étant donné que pytest capture les messages du journal et ne les affiche que pour les tests ayant échoué, vous ne souhaitez généralement pas désactiver la journalisation. Au lieu de cela, utilisez un settings.pyfichier séparé pour les tests (par exemple, test_settings.py), et ajoutez-y:

LOGGING_CONFIG = None

Cela indique à Django de ne pas configurer complètement la journalisation. Le LOGGINGparamètre sera ignoré et peut être supprimé des paramètres.

Avec cette approche, vous n'obtenez aucune journalisation pour les tests réussis et vous obtenez toute la journalisation disponible pour les tests ayant échoué.

Les tests seront exécutés à l'aide de la journalisation configurée par pytest. Il peut être configuré à votre guise dans les pytestparamètres (par exemple, tox.ini). Pour inclure les messages du journal de niveau de débogage, utilisez log_level = DEBUG(ou l'argument de ligne de commande correspondant).

Roger Dahl
la source