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

如何用Mock/Patches替代文件操作测试Python的create_logger函数

问题描述

我正尝试为自定义的create_logger函数编写单元测试,原函数代码如下:

def create_logger(path, file_name):
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    formatter = logging.Formatter(
        fmt="%(asctime)s %(levelname)s %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    fileHandler = logging.FileHandler(
        f"{path}/{file_name}.log", "w"
    )

    streamHandler = logging.StreamHandler()

    fileHandler.setFormatter(formatter)
    streamHandler.setFormatter(formatter)

    logger.addHandler(fileHandler)
    logger.addHandler(streamHandler)
    return logger

我已编写的单元测试会实际创建目录和日志文件,代码如下:

import os
import shutil
import unittest
from logger import create_logger

os.makedirs("unit_test", exist_ok=True)


class TestCreateLogger(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.logger = create_logger("unit_test", "logs")

    @classmethod
    def tearDownClass(cls):
        if os.path.isdir("unit_test"):
            shutil.rmtree("unit_test")
        else:
            raise Exception("The directory does not exist")
        del cls.logger

    def test_log_level(self):
        self.logger.info("First message")
        self.logger.debug("Second message")
        with open("unit_test/logs.log", "r") as fd:
            log_msg = fd.read()
        self.assertIn("First message", log_msg)
        self.assertNotIn("Second message", log_msg)

    def test_log_level_change(self):
        self.logger.setLevel("DEBUG")
        self.logger.info("First message")
        self.logger.debug("Second message")
        with open("unit_test/logs.log", "r") as fd:
            log_msg = fd.read()
        self.assertIn("First message", log_msg)
        self.assertIn("Second message", log_msg)

现在想去掉实际的文件操作,改用Mock/Patches实现相同测试逻辑,该怎么做?

解决方案

可以通过unittest.mock模块的patch来模拟logging.FileHandler和相关文件操作,同时捕获日志输出,不需要实际创建文件。具体实现如下:

改造后的测试代码

import unittest
from unittest.mock import patch, MagicMock
import logging
from logger import create_logger


class TestCreateLogger(unittest.TestCase):
    def setUp(self):
        # 每次测试前重置全局logger的handler,避免不同用例互相干扰
        self.root_logger = logging.getLogger()
        self.original_handlers = self.root_logger.handlers.copy()
        self.root_logger.handlers.clear()

    def tearDown(self):
        # 测试结束后恢复全局logger的原始handler
        self.root_logger.handlers = self.original_handlers

    @patch('logging.FileHandler')
    def test_log_level(self, mock_file_handler):
        # 创建FileHandler的mock实例,捕获emit方法的调用
        mock_handler_instance = MagicMock()
        mock_file_handler.return_value = mock_handler_instance

        # 调用目标函数创建logger
        logger = create_logger("unit_test", "logs")

        # 验证logger初始级别为INFO
        self.assertEqual(logger.level, logging.INFO)

        # 发送测试日志
        logger.info("First message")
        logger.debug("Second message")

        # 验证只有INFO消息被处理
        self.assertEqual(mock_handler_instance.emit.call_count, 1)
        log_record = mock_handler_instance.emit.call_args[0][0]
        self.assertIn("First message", log_record.getMessage())

        # 验证DEBUG消息未被处理
        debug_calls = [call for call in mock_handler_instance.emit.call_args_list 
                       if "Second message" in call[0][0].getMessage()]
        self.assertEqual(len(debug_calls), 0)

    @patch('logging.FileHandler')
    def test_log_level_change(self, mock_file_handler):
        mock_handler_instance = MagicMock()
        mock_file_handler.return_value = mock_handler_instance

        logger = create_logger("unit_test", "logs")
        # 修改日志级别为DEBUG
        logger.setLevel(logging.DEBUG)

        # 发送测试日志
        logger.info("First message")
        logger.debug("Second message")

        # 验证两条消息都被处理
        self.assertEqual(mock_handler_instance.emit.call_count, 2)
        log_messages = [call[0][0].getMessage() for call in mock_handler_instance.emit.call_args_list]
        self.assertIn("First message", log_messages)
        self.assertIn("Second message", log_messages)

关键说明

  • Mock文件操作:通过@patch('logging.FileHandler')替换标准库中的文件处理器,避免实际创建文件和目录
  • 捕获日志内容:模拟FileHandler的emit方法,获取日志记录对象后,调用getMessage()即可得到实际日志文本
  • 隔离测试环境:每次测试前后重置并恢复全局logger的handler,防止不同测试用例的日志配置互相影响
  • 完全无文件操作:整个测试过程不会生成任何实际文件,所有文件相关逻辑都被Mock替代

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 17:24:30