Jak mogę wyłączyć rejestrowanie podczas wykonywania testów jednostkowych w Python Django?

168

Aby przetestować moją aplikację Django, używam prostego testera opartego na testach jednostkowych.

Moja aplikacja jest skonfigurowana do korzystania z podstawowego loggera w settings.py przy użyciu:

logging.basicConfig(level=logging.DEBUG)

A w kodzie mojej aplikacji za pomocą:

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

Jednak podczas uruchamiania unittests chciałbym wyłączyć rejestrowanie, aby nie zaśmiecało moich wyników testu. Czy istnieje prosty sposób na wyłączenie logowania w sposób globalny, tak aby rejestratory specyficzne dla aplikacji nie zapisywały informacji na konsoli, gdy uruchamiam testy?

shreddd
źródło
Jak włączyłeś logowanie podczas przeprowadzania testów? i dlaczego nie używasz django LOGGING?
dalore

Odpowiedzi:

249
logging.disable(logging.CRITICAL)

wyłączy wszystkie wywołania logowania z poziomami mniejszymi lub równymi CRITICAL. Rejestrowanie można ponownie włączyć za pomocą

logging.disable(logging.NOTSET)
unutbu
źródło
42
Może to być oczywiste, ale czasami uważam, że pomocne jest stwierdzenie oczywistego z korzyścią dla innych czytelników: umieściłbyś wywołanie logging.disable(z zaakceptowanej odpowiedzi) na górze tests.pyaplikacji, która rejestruje.
CJ Gaconnet,
7
Skończyło się na tym, że sprawdziłem w setUp (), ale twój punkt widzenia jest dobrze zrozumiany.
shreddd
w metodzie setUp () testu lub w rzeczywistym teście, który generuje komunikaty dziennika, które chcesz ukryć.
qris,
10
A w twojej tearDown()metodzie: logging.disable(logging.NOTSET)starannie odkłada logowanie na miejsce.
mlissner
34
Umieszczenie go w init .py testsmodułu jest bardzo przydatne.
toabi
46

Ponieważ jesteś w Django, możesz dodać te linie do swojego settings.py:

import sys
import logging

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

W ten sposób nie musisz dodawać tej linii we wszystkich setUp()testach.

W ten sposób możesz również wprowadzić kilka przydatnych zmian dla swoich potrzeb testowych.

Istnieje inny „przyjemniejszy” lub „czystszy” sposób dodawania szczegółów do testów, a mianowicie tworzenie własnego narzędzia do uruchamiania testów.

Po prostu utwórz taką klasę:

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)

A teraz dodaj do swojego pliku settings.py:

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

Pozwala to na jedną naprawdę przydatną modyfikację, której nie ma w przypadku drugiego podejścia, a mianowicie sprawienie, że Django po prostu testuje aplikacje, które chcesz. Możesz to zrobić, zmieniając test_labelsdodanie tej linii do testu runner:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
Hassek
źródło
Jasne - umieszczenie go w settings.py uczyniłoby go globalnym.
shreddd
7
dla Django 1.6+ proszę sprawdzić odpowiedź @alukach.
Hassek
2
Czasami w testach jednostkowych chcę stwierdzić, że zarejestrowano błąd, więc ta metoda nie jest idealna. Mimo wszystko jest to dobra odpowiedź.
Sardathrion - przeciwko nadużyciom SE
23

Czy istnieje prosty sposób na wyłączenie logowania w sposób globalny, tak aby rejestratory specyficzne dla aplikacji nie zapisywały informacji na konsoli, gdy uruchamiam testy?

Inne odpowiedzi zapobiegają „zapisywaniu rzeczy na konsolę” przez globalne ustawienie infrastruktury rejestrowania tak, aby cokolwiek ignorowała. To działa, ale uważam, że jest to zbyt tępe podejście. Moje podejście polega na wprowadzeniu zmiany konfiguracji, która robi tylko to, co jest potrzebne, aby zapobiec wydostawaniu się dzienników na konsolę. Dlatego dodaję niestandardowy filtr rejestrowania do mojego 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

