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

Posted

技术标签:

【中文标题】如何在 Python Django 中运行单元测试时禁用日志记录?【英文标题】:How can I disable logging while running unit tests in Python Django? 【发布时间】:2011-07-12 10:57:39 【问题描述】:

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

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

logging.basicConfig(level=logging.DEBUG)

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

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

但是,在运行单元测试时,我想禁用日志记录,以免弄乱我的测试结果输出。有没有一种简单的方法可以以全局方式关闭日志记录,这样应用程序特定的记录器就不会在我运行测试时将内容写入控制台?

【问题讨论】:

您是如何在运行测试时启用日志记录的?为什么不使用 django LOGGING? 【参考方案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,
    ]
    ...

)

【讨论】:

【参考方案2】:

如果您在 2021 年或之后对此感到疑惑,您可能会问错问题

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

Printing of log messages to console requires debug to be True Tests run with DEBUG=False

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

如果你在测试中看到了一些东西,你要么

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

*现代版本含义:2.1 及更高版本,即不是古代版本。

【讨论】:

有一点,不过...... Django 记录设置LOGGING 的方式是完全定义它并覆盖默认设置。一个人需要确保他们以相同的方式重新定义了'console' 处理程序,以便当DEBUG=False 时它仍然有一个过滤器来停止输出。 @TimTisdall 这是正确的,也是一个很好的补充。我忘记了这一点,因为我倾向于通过对默认值进行深度复制来设置我的LOGGING,并且只更改我想要更改的那些部分。【参考方案3】:

b.h.

对我来说效果很好 - 在 conftest.py 中:

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

【讨论】:

【参考方案4】:

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

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

【讨论】:

谢谢,这是我一直在寻找的解决方案。【参考方案5】:

如果您使用 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")

【讨论】:

【参考方案6】:

我的一些测试包含有关日志输出的断言,因此更改日志级别会破坏它们。相反,我在运行测试时更改了我的 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',
        ,
    

【讨论】:

【参考方案7】:

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

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)

【讨论】:

【参考方案8】:

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

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 完成,记录器就会重新启用(或设置回其先前的状态)。

【讨论】:

【参考方案9】:

由于您在 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 中会使其成为全球性的。 对于 Django 1.6+,请查看@alukach 答案。 有时在单元测试中,我想断言记录了一个错误,因此这种方法并不理想。不过,它一个很好的答案。【参考方案10】:

如果您使用的是pytest

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

LOGGING_CONFIG = None

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

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

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

【讨论】:

【参考方案11】:

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

common.py 完成所有主要配置,包括这个 sn-p :

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 替换了控制台处理程序,这意味着仍然可以获取日志记录,但我不必接触生产代码库。

【讨论】:

【参考方案12】:

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

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

【讨论】:

【参考方案13】:

如果您不希望它在 setUp() 和 tearDown() 中重复打开/关闭单元测试(看不出原因),您可以在每个班级执行一次:

    import unittest
    import logging

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

【讨论】:

【参考方案14】:

我发现对于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)

【讨论】:

【参考方案15】:

有一些漂亮而干净的方法可以使用unittest.mock.patch 方法暂停登录测试。

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

python3 -m unittest tests 将不会产生任何日志输出。

【讨论】:

【参考方案16】:

是否有一种简单的方法可以以全局方式关闭日志记录,以便在我运行测试时特定于应用程序的记录器不会将内容写入控制台?

其他答案通过将日志基础设施全局设置为忽略任何内容来防止“将内容写入控制台”。这行得通,但我觉得这种方法太生硬了。我的方法是执行配置更改,该更改仅执行防止日志从控制台流出所需的操作。所以我将custom logging filter 添加到我的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

我 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)会在测试期间捕获日志,并将日志的相关部分与测试失败一起输出。它有助于找出测试失败的原因。如果日志记录功能完全关闭,则无法捕获任何内容。

【讨论】:

“我使用的任何测试运行程序都会以一种使其成为 True 的方式加载 Django 代码。”有趣...如何? 我有一个test_settings.py 文件,它位于我项目的settings.py 旁边。它设置为加载settings.py 并进行一些更改,例如将TESTING_MODE 设置为True。我的测试运行器被组织成 test_settings 是为 Django 项目设置加载的模块。有很多方法可以做到这一点。我通常会将环境变量DJANGO_SETTINGS_MODULE 设置为proj.test_settings 这太棒了,完全符合我的要求。在单元测试期间隐藏日志记录,直到出现故障——然后 Django Nose 获取输出并将其与故障一起打印。完美的。将其与this 结合以确定单元测试是否处于活动状态。 我只是浪费了几个小时试图找出我的assertLogs 失败的原因,这是由于logging.disable(logging.CRITICAL)。以某种方式关闭控制台输出比完全破坏日志记录要好得多。 没有内置TESTING_MODE(我刚查过),所以这个需要稍微修改一下。可以在 settings.py 中添加以下内容:TESTING_MODE = 'test' in sys.argv.【参考方案17】:

有时您需要日志,有时则不需要。我的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 我非常喜欢这个答案,因为它可以让您在命令行中控制日志级别。这在禁用 CI 日志记录但保持启用以运行本地测试时很有用。非常感谢。【参考方案18】:

就我而言,我有一个专门为测试目的创建的设置文件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

【讨论】:

【参考方案19】:
logging.disable(logging.CRITICAL)

将禁用所有级别低于或等于CRITICAL 的日志记录调用。可以通过

重新启用日志记录
logging.disable(logging.NOTSET)

【讨论】:

这可能很明显,但我发现有时为了其他读者的利益陈述显而易见的事情是有帮助的:您可以在@987654325 的顶部拨打logging.disable(来自已接受的答案) @ 在您正在执行日志记录的应用程序中。 我最终把电话放在 setUp() 中,但你的观点很好。 在测试的 setUp() 方法中,或在生成要隐藏的日志消息的实际测试中。 在您的 tearDown() 方法中:logging.disable(logging.NOTSET) 将日志记录整齐地放回原处。 放在tests模块的init.py中很有用。【参考方案20】:

我喜欢 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)

【讨论】:

我在尝试了很多事情后找到了您的解决方案。但是我无法在设置中设置变量 TEST_RUNNER,因为它无法导入 test_runner 文件所在的模块。 听起来像是导入问题。您是否将 TEST_RUNNER 设置为运行器的字符串路径(不是实际的 Python 模块)?另外,你的跑步者在哪里?我在一个名为helpers 的单独应用程序中有我的,它只有不从项目中其他任何地方导入的实用程序。

以上是关于如何在 Python Django 中运行单元测试时禁用日志记录?的主要内容,如果未能解决你的问题,请参考以下文章

如何知道在 django 中运行单元测试的环境适用于哪个环境

运行单元测试时禁用 Django South?

如何跳过 Django 中的单元测试?

在 Django 单元测试中使用 mock 修补 celery 任务

Django中的单元测试以及Python单元测试

如何在目录中运行所有 Python 单元测试?