Wie kann ich die Protokollierung deaktivieren, während Unit-Tests in Python Django ausgeführt werden?

167

Ich verwende einen einfachen Unit-Test-basierten Testläufer, um meine Django-Anwendung zu testen.

Meine Anwendung selbst ist so konfiguriert, dass sie einen grundlegenden Logger in settings.py verwendet.

logging.basicConfig(level=logging.DEBUG)

Und in meinem Anwendungscode mit:

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

Wenn Sie jedoch Unittests ausführen, möchte ich die Protokollierung deaktivieren, damit meine Ausgabe der Testergebnisse nicht überladen wird. Gibt es eine einfache Möglichkeit, die Protokollierung global zu deaktivieren, damit die anwendungsspezifischen Protokollierer beim Ausführen von Tests keine Daten an die Konsole schreiben?

Shreddd
quelle
Wie haben Sie die Protokollierung beim Ausführen von Tests aktiviert? und warum benutzt du nicht django LOGGING?
Bangalore

Antworten:

248
logging.disable(logging.CRITICAL)

Deaktiviert alle Protokollierungsanrufe mit Ebenen, die weniger schwerwiegend als oder gleich sind CRITICAL. Die Protokollierung kann mit wieder aktiviert werden

logging.disable(logging.NOTSET)
unutbu
quelle
41
Dies mag offensichtlich sein, aber ich finde es hilfreich, manchmal das Offensichtliche zum Nutzen anderer Leser anzugeben: Sie würden den Anruf logging.disable(aus der akzeptierten Antwort) oben tests.pyin Ihrer Anwendung, die die Protokollierung durchführt, platzieren.
CJ Gaconnet
7
Am Ende habe ich den Aufruf in setUp () gesetzt, aber Ihr Standpunkt ist gut aufgenommen.
Shreddd
in der setUp () -Methode Ihres Tests oder im eigentlichen Test, der die Protokollnachrichten generiert, die Sie ausblenden möchten.
Qris
10
Und in Ihrer tearDown()Methode: logging.disable(logging.NOTSET)Bringt die Protokollierung wieder ordentlich an.
mlissner
34
Das Einfügen in die Init .py des testsModuls ist sehr nützlich.
Toabi
46

Da Sie sich in Django befinden, können Sie diese Zeilen zu Ihren settings.py hinzufügen:

import sys
import logging

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

Auf diese Weise müssen Sie diese Zeile nicht bei jedem setUp()Ihrer Tests hinzufügen .

Auf diese Weise können Sie auch einige nützliche Änderungen für Ihre Testanforderungen vornehmen.

Es gibt eine andere "schönere" oder "sauberere" Möglichkeit, Ihren Tests Besonderheiten hinzuzufügen, und das macht Ihren eigenen Testläufer.

Erstellen Sie einfach eine Klasse wie diese:

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)

Und jetzt fügen Sie Ihrer settings.py-Datei Folgendes hinzu:

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

Auf diese Weise können Sie eine wirklich praktische Änderung vornehmen, die der andere Ansatz nicht tut. Django testet lediglich die gewünschten Anwendungen. Sie können dies tun, indem Sie das test_labelsHinzufügen dieser Zeile zum Testläufer ändern :

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
Hassek
quelle
Sicher - wenn Sie es in settings.py einfügen, wird es global.
Shreddd
7
Für Django 1.6+ überprüfen Sie bitte die Antwort von @alukach.
Hassek
2
In Unit-Tests möchte ich manchmal behaupten, dass ein Fehler protokolliert wurde, daher ist diese Methode nicht ideal. Dennoch ist es ist eine gute Antwort.
Sardathrion - gegen SE Missbrauch
23

Gibt es eine einfache Möglichkeit, die Protokollierung global zu deaktivieren, damit die anwendungsspezifischen Protokollierer beim Ausführen von Tests keine Daten an die Konsole schreiben?

