我正在使用一个简单的基于单元测试的测试运行器来测试我的 Django 应用程序。
我的应用程序本身配置为使用 settings.py 中的基本记录器,使用:
logging.basicConfig(level=logging.DEBUG)
在我的应用程序代码中使用:
logger = logging.getLogger(__name__)
logger.setLevel(getattr(settings, 'LOG_LEVEL', logging.DEBUG))
但是,在运行单元测试时,我想禁用日志记录,以免弄乱我的测试结果输出。是否有一种简单的方法可以以全局方式关闭日志记录,以便在我运行测试时特定于应用程序的记录器不会将内容写入控制台?
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', ...]
是否有一种简单的方法可以以全局方式关闭日志记录,以便在我运行测试时特定于应用程序的记录器不会将内容写入控制台?
其他答案通过将日志基础设施全局设置为忽略任何内容来防止“将内容写入控制台”。这行得通,但我觉得这种方法太生硬了。我的方法是执行配置更改,该更改仅执行防止日志从控制台流出所需的操作。所以我在我的 settings.py
中添加了一个 custom logging filter:
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
我configure the Django logging使用过滤器:
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.py
旁边有一个 test_settings.py
文件。它设置为加载 settings.py
并进行一些更改,例如将 TESTING_MODE
设置为 True
。我的测试运行器被组织成 test_settings
是为 Django 项目设置加载的模块。有很多方法可以做到这一点。我通常会将环境变量 DJANGO_SETTINGS_MODULE
设置为 proj.test_settings
。
assertLogs
失败的原因,这是由于 logging.disable(logging.CRITICAL)
。以某种方式关闭控制台输出比完全破坏日志记录要好得多。
TESTING_MODE
(我刚刚检查过),所以需要稍微修改一下。可以将以下内容添加到 settings.py:TESTING_MODE = 'test' in sys.argv
。
我喜欢 Hassek 的自定义测试运行器的想法。需要注意的是,DjangoTestSuiteRunner
不再是 Django 1.6+ 中的默认测试运行程序,它已被 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)
helpers
的单独应用程序中有我的,它只有不从项目中其他任何地方导入的实用程序。
我发现对于 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)
有一些漂亮而干净的方法可以使用 unittest.mock.patch
方法暂停登录测试。
foo.py:
import logging
logger = logging.getLogger(__name__)
def bar():
logger.error('There is some error output here!')
return True
测试.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
不会产生任何日志输出。
我正在使用一个简单的方法装饰器来禁用仅在特定测试方法中的日志记录。
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
有时您需要日志,有时则不需要。我的 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.
如果您想加快持续集成流程的测试速度,这将非常有帮助。
manage.py
中,而不是在 settings.py
中
禁用整个模块的日志记录:
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
如果您使用 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',
},
}
}
如果您不希望它在 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)
bh
对我来说效果很好 - 在 conftest.py 中:
import confing
# disable logging for tests
[logging.disable(level) for level in [logging.DEBUG,
logging.INFO,
logging.ERROR,
logging.CRITICAL]]
就我而言,我有一个专门为测试目的而创建的设置文件 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
。
如果您有不同的初始化模块用于测试、开发和生产,那么您可以禁用任何东西或在初始化程序中重定向它。我有 local.py、test.py 和 production.py,它们都继承自 common.y
common.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 替换了控制台处理程序,意味着仍然可以获得日志记录,但我不必接触生产代码库。
如果您使用的是 pytest,您可以安装超级有用的 pytest-mock 插件并定义一个可以由 env var 触发的自动使用、会话范围的固定装置:
# 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")
如果您在 2021 年或之后对此感到疑惑,您可能会问错问题
在现代版本的 Django* 上,使用开箱即用的配置,测试不应在屏幕上产生任何日志记录。因此,如果您提出这个问题,真正的答案可能是“配置错误”。这是因为(默认情况下):
将日志消息打印到控制台需要调试为 True
使用 DEBUG=False 运行测试
因此,如果您使用的记录器与您在 LOGGING['loggers']
中定义并由 "console"
处理程序处理的记录器相匹配,则测试不应在屏幕上产生任何日志记录。
如果你在测试中看到一些东西,你要么
记录器的名称与您在 settings.LOGGING 中定义的名称不匹配
正在使用 DEBUG=True 运行测试(需要覆盖)
已从控制台处理程序的过滤器中删除“require_debug_true”。
*现代版本意味着:2.1 及以上,即不是古代。
LOGGING
的方式是完全定义它并覆盖默认设置。人们需要确保他们以相同的方式重新定义了 'console'
处理程序,以便它仍然有一个过滤器来在 DEBUG=False
时停止输出。
LOGGING
,并且只更改我想要更改的那些部分。
我们使用 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,
]
...
)
tearDown()
方法中:logging.disable(logging.NOTSET)
将日志记录整齐地放回原处。tests
模块的 init.py 中非常有用。