在Python Django中运行单元测试时,如何禁用日志记录?

我正在使用一个简单的基于单元测试的测试运行器来测试我的Django应用程序。

我的应用程序本身被配置为在settings.py中使用一个基本的日志记录器:

logging.basicConfig(level=logging.DEBUG)

并在我的应用程序代码使用:

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

但是,在运行单元测试时,我想禁用日志记录,这样它就不会使测试结果输出混乱。有没有一种简单的方法以全局方式关闭日志记录,这样当我运行测试时,特定于应用程序的日志记录器就不会把东西写到控制台了?

74928 次浏览
logging.disable(logging.CRITICAL)

将禁用级别小于或等于CRITICAL的所有日志调用。可以重新启用日志记录

logging.disable(logging.NOTSET)

因为你在Django中,你可以添加这些行到你的settings.py:

import sys
import logging


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

这样你就不必在测试中的每一个setUp()中都添加这一行。

您还可以通过这种方式对您的测试需求进行一些方便的更改。

还有另一种“更好”或“更清晰”的方法可以向测试中添加细节,那就是创建自己的测试运行器。

就像这样创建一个类:

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)

现在添加到你的settings.py文件:

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

这让您可以做另一种方法做不到的非常方便的修改,即让Django只测试您想要的应用程序。你可以通过改变test_labels来做到这一点,将这一行添加到测试运行器中:

if not test_labels:
test_labels = ['my_app1', 'my_app2', ...]

我喜欢Hassek的自定义测试运行器的想法。值得注意的是,在Django 1.6+中,DjangoTestSuiteRunner不再是默认的测试运行器,它已经被DiscoverRunner。对于默认行为,测试运行器应该更像:

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)

有没有一种简单的方法以全局方式关闭日志记录,这样当我运行测试时,特定于应用程序的日志记录器就不会把东西写到控制台了?

其他答案通过全局设置日志基础设施以忽略任何内容来防止“将内容写入控制台”。这是可行的,但我觉得这种方法太生硬了。我的方法是执行配置更改,只做防止日志泄漏到控制台上所需的事情。因此,我在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

并且我配置Django日志来使用过滤器:

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

最终结果是:当我在测试时,没有任何内容进入控制台,但其他内容保持不变。

为什么这么做?

我设计了包含日志记录指令的代码,这些指令仅在特定情况下触发,并且在出现问题时输出诊断所需的准确数据。因此,我测验,他们做什么,他们应该做,因此完全禁用日志是不可行的我。我不想在软件投入生产后发现我认为将要记录的内容没有被记录。

此外,一些测试运行程序(例如Nose)将在测试期间捕获日志,并将日志的相关部分与测试失败一起输出。它有助于找出测试失败的原因。如果日志记录完全关闭,那么就无法捕获任何东西。

在我的例子中,我有一个专门为测试目的创建的设置文件settings/test.py,下面是它的样子:

from .base import *


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


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


LOGGING = {}

我将环境变量DJANGO_SETTINGS_MODULE=settings.test放到/etc/environment中。

有时你想要日志,有时不想要。我在settings.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)

所以如果你用--no-logs选项运行测试,你将只得到critical日志:

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

如果您想加快持续集成流的测试,这是非常有用的。

有一些漂亮和干净的方法来暂停测试中的登录unittest.mock.patch方法。

< p > foo.py:

import logging




logger = logging.getLogger(__name__)


def bar():
logger.error('There is some error output here!')
return True
< p > 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())

并且python3 -m unittest tests将不会产生日志输出。

我发现,对于在unittest或类似框架中的测试,在单元测试中安全禁用不必要的日志记录的最有效方法是在特定测试用例的setUp/tearDown方法中启用/禁用。这样就可以明确地锁定应该禁用日志的位置。您也可以在测试类的记录器上显式地执行此操作。

import unittest
import logging


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


def tearDown(self):
logging.disable(logging.NOTSET)

如果你不想在setUp()和tearDown()中反复为unittest打开/关闭它(不知道原因),你可以在每个类中只做一次:

    import unittest
import logging


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

如果你有不同的初始化器模块用于测试,开发和生产,那么你可以禁用任何东西或重定向初始化器。我有local。py, test。py和production。py,它们都继承自common。y