Die anderen Antworten verhindern das "Schreiben von Inhalten an die Konsole", indem die Protokollierungsinfrastruktur global so eingestellt wird, dass alles ignoriert wird. Das funktioniert, aber ich finde es zu stumpf. Mein Ansatz besteht darin, eine Konfigurationsänderung durchzuführen, die nur das tut, was erforderlich ist, um zu verhindern, dass Protokolle auf der Konsole veröffentlicht werden. Also füge ich meinem : einen benutzerdefinierten Protokollierungsfilter hinzusettings.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

Und ich konfiguriere die Django-Protokollierung so , dass sie den Filter verwendet:

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,
        },
    }
}

Endergebnis: Wenn ich teste, geht nichts an die Konsole, aber alles andere bleibt gleich.

Warum das tun?

Ich entwerfe Code, der Protokollierungsanweisungen enthält, die nur unter bestimmten Umständen ausgelöst werden und die genau die Daten ausgeben sollen, die ich für die Diagnose benötige, wenn etwas schief geht. Daher teste ich , dass sie das tun, was sie tun sollen, und daher ist es für mich nicht sinnvoll, die Protokollierung vollständig zu deaktivieren. Ich möchte nicht feststellen, dass das, was ich für protokolliert hielt , nicht protokolliert wird , sobald die Software in Produktion ist.

Darüber hinaus erfassen einige Testläufer (z. B. Nose) während des Tests Protokolle und geben den relevanten Teil des Protokolls zusammen mit einem Testfehler aus. Dies ist hilfreich, um herauszufinden, warum ein Test fehlgeschlagen ist. Wenn die Protokollierung vollständig deaktiviert ist, kann nichts erfasst werden.

Louis
quelle
"Jeder Testläufer, den ich verwende, lädt den Django-Code so, dass er wahr ist." Interessant ... wie?
Webtweakers
Ich habe eine test_settings.pyDatei neben der meines Projekts settings.py. Es ist so eingestellt, dass es geladen wird settings.pyund einige Änderungen wie "set TESTING_MODEto" vornimmt True. Meine Testläufer sind so organisiert, dass test_settingsdas Modul für die Django-Projekteinstellungen geladen wird. Es gibt viele Möglichkeiten, dies zu tun. Normalerweise setze ich die Umgebungsvariable DJANGO_SETTINGS_MODULEauf proj.test_settings.
Louis
Das ist großartig und macht genau das, was ich will. Blendet die Protokollierung während Unittests aus, bis etwas fehlschlägt. Dann nimmt Django Nose die Ausgabe auf und druckt sie mit dem Fehler. Perfekt. Kombinieren Sie dies damit, um festzustellen, ob der Komponententest aktiv ist.
Rrauenza
21

Ich mag Hasseks Idee eines benutzerdefinierten Testläufers. Es ist zu beachten, dass dies DjangoTestSuiteRunnernicht mehr der Standard-Testläufer in Django 1.6+ ist, sondern durch den ersetzt wurde DiscoverRunner. Für das Standardverhalten sollte der Testläufer eher wie folgt aussehen:

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
quelle
Ich habe Ihre Lösung gefunden, nachdem ich viele Dinge ausprobiert hatte. Ich kann die Variable TEST_RUNNER jedoch nicht in den Einstellungen festlegen, da das Modul, in dem sich die Datei test_runner befindet, nicht importiert werden kann.
Bunny Rabbit
Klingt nach einem Importproblem. Setzen Sie TEST_RUNNER auf einen Zeichenfolgenpfad zum Runner (nicht zum eigentlichen Python-Modul)? Wo befindet sich Ihr Läufer? Ich habe meine in einer separaten App namens helpers, die nur Utils enthält, die von keiner anderen Stelle im Projekt importiert werden.
Alukach
5

Ich habe festgestellt, dass für Tests innerhalb unittestoder ähnlich eines Frameworks der effektivste Weg, um unerwünschte Anmeldungen in Komponententests sicher zu deaktivieren, darin besteht, die setUp/ tearDown-Methoden eines bestimmten Testfalls zu aktivieren / deaktivieren . Auf diese Weise kann ein Ziel speziell festgelegt werden, wo Protokolle deaktiviert werden sollen. Sie können dies auch explizit auf dem Logger der Klasse tun, die Sie testen.

import unittest
import logging

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

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

