首页 > 解决方案 > 在 Python 单元测试中模拟日志输出

问题描述

我正在尝试验证 Python 应用程序中的日志输出,该应用程序将日志消息写入具有不同级别的两个处理程序 - stdout 和一个文件。

使用 @mock.patch 来修补 sys.stdout 和 io.StringIO 我能够验证那里写入了适当的消息。我可以类似地定位日志模块使用的 TextIO 并将我自己的 StringIO 对象放在那里吗?

import io
import logging
import os
import sys
import unittest.mock

import logutils.colorize


class TestMain(unittest.TestCase):
    def _setup_logger(self, stdout_level: str, file_level: str):
        try:
            os.unlink("test.log")
        except FileNotFoundError:
            pass

        log_stdout_handler = logutils.colorize.ColorizingStreamHandler(sys.stdout)
        log_stdout_handler.setFormatter(logging.Formatter("%(message)s"))
        log_stdout_handler.setLevel(stdout_level)

        log_file_handler = logging.FileHandler(filename="test.log", mode="a", encoding="utf8")
        log_file_handler.setFormatter(logging.Formatter("%(message)s"))
        log_file_handler.setLevel(file_level)

        _root_logger = logging.getLogger()
        _root_logger.addHandler(log_stdout_handler)
        _root_logger.addHandler(log_file_handler)
        _root_logger.setLevel(logging.DEBUG)

    def _log_messages(self):
        log = logging.getLogger("test")
        log.debug("debug")
        log.info("info")
        log.warning("warning")
        log.error("error")
        log.critical("critical")

    @unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
    @unittest.mock.patch("?", new_callable=io.StringIO)  # <--- can I target FileHandler's io.TextIOWrapper here?
    def test_log_stdout_info_file_debug_then_messages_logged_appropriately(self, mock_file: io.StringIO, mock_stdout: io.StringIO):
        self._setup_logger("WARNING", "DEBUG")
        self._log_messages()

        stdout_output = mock_stdout.getvalue()
        stdout_lines = stdout_output.splitlines()
        self.assertEqual(len(stdout_lines), 3)  # irl i would probably regex match the messages

        file_output = mock_file.getvalue()
        file_lines = file_output.splitlines()
        self.assertEqual(len(file_lines), 5)

步入logging\__init.py:1054我可以看到FileHandler的流字段是 的一个实例_io.TextIOWrapper,但是做@unittest.mock.patch("_io.TextIOWrapper", new_callable=io.StringIO)不起作用。也不使用io.TextIOWrapper,logging.io.TextIOWrapperlogging._io.TextIOWrapper.

或者也许有更好的方法来完成我所需要的?

标签: pythonunit-testingmocking

解决方案


我对此一无所获,所以我最终做的是模拟emit日志处理程序的函数(以及 的_open函数FileHandler,因为我不希望创建任何日志文件),并对模拟进行验证。

def _assertEmitted(self, emit_mock: unittest.mock.MagicMock, messages: list[str]) -> typing.NoReturn:
    self.assertEqual(emit_mock.call_count, len(messages))
    for index, message in enumerate(messages):
        self.assertEqual(emit_mock.call_args_list[index].args[0].msg, message)

@unittest.mock.patch.object(logutils.colorize.ColorizingStreamHandler, "emit")
@unittest.mock.patch.object(logging.FileHandler, "emit")
@unittest.mock.patch.object(logging.FileHandler, "_open")
def test_given_log_levels_when_logging_then_appropriate_messages_logged(self, _, file_emit, stdout_emit):
    # setup logger
    stdout_handler = logutils.colorize.ColorizingStreamHandler(sys.stdout)
    stdout_handler.setFormatter(logging.Formatter("..."))
    stdout_handler.setLevel("INFO")
    file_handler = logging.FileHandler(filename="doesn't matter", mode="a", encoding="utf8")
    file_handler.setFormatter(logging.Formatter("..."))
    file_handler.setLevel("DEBUG")
    root_logger = logging.getLogger()
    root_logger.addHandler(log_stdout_handler)
    root_logger.addHandler(log_file_handler)
    root_logger.setLevel(logging.DEBUG)

    # do log calls here
    log = logging.getLogger("test")
    log.info("info")
    log.debug("debug")

    self._assertEmitted(stdout_emit, ["info"])
    self._assertEmitted(file_emit, ["info", "debug"])

推荐阅读