I skonfigurowałem logowanie Django, aby użyło filtra:

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

Wynik końcowy: kiedy testuję, nic nie trafia do konsoli, ale wszystko inne pozostaje takie samo.

Czemu to robić?

Projektuję kod, który zawiera instrukcje logowania, które są uruchamiane tylko w określonych okolicznościach i który powinien podawać dokładne dane potrzebne do diagnozy, jeśli coś pójdzie nie tak. Dlatego testuję , że robią to, co powinni, a zatem całkowite wyłączenie logowania nie jest dla mnie opłacalne. Nie chcę znaleźć, gdy oprogramowanie jest już w produkcji, że to, co myślałem, że zostanie zarejestrowane, nie jest rejestrowane.

Co więcej, niektóre programy uruchamiające testy (na przykład Nos) przechwytują dzienniki podczas testowania i wyświetlają odpowiednią część dziennika wraz z niepowodzeniem testu. Jest to przydatne w ustalaniu przyczyny niepowodzenia testu. Jeśli rejestrowanie jest całkowicie wyłączone, nic nie można przechwycić.

Louis
źródło
„Każdy program uruchamiający testy, którego używam, załaduje kod Django w sposób, który sprawia, że ​​jest on prawdziwy”. Ciekawe ... Jak?
webtweakers
Mam test_settings.pyplik, który znajduje się obok mojego projektu settings.py. Jest ustawiony na ładowanie settings.pyi wprowadzanie zmian, takich jak ustawione TESTING_MODEna True. Moje programy uruchamiające testy są tak zorganizowane, że test_settingsjest to moduł załadowany dla ustawień projektu Django. Można to zrobić na wiele sposobów. Zwykle wybieram ustawienie zmiennej środowiskowej DJANGO_SETTINGS_MODULEna proj.test_settings.
Louis
To jest niesamowite i robi dokładnie to, czego chcę. Ukrywa logowanie podczas unittestów, aż coś się nie powiedzie - wtedy Django Nose odbiera dane wyjściowe i drukuje je z błędem. Idealny. Połącz to z tym, aby określić, czy testowanie jednostkowe jest aktywne.
rrauenza
21

Podoba mi się pomysł niestandardowego testera Hasseka. Należy zauważyć, że DjangoTestSuiteRunnernie jest już domyślnym programem uruchamiającym testy w Django 1.6+, został zastąpiony przez DiscoverRunner. W przypadku zachowania domyślnego program uruchamiający testy powinien wyglądać bardziej jak:

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
źródło
Znalazłem rozwiązanie po wypróbowaniu wielu rzeczy. Jednak nie mogę ustawić zmiennej TEST_RUNNER w ustawieniach, ponieważ nie jest w stanie zaimportować modułu, w którym znajduje się plik test_runner.
Bunny Rabbit
Brzmi jak problem z importem. Czy ustawiasz TEST_RUNNER na ścieżkę łańcucha do elementu uruchamiającego (a nie faktyczny moduł Pythona)? Gdzie znajduje się Twój biegacz? Mam swoją w osobnej aplikacji o nazwie helpers, która zawiera tylko narzędzia, które nie są importowane z żadnego innego miejsca w projekcie.
alukach
5

Zauważyłem, że w przypadku testów w ramach unittestlub podobnej strukturze najskuteczniejszym sposobem bezpiecznego wyłączenia niechcianego logowania w testach jednostkowych jest włączenie / wyłączenie w setUp/ tearDownMethods konkretnego przypadku testowego. Pozwala to na jeden cel konkretnie, w którym dzienniki powinny być wyłączone. Możesz to również zrobić jawnie w programie rejestrującym testowanej klasy.

import unittest
import logging

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

    def tearDown(self):
        logging.disable(logging.NOTSET)
