如何让 python unittest 仅在失败的测试中显示日志消息

Sat*_* A. 5 python python-unittest python-logging

问题

我一直在尝试使用unittest --buffer标志来抑制成功测试的日志并显示失败测试的日志。但无论如何它似乎都会显示日志输出。这是日志记录模块的怪癖吗?如何仅在失败的测试中获取日志输出?记录器上是否需要特殊配置?我发现的其他问题和答案采用了暴力方法来禁用测试期间的所有日志记录。

示例代码

import logging
import unittest
import sys

logger = logging.getLogger('abc')

logging.basicConfig(
    format = '%(asctime)s %(module)s %(levelname)s: %(message)s',
    level = logging.INFO,
    stream = sys.stdout)


class TestABC(unittest.TestCase):
    def test_abc_pass(self):
        logger.info('log abc in pass')
        print('print abc in pass')
        self.assertTrue(True)

    def test_abc_fail(self):
        logger.info('log abc in fail')
        print('print abc in fail')
        self.assertTrue(False)
Run Code Online (Sandbox Code Playgroud)
测试输出
$ python -m unittest --buffer
2021-09-15 17:38:48,462 test INFO: log abc in fail
F
Stdout:
print abc in fail
2021-09-15 17:38:48,463 test INFO: log abc in pass
.
======================================================================
FAIL: test_abc_fail (test.TestABC)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test.py", line 22, in test_abc_fail
    self.assertTrue(False)
AssertionError: False is not true

Stdout:
print abc in fail

----------------------------------------------------------------------
Ran 2 tests in 3.401s

FAILED (failures=1)
Run Code Online (Sandbox Code Playgroud)

因此缓冲区确实成功地抑制了print通过测试中语句的输出。但它不会抑制日志输出。

Sat*_* A. 6

示例代码的解决方案

在测试运行之前,我们需要更新日志处理程序上的流,以指向 unittest 为捕获测试输出而设置的缓冲区。

import logging
import unittest
import sys

logger = logging.getLogger('abc')

logging.basicConfig(
    format = '%(asctime)s %(module)s %(levelname)s: %(message)s',
    level = logging.INFO,
    stream = sys.stdout)


class LoggerRedirector:

    # Keep a reference to the real streams so we can revert
    _real_stdout = sys.stdout
    _real_stderr = sys.stderr

    @staticmethod
    def all_loggers():
        loggers = [logging.getLogger()]
        loggers += [logging.getLogger(name) for name in logging.root.manager.loggerDict]
        return loggers

    @classmethod
    def redirect_loggers(cls, fake_stdout=None, fake_stderr=None):
        if ((not fake_stdout or fake_stdout is cls._real_stdout)
             and (not fake_stderr or fake_stderr is cls._real_stderr)):
            return
        for logger in cls.all_loggers():
            for handler in logger.handlers:
                if hasattr(handler, 'stream'):
                    if handler.stream is cls._real_stdout:
                        handler.setStream(fake_stdout)
                    if handler.stream is cls._real_stderr:
                        handler.setStream(fake_stderr)

    @classmethod
    def reset_loggers(cls, fake_stdout=None, fake_stderr=None):
        if ((not fake_stdout or fake_stdout is cls._real_stdout)
             and (not fake_stderr or fake_stderr is cls._real_stderr)):
            return
        for logger in cls.all_loggers():
            for handler in logger.handlers:
                if hasattr(handler, 'stream'):
                    if handler.stream is fake_stdout:
                        handler.setStream(cls._real_stdout)
                    if handler.stream is fake_stderr:
                        handler.setStream(cls._real_stderr)


class TestABC(unittest.TestCase):
    def setUp(self):
        # unittest has reassigned sys.stdout and sys.stderr by this point
        LoggerRedirector.redirect_loggers(fake_stdout=sys.stdout, fake_stderr=sys.stderr)

    def tearDown(self):
        LoggerRedirector.reset_loggers(fake_stdout=sys.stdout, fake_stderr=sys.stderr)
        # unittest will revert sys.stdout and sys.stderr after this

    def test_abc_pass(self):
        logger.info('log abc in pass')
        print('print abc in pass')
        self.assertTrue(True)

    def test_abc_fail(self):
        logger.info('log abc in fail')
        print('print abc in fail')
        self.assertTrue(False)
Run Code Online (Sandbox Code Playgroud)

方式和原因

unittest该问题是如何捕获测试stdoutstderr测试以及logging通常如何设置的副作用。通常是在程序执行的早期就设置的,这意味着日志处理程序将在其实例中存储对其logging实例的引用(代码链接)。但是,在测试运行之前,为两个流创建一个缓冲区并重新分配给新缓冲区(代码链接)。sys.stdoutsys.stderrunittestio.StringIO()sys.stdoutsys.stderr

因此,在测试运行之前,为了捕获unittest日志输出,我们需要告诉日志处理程序将其流指向已unittest设置的缓冲区。测试完成后,流将恢复正常。但是,unittest为每个测试创建一个新的缓冲区,因此我们需要在每次测试之前和之后更新日志处理程序。

由于日志处理程序指向unittest设置的缓冲区,因此如果测试失败,则在使用该--buffer选项时将显示该测试的所有日志。

LoggerRedirector上面的解决方案中的类只是提供了方便的方法来重新分配可能指向的所有处理程序或sys.stdout已设置的sys.stderr新缓冲区,然后提供一种简单的方法来恢复它们。unittest由于到setUp()运行时,unittest已经重新分配sys.stdout,我们使用这些来引用已设置的sys.stderr新缓冲区。unittest