py执行所有主要配置,包括以下代码段:

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

然后在test.py中,我有这个:

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

这将控制台处理程序替换为FileHandler,这意味着仍然可以获得日志记录,但我不必触及生产代码库。

我使用了一个简单的方法装饰器来禁用特定测试方法中的日志记录。

def disable_logging(f):


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


return result


return wrapper

然后我在下面的例子中使用它:

class ScenarioTestCase(TestCase):


@disable_logging
test_scenario(self):
pass

如果你正在使用pytest:

由于pytest捕获日志消息,并且仅在失败的测试中显示它们,因此通常不希望禁用任何日志记录。相反,使用单独的settings.py文件进行测试(例如,test_settings.py),并向其添加:

LOGGING_CONFIG = None

这告诉Django完全跳过日志配置。LOGGING设置将被忽略,并可以从设置中删除。

使用这种方法,您不会获得通过测试的任何日志记录,而会获得失败测试的所有可用日志记录。

测试将使用由pytest设置的日志运行。它可以在pytest设置中根据您的喜好进行配置(例如,tox.ini)。要包含调试级别的日志消息,使用log_level = DEBUG(或相应的命令行参数)。

在我希望暂时抑制特定记录器的情况下,我写了一个我发现有用的小上下文管理器:

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

然后你可以这样使用它:

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

这样做的好处是,一旦with完成,记录器将被重新启用(或设置回其先前的状态)。

你可以把它放在单元测试的顶层目录__init__.py文件中。这将在单元测试套件中禁用全局日志记录。

# tests/unit/__init__.py
import logging


logging.disable(logging.CRITICAL)

我的一些测试包含关于日志输出的断言,因此更改日志级别会破坏它们。相反,我改变了Django的LOGGING设置,在运行测试时使用NullHandler:

if 'test' in sys.argv:
_LOG_HANDLERS = ['null']
else:
_LOG_HANDLERS = ['console']
    

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
},
'loggers': {
'django': {
'handlers': _LOG_HANDLERS,
'propagate': True,
'level': 'INFO',
},
}
}

如果你正在使用pytest,你可以安装超级有用的pytest-mock插件,并定义一个自动使用的会话范围的fixture,可以由env变量触发:

# conftest.py


import os
import pytest




@pytest.fixture(autouse=True, scope="session")
def _shut_logger(session_mocker):
if os.getenv("SHUT_LOGGER", None):
return session_mocker.patch("foo.logger")

禁用整个模块的日志记录:

import logging




def setUpModule():
"""Disable logging while doing these tests."""
logging.disable()




def tearDownModule():
"""Re-enable logging after doing these tests."""
logging.disable(logging.NOTSET)




class TestFoo(unittest.TestCase):


def test_foo(self):
pass

b.h。

对我来说,在conftest.py:

 import confing
# disable logging for tests
[logging.disable(level) for level in [logging.DEBUG,
logging.INFO,
logging.ERROR,
logging.CRITICAL]]

如果你想在2021年或以后考虑这个问题,你可能问错了问题

在现代版本的Django*中,使用开箱即用的配置,测试应该不会在屏幕上产生任何日志记录。因此,如果你在问这个问题,真正的答案可能是“有些东西配置错误”。这是因为(默认情况下):

因此,如果你使用的记录器与你在LOGGING['loggers']中定义的记录器匹配,并且由"console"处理程序处理,测试应该不会在屏幕上产生任何日志记录。

如果你在测试中看到了什么,你也

  • 您的日志记录器的名称与您在settings.LOGGING中定义的东西不匹配
  • 正在使用DEBUG=True运行测试(需要重写)
  • 已经从控制台处理程序的过滤器中删除了"require_debug_true"

*现代版本的意思是:2.1及以上,即不古老。

我们使用structlog,它的禁用有点复杂:

from structlog import DropEvent


def disable_logging_in_tests(_, __, event_dict):
if len(sys.argv) > 1 and sys.argv[1] == 'test':
raise DropEvent
return event_dict




structlog.configure(
processors=[
...
disable_logging_in_tests,
]
...


)