Como posso desativar o registro durante a execução de testes de unidade no Python Django?

168

Estou usando um simples testador de unidade baseado em teste para testar meu aplicativo Django.

Meu próprio aplicativo está configurado para usar um logger básico em settings.py usando:

logging.basicConfig(level=logging.DEBUG)

E no meu código de aplicação usando:

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

No entanto, ao executar unittests, eu gostaria de desativar o log para que não atrapalhe minha saída do resultado do teste. Existe uma maneira simples de desativar o log de maneira global, para que os criadores de log específicos do aplicativo não gravem coisas no console quando executo testes?

shreddd
fonte
Como você ativou o log durante a execução de testes? e por que você não está usando o django LOGGING?
dalore

Respostas:

249
logging.disable(logging.CRITICAL)

desativará todas as chamadas de log com níveis menos severos ou iguais a CRITICAL. O registro pode ser reativado com

logging.disable(logging.NOTSET)
unutbu
fonte
42
Isso pode ser óbvio, mas às vezes acho útil afirmar o óbvio para o benefício de outros leitores: Você telefonaria para logging.disable(a partir da resposta aceita) na parte superior do tests.pyaplicativo que está fazendo o registro.
CJ Gaconnet 06/04
7
Acabei fazendo a ligação em setUp (), mas o seu argumento está bem entendido.
Shreddd 06/04
no método setUp () do seu teste ou no teste real que gera as mensagens de log que você deseja ocultar.
QRIS
10
E no seu tearDown()método: logging.disable(logging.NOTSET)coloca o log de volta no lugar ordenadamente.
mlissner
34
Colocá-lo no init .py do testsmódulo é muito útil.
toabi 15/05
46

Como você está no Django, você pode adicionar essas linhas às suas configurações.py:

import sys
import logging

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

Dessa forma, você não precisa adicionar essa linha em todos os setUp()seus testes.

Você também pode fazer algumas alterações úteis para que suas necessidades de teste sejam feitas dessa maneira.

Há outra maneira "mais agradável" ou "mais limpa" de adicionar detalhes aos seus testes, e isso é criar seu próprio executor de testes.

Basta criar uma classe como esta:

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)

E agora adicione ao seu arquivo settings.py:

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

Isso permite que você faça uma modificação realmente útil que a outra abordagem não faz, que é fazer o Django apenas testar os aplicativos que você deseja. Você pode fazer isso alterando a test_labelsadição desta linha ao executor de teste:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
Hassek
fonte
Claro - colocá-lo em settings.py o tornaria global.
Shreddd
7
para o Django 1.6+, verifique a resposta @alukach.
Hassek 30/07
2
Às vezes, em testes de unidade, quero afirmar que um erro foi registrado, portanto, esse método não é o ideal. Ainda assim, é uma boa resposta.
Sardathrion - contra abuso do SE
23

Existe uma maneira simples de desativar o log de maneira global, para que os criadores de log específicos do aplicativo não gravem coisas no console quando executo testes?

As outras respostas evitam "gravar coisas no console", configurando globalmente a infraestrutura de log para ignorar qualquer coisa. Isso funciona, mas acho uma abordagem muito franca. Minha abordagem é executar uma alteração na configuração, que faz apenas o necessário para impedir que os logs saiam no console. Então, adiciono um filtro de log personalizado ao meu 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

E eu configuro o log do Django para usar o filtro:

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

Resultado final: quando estou testando, nada vai para o console, mas todo o resto permanece o mesmo.

Por que fazer isso?

Projecto código que contém instruções de registro que são acionadas apenas em circunstâncias específicas e que devem gerar os dados exatos necessários para o diagnóstico, se algo der errado. Portanto, testo que eles fazem o que deveriam e, portanto, desativar completamente o log não é viável para mim. Não quero descobrir, depois que o software estiver em produção, que o que eu pensava que seria registrado não está registrado.

Além disso, alguns executores de teste (nariz, por exemplo) capturam logs durante o teste e emitem a parte relevante do log juntamente com uma falha no teste. É útil descobrir por que um teste falhou. Se o registro estiver completamente desativado, não há nada que possa ser capturado.

Louis
fonte
"Qualquer executor de teste que eu usar carregará o código do Django de uma maneira que o torne verdadeiro." Interessante ... Como?
Webtweakers
Eu tenho um test_settings.pyarquivo que fica ao lado do meu projeto settings.py. Ele é definido para carregar settings.pye fazer algumas mudanças, como conjunto TESTING_MODEpara True. Meus executores de teste estão organizados de modo que test_settingsseja o módulo carregado para as configurações do projeto Django. Há muitas maneiras de fazer isso. Eu costumo definir a variável de ambiente DJANGO_SETTINGS_MODULEcomo proj.test_settings.
Louis
Isso é incrível e faz exatamente o que eu quero. Oculta o log durante unittests até que algo falhe - então o Django Nose pega a saída e a imprime com a falha. Perfeito. Combine com isso para determinar se o teste de unidade está ativo.
Rrauenza
21

