¿Cómo puedo desactivar el registro mientras ejecuto pruebas unitarias en Python Django?

168

Estoy usando un simple corredor de prueba basado en pruebas unitarias para probar mi aplicación Django.

Mi aplicación está configurada para usar un registrador básico en settings.py usando:

logging.basicConfig(level=logging.DEBUG)

Y en mi código de aplicación usando:

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

Sin embargo, cuando ejecuto las pruebas unitarias, me gustaría deshabilitar el registro para que no sature la salida del resultado de mi prueba. ¿Hay una manera simple de desactivar el registro de manera global, de modo que los registradores específicos de la aplicación no escriban cosas en la consola cuando ejecuto las pruebas?

shreddd
fuente
¿Cómo habilitó el registro mientras ejecutaba pruebas? y por qué no usas django LOGGING?
Dalore

Respuestas:

249
logging.disable(logging.CRITICAL)

desactivará todas las llamadas de registro con niveles menos severos o iguales CRITICAL. El registro se puede volver a habilitar con

logging.disable(logging.NOTSET)
unutbu
fuente
42
Esto puede ser obvio, pero me parece útil decir lo obvio para beneficio de otros lectores: colocaría la llamada logging.disable(desde la respuesta aceptada) en la parte superior de tests.pysu aplicación que está haciendo el registro.
CJ Gaconnet
77
Terminé poniendo la llamada en setUp () pero su punto está bien tomado.
Shreddd
en el método setUp () de su prueba, o en la prueba real que genera los mensajes de registro que desea ocultar.
qris
10
Y en su tearDown()método: logging.disable(logging.NOTSET)vuelve a colocar el registro en su lugar perfectamente.
mlissner
34
Ponerlo en init .py del testsmódulo es muy útil.
toabi
46

Como estás en Django, puedes agregar estas líneas a tu settings.py:

import sys
import logging

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

De esa manera, no tiene que agregar esa línea en cada una setUp()de sus pruebas.

También podría hacer un par de cambios útiles para sus necesidades de prueba de esta manera.

Hay otra forma "más agradable" o "más limpia" de agregar detalles a sus pruebas y es hacer su propio corredor de pruebas.

Solo crea una clase 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)

Y ahora agregue a su archivo settings.py:

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

Esto le permite hacer una modificación realmente útil que el otro enfoque no hace, que es hacer que Django solo pruebe las aplicaciones que desea. Puede hacerlo cambiando la test_labelsadición de esta línea al corredor de prueba:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
Hassek
fuente
Claro, ponerlo en settings.py lo haría global.
Shreddd
77
para Django 1.6+, verifique la respuesta @alukach.
Hassek
2
A veces, en las pruebas unitarias, quiero afirmar que se registró un error, por lo que este método no es ideal. Aún así, es una buena respuesta.
Sardathrion - contra el abuso SE
23

¿Existe una manera simple de desactivar el registro de manera global, de modo que los registradores específicos de la aplicación no escriban cosas en la consola cuando ejecuto las pruebas?

Las otras respuestas evitan "escribir cosas en la consola" al configurar globalmente la infraestructura de registro para ignorar cualquier cosa. Esto funciona pero me parece un enfoque demasiado contundente. Mi enfoque es realizar un cambio de configuración que solo haga lo necesario para evitar que los registros salgan de la consola. Entonces agrego un filtro de registro personalizado a mi 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

Y configuro el registro de Django para usar el 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: cuando estoy probando, nada va a la consola, pero todo lo demás permanece igual.

¿Por qué hacer esto?

Diseño un código que contiene instrucciones de registro que se activan solo en circunstancias específicas y que deberían generar los datos exactos que necesito para el diagnóstico si las cosas salen mal. Por lo tanto, pruebo que hacen lo que se supone que deben hacer y, por lo tanto, deshabilitar completamente el registro no es viable para mí. No quiero encontrar una vez que el software esté en producción que lo que pensé que se registraría no se registra.

Además, algunos corredores de prueba (Nose, por ejemplo) capturarán registros durante la prueba y mostrarán la parte relevante del registro junto con una falla de prueba. Es útil para descubrir por qué falló una prueba. Si el registro está completamente desactivado, entonces no hay nada que pueda capturarse.

Louis
fuente
"Cualquier corredor de prueba que use cargará el código de Django de una manera que lo haga verdadero". Interesante ... ¿Cómo?
webtweakers
Tengo un test_settings.pyarchivo que se encuentra al lado de mi proyecto settings.py. Está configurado para cargar settings.pyy hacer algunos cambios, como establecer TESTING_MODEen True. Mis corredores de prueba están organizados para que test_settingssea ​​el módulo cargado para la configuración del proyecto Django. Hay muchas formas en que esto se puede hacer. Por lo general, voy con la configuración de la variable de entorno DJANGO_SETTINGS_MODULEa proj.test_settings.
Louis
Esto es increíble y hace exactamente lo que quiero. Oculta el registro durante las pruebas unitarias hasta que algo falla, luego Django Nose recoge la salida y la imprime con la falla. Perfecto. Combínelo con esto para determinar si las pruebas unitarias están activas.
rrauenza
21