mcguip
źródło
4

Używam dekoratora prostej metody, aby wyłączyć rejestrowanie tylko w określonej metodzie testowej.

def disable_logging(f):

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

        return result

    return wrapper

A potem używam go jak w poniższym przykładzie:

class ScenarioTestCase(TestCase):

    @disable_logging
    test_scenario(self):
        pass
Eduard Mukans
źródło
3

Istnieje pewna ładna i przejrzysta metoda zawieszania logowania testów za pomocą unittest.mock.patchmetody.

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

I nie python3 -m unittest testsbędzie generować żadnych danych logowania.

valex
źródło
1

Czasami potrzebujesz dzienników, a czasami nie. Mam ten kod w moimsettings.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)

Jeśli więc uruchomisz test z --no-logsopcjami, otrzymasz tylko criticaldzienniki:

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

Jest to bardzo pomocne, jeśli chcesz przyspieszyć testy w przepływie ciągłej integracji.

Karim N Gorjux
źródło
1

Jeśli nie chcesz, aby to wielokrotnie włączało / wyłączało się w setUp () i tearDown () dla unittest (nie widzisz powodu), możesz to zrobić raz na zajęcia:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)
poduszka
źródło
1

W przypadkach, w których chcę tymczasowo ukryć określony rejestrator, napisałem mały menedżer kontekstu, który okazał się przydatny:

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

Następnie używasz go tak:

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

Ma to tę zaletę, że rejestrator jest ponownie włączany (lub przywracany do poprzedniego stanu) po withzakończeniu.

Nathan Villaescusa
źródło
1

Możesz umieścić to w katalogu najwyższego poziomu dla __init__.pypliku testów jednostkowych . Spowoduje to wyłączenie globalnego rejestrowania w zestawie testów jednostkowych.

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)
Aaron Lelevier
źródło
0

W moim przypadku mam plik ustawień settings/test.pystworzony specjalnie do celów testowych, oto jak to wygląda:

from .base import *

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

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

LOGGING = {}

Umieściłem zmienną środowiskową DJANGO_SETTINGS_MODULE=settings.testw /etc/environment.

Dmitrij Michajłow
źródło
0

Jeśli masz różne moduły inicjalizacyjne do testowania, tworzenia i produkcji, możesz wyłączyć cokolwiek lub przekierować to w inicjatorze. Mam local.py, test.py i production.py, które dziedziczą po common.y

common.py wykonuje całą główną konfigurację, w tym ten fragment:

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

Następnie w test.py mam to:

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

Zastępuje to obsługę konsoli przez FileHandler i oznacza, że ​​nadal rejestruję, ale nie muszę dotykać bazy kodu produkcyjnego.

Christopher Broderick
źródło
0

Jeśli używasz pytest:

Ponieważ pytest przechwytuje komunikaty dziennika i wyświetla je tylko w przypadku testów zakończonych niepowodzeniem, zazwyczaj nie chcesz wyłączać żadnego rejestrowania. Zamiast tego użyj oddzielnego settings.pypliku do testów (np. test_settings.py) I dodaj do niego:

LOGGING_CONFIG = None

To mówi Django, aby całkowicie pominął konfigurowanie logowania. LOGGINGUstawienie zostanie zignorowany i mogą być usunięte z ustawieniami.

Dzięki takiemu podejściu nie otrzymujesz żadnego rejestrowania dla zdanych testów, a otrzymasz wszystkie dostępne rejestracje dla testów zakończonych niepowodzeniem.

Testy zostaną uruchomione przy użyciu rejestrowania skonfigurowanego przez pytest. Można go skonfigurować według własnych upodobań w pytestustawieniach (np tox.ini.). Aby dołączyć komunikaty dziennika poziomu debugowania, użyj log_level = DEBUG(lub odpowiedniego argumentu wiersza poleceń).

Roger Dahl
źródło