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

How to get python unittest to show log messages only on failed tests

问题

我一直在尝试使用 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)
测试输出
$ 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)

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

示例代码的解决方案

就在测试运行之前,我们需要更新日志处理程序上的流以指向缓冲区 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)

如何以及为什么

这个问题是 unittest 如何为测试捕获 stdoutstderr 以及 logging 通常如何设置的副作用。通常 logging 在程序执行的早期设置,这意味着日志处理程序将在其实例 (code link). However, just before the test runs, unittest creates a io.StringIO() buffer for both streams and reassigns sys.stdout and sys.stderr to the new buffers (code link) 中存储对 sys.stdoutsys.stderr 的引用。

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

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

上面解决方案中的 LoggerRedirector class 只是提供了方便的方法来将所有可能指向 sys.stdoutsys.stderr 的处理程序重新分配给新的缓冲区unittest 已经设置好了,然后是恢复它们的简单方法。因为在 setUp() 运行时,unittest 已经重新分配了 sys.stdoutsys.stderr 我们正在使用它们来引用 unittest 已经设置的新缓冲区。