Me gusta la idea del corredor de pruebas personalizado de Hassek. Cabe señalar que DjangoTestSuiteRunnerya no es el corredor de prueba predeterminado en Django 1.6+, ha sido reemplazado por el DiscoverRunner. Para el comportamiento predeterminado, el corredor de prueba debería ser más parecido a:

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
fuente
Encontré tu solución después de probar muchas cosas. Sin embargo, no puedo configurar la variable TEST_RUNNER en la configuración ya que no puede importar el módulo donde está el archivo test_runner.
Bunny Rabbit
Suena como un problema de importación. ¿Está configurando TEST_RUNNER en una ruta de cadena para el corredor (no el módulo Python real)? Además, ¿dónde está ubicado tu corredor? Tengo el mío en una aplicación separada llamada helpers, que solo tiene utilidades que no importan desde ningún otro lugar dentro del proyecto.
alukach
5

He descubierto que para las pruebas dentro de unittestun marco o similar, la forma más efectiva de deshabilitar de forma segura el registro no deseado en las pruebas unitarias es habilitar / deshabilitar los métodos setUp/ tearDownde un caso de prueba en particular. Esto permite un objetivo específico donde los registros deben estar deshabilitados. También puede hacer esto explícitamente en el registrador de la clase que está probando.

import unittest
import logging

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

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

Estoy usando un decorador de métodos simple para deshabilitar el registro solo en un método de prueba en particular.

def disable_logging(f):

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

        return result

    return wrapper

Y luego lo uso como en el siguiente ejemplo:

class ScenarioTestCase(TestCase):

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

Hay un método bonito y limpio para suspender el inicio de sesión en las pruebas con el 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())

Y python3 -m unittest testsno producirá salida de registro.

valex
fuente
1

Algunas veces quieres los registros y otras veces no. Tengo este código en misettings.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)

Entonces, si ejecuta su prueba con las --no-logsopciones, obtendrá solo los criticalregistros:

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

Es muy útil si desea acelerar las pruebas en su flujo de integración continua.

Karim N Gorjux
fuente
1

Si no lo desea, enciéndalo / desactívelo repetidamente en setUp () y tearDown () para unittest (no veo el motivo), puede hacerlo una vez por clase:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)
la almohada
fuente
1

En los casos en que deseo suprimir temporalmente un registrador específico, he escrito un pequeño administrador de contexto que he encontrado ú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

Luego lo usas como:

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

Esto tiene la ventaja de que el registrador se vuelve a habilitar (o se restablece a su estado anterior) una vez que se withcompleta.

Nathan Villaescusa
fuente
1

Puede poner esto en el directorio de nivel superior para el __init__.pyarchivo de pruebas unitarias . Esto deshabilitará el registro global en el conjunto de pruebas unitarias.

# tests/unit/__init__.py
import logging

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

En mi caso, tengo un archivo de configuración settings/test.pycreado específicamente para fines de prueba, así es como se ve:

from .base import *

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

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

LOGGING = {}

Puse una variable de entorno DJANGO_SETTINGS_MODULE=settings.testa /etc/environment.

Dmitrii Mikhailov
fuente
0

Si tiene diferentes módulos de inicialización para prueba, desarrollo y producción, puede deshabilitar cualquier cosa o redirigirla en el servidor inicial. Tengo local.py, test.py y production.py que todos heredan de common.y

common.py hace toda la configuración principal, incluido este fragmento:

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

Luego en test.py tengo esto:

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

Esto reemplaza el controlador de la consola con un FileHandler y significa que todavía tengo que iniciar sesión, pero no tengo que tocar la base del código de producción.

Christopher Broderick
fuente
0

Si estas usando pytest :

Dado que pytest captura mensajes de registro y solo los muestra para pruebas fallidas, por lo general, no desea deshabilitar ningún registro. En su lugar, use un settings.pyarchivo separado para las pruebas (por ejemplo, test_settings.py) y agréguelo:

LOGGING_CONFIG = None

Esto le dice a Django que omita la configuración del registro por completo. losLOGGING configuración se ignorará y se puede eliminar de la configuración.

Con este enfoque, no obtiene ningún registro para las pruebas aprobadas, y obtiene todos los registros disponibles para las pruebas fallidas.

Las pruebas se ejecutarán utilizando el registro configurado por pytest. Se puede configurar a su gusto en la pytestconfiguración (por ejemplo, tox.ini). Para incluir mensajes de registro de nivel de depuración, use log_level = DEBUG(o el argumento de línea de comando correspondiente).

Roger Dahl
fuente