Ich verwende einen einfachen Methodendekorateur, um die Protokollierung nur in einer bestimmten Testmethode zu deaktivieren.

def disable_logging(f):

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

        return result

    return wrapper

Und dann benutze ich es wie im folgenden Beispiel:

class ScenarioTestCase(TestCase):

    @disable_logging
    test_scenario(self):
        pass
Eduard Mukans
quelle
3

Es gibt eine hübsche und saubere Methode, um die Anmeldung von Tests mit der unittest.mock.patchMethode auszusetzen .

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

Und python3 -m unittest testserzeugt keine Protokollausgabe.

Valex
quelle
1

Manchmal möchten Sie die Protokolle und manchmal nicht. Ich habe diesen Code in meinemsettings.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)

Wenn Sie also Ihren Test mit den --no-logsOptionen ausführen , erhalten Sie nur die criticalProtokolle:

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

Es ist sehr hilfreich, wenn Sie die Tests für Ihren kontinuierlichen Integrationsfluss beschleunigen möchten.

Karim N Gorjux
quelle
1

Wenn Sie nicht möchten, dass es wiederholt in setUp () und tearDown () für unittest ein- und ausgeschaltet wird (sehen Sie den Grund dafür nicht), können Sie es einfach einmal pro Klasse tun:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)
das Kissen
quelle
1

In Fällen, in denen ich einen bestimmten Logger vorübergehend unterdrücken möchte, habe ich einen kleinen Kontextmanager geschrieben, den ich als nützlich empfunden habe:

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

Sie verwenden es dann wie folgt:

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

Dies hat den Vorteil, dass der Logger nach Abschluss wieder aktiviert (oder auf seinen vorherigen Zustand zurückgesetzt) ​​wird with.

Nathan Villaescusa
quelle
1

Sie können dies im Verzeichnis der obersten Ebene für Unit-Tests- __init__.pyDateien ablegen. Dadurch wird die globale Protokollierung in der Unit-Test-Suite deaktiviert.

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)
Aaron Lelevier
quelle
0

In meinem Fall habe ich eine Einstellungsdatei, settings/test.pydie speziell zu Testzwecken erstellt wurde. So sieht es aus:

from .base import *

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

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

LOGGING = {}

Ich habe eine Umgebungsvariable DJANGO_SETTINGS_MODULE=settings.testauf gesetzt /etc/environment.

Dmitrii Mikhailov
quelle
0

Wenn Sie verschiedene Initialisierungsmodule für Test, Entwicklung und Produktion haben, können Sie alles deaktivieren oder im Initialisierer umleiten. Ich habe local.py, test.py und Production.py, die alle von common.y erben

common.py führt die gesamte Hauptkonfiguration einschließlich dieses Snippets aus:

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,
    },
}

Dann habe ich in test.py folgendes:

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

Dies ersetzt den Konsolenhandler durch einen FileHandler und bedeutet, dass weiterhin protokolliert wird, aber ich muss die Produktionscodebasis nicht berühren.

Christopher Broderick
quelle
0

Wenn Sie verwenden pytest :

Da pytest Protokollnachrichten erfasst und nur für fehlgeschlagene Tests anzeigt, möchten Sie normalerweise keine Protokollierung deaktivieren. Verwenden Sie stattdessen eine separate settings.pyDatei für Tests (z. B. test_settings.py) und fügen Sie sie hinzu:

LOGGING_CONFIG = None

Dies weist Django an, die Konfiguration der Protokollierung insgesamt zu überspringen. DasLOGGING Einstellung wird ignoriert und kann aus den Einstellungen entfernt werden.

Mit diesem Ansatz erhalten Sie keine Protokollierung für bestandene Tests und alle verfügbaren Protokolle für fehlgeschlagene Tests.

Die Tests werden mit der Protokollierung ausgeführt, die von eingerichtet wurde pytest. Es kann in den pytestEinstellungen (z tox.ini. B. ) nach Ihren Wünschen konfiguriert werden . Verwenden Sie log_level = DEBUG(oder das entsprechende Befehlszeilenargument), um Protokollnachrichten auf Debug-Ebene einzuschließen .

Roger Dahl
quelle