You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python单元测试中如何Mock logging模块FileHandler的输出完成校验

为什么你之前的patch方式不生效

FileHandler 内部是通过调用内置函数 open() 获得文件流实例的,你直接patch TextIOWrapper 类无法生效,一方面是patch路径不对,另一方面StringIO初始化参数和真实文件流不匹配,open()传入的文件名、打开模式等参数StringIO无法处理,自然会初始化失败。

可行方案(不需要复杂的patch操作)


方案1:无侵入替换已有FileHandler的流对象

不需要修改原有日志配置逻辑,只需要在创建完FileHandler后,手动替换它的stream属性为自定义的StringIO实例即可,logging写日志时只会操作handler的stream属性,不关心它的真实类型。
修改后的测试代码如下:

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

import logutils.colorize


class TestMain(unittest.TestCase):
    # 修改_setup_logger返回创建的两个handler
    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)
        
        return log_stdout_handler, log_file_handler

    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)
    def test_log_stdout_info_file_debug_then_messages_logged_appropriately(self, mock_stdout: io.StringIO):
        _, file_handler = self._setup_logger("WARNING", "DEBUG")
        # 自定义StringIO替换原文件流
        mock_file = io.StringIO()
        original_stream = file_handler.stream
        file_handler.stream = mock_file
        
        try:
            self._log_messages()

            stdout_output = mock_stdout.getvalue()
            stdout_lines = stdout_output.splitlines()
            self.assertEqual(len(stdout_lines), 3)

            file_output = mock_file.getvalue()
            file_lines = file_output.splitlines()
            self.assertEqual(len(file_lines), 5)
        finally:
            # 测试结束恢复资源,避免影响其他用例
            mock_file.close()
            file_handler.stream = original_stream
            file_handler.close()
            os.remove("test.log")
            # 清空root logger的handler,避免多个测试用例重复添加
            logging.getLogger().handlers.clear()

方案2:测试场景直接用StreamHandler模拟FileHandler(更推荐)

调整日志初始化逻辑,支持传入自定义的文件流,测试时直接传入StringIO,全程不会产生真实的磁盘文件,测试无副作用、速度更快。
修改后的代码如下:

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

import logutils.colorize


class TestMain(unittest.TestCase):
    # 新增file_stream参数,支持传入自定义流
    def _setup_logger(self, stdout_level: str, file_level: str, file_stream = None):
        log_stdout_handler = logutils.colorize.ColorizingStreamHandler(sys.stdout)
        log_stdout_handler.setFormatter(logging.Formatter("%(message)s"))
        log_stdout_handler.setLevel(stdout_level)

        # 有自定义流就用StreamHandler,否则走原FileHandler逻辑
        if file_stream:
            log_file_handler = logging.StreamHandler(file_stream)
        else:
            try:
                os.unlink("test.log")
            except FileNotFoundError:
                pass
            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)
    def test_log_stdout_info_file_debug_then_messages_logged_appropriately(self, mock_stdout: io.StringIO):
        mock_file = io.StringIO()
        self._setup_logger("WARNING", "DEBUG", file_stream=mock_file)
        self._log_messages()

        stdout_output = mock_stdout.getvalue()
        stdout_lines = stdout_output.splitlines()
        self.assertEqual(len(stdout_lines), 3)

        file_output = mock_file.getvalue()
        file_lines = file_output.splitlines()
        self.assertEqual(len(file_lines), 5)
        
        # 测试结束清理资源
        mock_file.close()
        logging.getLogger().handlers.clear()

补充注意点

每次测试结束后一定要清空root logger的handler列表,否则多个测试用例会重复添加handler,导致日志重复输出、断言失败。

内容的提问来源于stack exchange,提问作者Konrad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 12:18:01