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
相关产品推荐
相关产品推荐