Eu gosto da ideia do corredor de teste personalizado de Hassek. Note-se que DjangoTestSuiteRunnernão é mais o executor de teste padrão no Django 1.6+, ele foi substituído pelo DiscoverRunner. Para o comportamento padrão, o executor de teste deve ser mais parecido com:

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
fonte
Encontrei sua solução depois de tentar várias coisas. No entanto, não consigo definir a variável TEST_RUNNER nas configurações, pois não é possível importar o módulo onde está o arquivo test_runner.
Bunny Rabbit
Parece um problema de importação. Você está configurando TEST_RUNNER como um caminho de cadeia para o corredor (não o módulo Python atual)? Além disso, onde está localizado o seu corredor? Eu tenho o meu em um aplicativo separado chamado helpers, que possui apenas utilitários que não importam de qualquer outro lugar dentro do projeto.
Alukach # 15/15
5

Descobri que, para testes em unittestuma estrutura ou similar, a maneira mais eficaz de desativar com segurança o log indesejado em testes de unidade é habilitar / desabilitar os métodos setUp/ tearDownde um caso de teste específico. Isso permite que um destino seja especificamente onde os logs devem ser desativados. Você também pode fazer isso explicitamente no criador de logs da classe que está testando.

import unittest
import logging

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

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

Estou usando um decorador de método simples para desativar o log apenas em um método de teste específico.

def disable_logging(f):

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

        return result

    return wrapper

E então eu o uso como no exemplo a seguir:

class ScenarioTestCase(TestCase):

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

Existe algum método bonito e limpo para suspender o log de testes com o unittest.mock.patchmétodo

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

E python3 -m unittest testsnão produzirá nenhuma saída de log.

valex
fonte
1

Às vezes você deseja os logs e às vezes não. Eu tenho esse código no meusettings.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)

Portanto, se você executar seu teste com as --no-logsopções, obterá apenas os criticallogs:

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

É muito útil se você deseja acelerar os testes no seu fluxo de integração contínua.

Karim N Gorjux
fonte
1

Se você não quiser ativá-lo / desativá-lo repetidamente em setUp () e tearDown () por unittest (não veja a razão disso), faça isso apenas uma vez por 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)
o travesseiro
fonte
1

Nos casos em que desejo suprimir temporariamente um criador de logs específico, escrevi um pequeno gerenciador de contexto que achei útil:

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

Você então usa como:

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

Isso tem a vantagem de que o criador de logs seja reativado (ou retornado ao seu estado anterior) após a withconclusão.

Nathan Villaescusa
fonte
1

Você pode colocar isso no diretório de nível superior para o __init__.pyarquivo de testes de unidade . Isso desativará o registro global no conjunto de testes de unidade.

# tests/unit/__init__.py
import logging

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

No meu caso, eu tenho um arquivo de configurações settings/test.pycriado especificamente para fins de teste, eis como ele se parece:

from .base import *

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

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

LOGGING = {}

Eu coloquei uma variável de ambiente DJANGO_SETTINGS_MODULE=settings.testpara /etc/environment.

Dmitrii Mikhailov
fonte
0

Se você tiver diferentes módulos de inicialização para teste, desenvolvimento e produção, poderá desativar qualquer coisa ou redirecioná-lo no inicializador. Eu tenho local.py, test.py e production.py que todos herdam do common.y

O common.py faz toda a configuração principal, incluindo este trecho:

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

Então no test.py eu tenho o seguinte:

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

Isso substitui o manipulador de console por um FileHandler e ainda significa obter log, mas não preciso tocar na base de código de produção.

Christopher Broderick
fonte
0

Se você estiver usando pytest :

Como o pytest captura as mensagens de log e as exibe apenas para testes com falha, normalmente você não deseja desativar nenhum log. Em vez disso, use um settings.pyarquivo separado para testes (por exemplo, test_settings.py) e adicione-o:

LOGGING_CONFIG = None

Isso diz ao Django para pular a configuração do log completamente. oLOGGING configuração será ignorada e pode ser removida das configurações.

Com essa abordagem, você não obtém nenhum log para testes aprovados e todos os logs disponíveis para testes com falha.

Os testes serão executados usando o log que foi configurado por pytest. Pode ser configurado ao seu gosto nas pytestconfigurações (por exemplo, tox.ini). Para incluir mensagens de log no nível de depuração, use log_level = DEBUG(ou o argumento da linha de comando correspondente).

Roger Dahl
fonte