pin_drop当前位置:知识文库 ❯ 图文

Python Mock 对象完全教程 - 模拟依赖的测试利器

一、Mock 对象概述

Mock 对象是单元测试中用于模拟真实依赖的替代对象。当你需要测试的代码依赖于外部服务(如数据库、API、文件系统)时,Mock 对象可以模拟这些依赖的行为,使测试变得快速、可靠且独立。

使用 Mock 的主要场景:

  • 外部 API 调用:模拟 HTTP 请求,避免网络依赖

  • 数据库操作:模拟数据库查询,避免测试数据污染

  • 文件系统操作:模拟文件读写,避免真实文件操作

  • 时间相关:模拟当前时间,测试时间敏感逻辑

  • 第三方库:模拟复杂第三方库的行为


二、unittest.mock 核心组件

Python 标准库提供了 unittest.mock 模块,包含以下核心组件:

组件 说明
Mock 创建模拟对象,记录所有交互
MagicMock Mock 的子类,支持魔法方法(如 __str__)
patch 替换对象/方法/属性为 Mock
patch.object 替换对象的特定属性
patch.dict 临时修改字典内容

三、基本用法与实战示例

1. 创建 Mock 对象

代码示例

from unittest.mock import Mock

# 创建最简单的 Mock
mock_obj = Mock()

# 设置返回值
mock_obj.get_user.return_value = {"id": 1, "name": "张三"}

# 调用 Mock 方法
result = mock_obj.get_user(1)
print(result)  # {'id': 1, 'name': '张三'}

# 验证方法被调用过
mock_obj.get_user.assert_called_once_with(1)

# 查看调用次数
print(mock_obj.get_user.call_count)  # 1

# 查看调用参数
print(mock_obj.get_user.call_args)  # call(1)

2. 模拟 API 客户端

代码示例

from unittest.mock import Mock
import unittest

# 假设这是我们的业务代码
class UserService:
    def __init__(self, api_client):
        self.api_client = api_client

    def get_user_info(self, user_id):
        response = self.api_client.get(f"/users/{user_id}")
        if response["status"] == 200:
            return response["data"]
        return None

# 测试代码
class TestUserService(unittest.TestCase):
    def test_get_user_info(self):
        # 创建 Mock API 客户端
        mock_client = Mock()
        mock_client.get.return_value = {
            "status": 200,
            "data": {"id": 1, "name": "张三", "email": "zhangsan@example.com"}
        }

        # 注入 Mock 并测试
        service = UserService(mock_client)
        result = service.get_user_info(1)

        # 验证结果
        self.assertEqual(result["name"], "张三")
        mock_client.get.assert_called_once_with("/users/1")

    def test_get_user_not_found(self):
        mock_client = Mock()
        mock_client.get.return_value = {"status": 404, "data": None}

        service = UserService(mock_client)
        result = service.get_user_info(999)

        self.assertIsNone(result)

3. 模拟异常

代码示例

from unittest.mock import Mock
import unittest

class TestExceptions(unittest.TestCase):
    def test_api_error(self):
        mock_client = Mock()
        # 模拟抛出异常
        mock_client.get.side_effect = ConnectionError("网络错误")

        service = UserService(mock_client)

        with self.assertRaises(ConnectionError):
            service.get_user_info(1)

四、patch 装饰器详解

1. patch 装饰器

代码示例

from unittest.mock import patch
import unittest
from datetime import datetime

# 业务代码
def is_business_hours():
    """判断是否在营业时间(9点-18点)"""
    now = datetime.now()
    return 9 <= now.hour < 18

# 测试代码
class TestBusinessHours(unittest.TestCase):

    @patch('my_module.datetime')
    def test_during_business_hours(self, mock_datetime):
        """模拟当前时间为上午10点"""
        mock_datetime.now.return_value = datetime(2026, 1, 1, 10, 0)
        self.assertTrue(is_business_hours())

    @patch('my_module.datetime')
    def test_outside_business_hours(self, mock_datetime):
        """模拟当前时间为晚上20点"""
        mock_datetime.now.return_value = datetime(2026, 1, 1, 20, 0)
        self.assertFalse(is_business_hours())

2. patch 作为上下文管理器

代码示例

from unittest.mock import patch

def test_with_context_manager():
    """使用 with 语句的 patch"""
    with patch('my_module.requests.get') as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {"key": "value"}

        # 在这里进行测试
        result = some_function_using_requests()
        assert result == {"key": "value"}

        mock_get.assert_called_once()

3. patch.object

代码示例

from unittest.mock import patch
import unittest

class EmailService:
    def send_email(self, to, subject, body):
        # 实际发送邮件的逻辑
        pass

    def validate_email(self, email):
        return "@" in email

class TestEmailService(unittest.TestCase):

    @patch.object(EmailService, 'send_email')
    def test_send_welcome_email(self, mock_send):
        service = EmailService()
        service.send_email("user@example.com", "欢迎", "欢迎加入我们!")

        mock_send.assert_called_once_with(
            "user@example.com", "欢迎", "欢迎加入我们!"
        )

4. patch.dict

代码示例

import os
from unittest.mock import patch
import unittest

class TestEnvironment(unittest.TestCase):

    @patch.dict(os.environ, {"APP_ENV": "testing", "DEBUG": "true"})
    def test_testing_environment(self):
        """临时修改环境变量"""
        self.assertEqual(os.environ["APP_ENV"], "testing")
        self.assertEqual(os.environ["DEBUG"], "true")

    def test_env_restored(self):
        """验证 patch.dict 结束后环境变量已恢复"""
        # 如果原来没有 APP_ENV,这里会恢复
        pass

五、高级技巧与最佳实践

1. side_effect 的高级用法

代码示例

from unittest.mock import Mock

mock = Mock()

# side_effect 为列表 - 每次调用返回不同值
mock.func.side_effect = [1, 2, 3]
print(mock.func())  # 1
print(mock.func())  # 2
print(mock.func())  # 3

# side_effect 为函数 - 根据输入动态返回
def dynamic_response(x):
    return x * 2

mock.calculate.side_effect = dynamic_response
print(mock.calculate(5))  # 10
print(mock.calculate(3))  # 6

2. spec 参数限制 Mock

代码示例

from unittest.mock import Mock

class Database:
    def connect(self): pass
    def query(self, sql): pass
    def close(self): pass

# 使用 spec 限制 Mock 只能访问指定类的属性
mock_db = Mock(spec=Database)
mock_db.query.return_value = [{"id": 1}]

# 访问 spec 中不存在的方法会报错
# mock_db.delete()  # AttributeError: Mock object has no attribute 'delete'

3. pytest 中的 mock

代码示例

# pip install pytest-mock
import pytest

def test_with_pytest_mock(mocker):
    """pytest 的 mocker fixture"""
    # mocker 是 unittest.mock.patch 的简化版
    mock_api = mocker.patch('my_module.requests.get')
    mock_api.return_value.json.return_value = {"status": "ok"}

    result = my_module.fetch_data()
    assert result == {"status": "ok"}
    mock_api.assert_called_once()

注意1:patch 的目标路径:patch 应该指向被测试代码导入的位置,而不是原始定义位置。如果代码中使用了 from module import func,则 patch 的目标应该是 测试模块.func

注意2:避免过度 Mock:Mock 应该只用于隔离外部依赖,不要 Mock 被测试代码本身的逻辑。过度 Mock 会导致测试变得脆弱且失去意义。

练习1

为一个订单处理系统编写测试,使用 Mock 模拟支付网关(成功/失败/超时三种场景),验证订单状态的正确更新。

练习2

使用 patch 装饰器和上下文管理器两种方式,模拟文件读取操作,测试一个读取配置文件并解析的函数。


六、课程小结

  • Mock 隔离外部依赖:让测试独立、快速、可重复

  • return_value 和 side_effect:控制 Mock 的返回值和行为

  • patch 装饰器:临时替换模块、对象或方法

  • patch 路径很重要:要 patch 被导入的位置而非定义位置

  • assert_called 验证交互:确保 Mock 被正确调用

常见问题

patch 的 target 路径应该如何确定?

patch 的 target 应该是被测试代码导入该对象的位置。如果 module_a 中写了 from module_b import func,则 patch 的目标应该是 module_a.func,而不是 module_b.func。

Mock 和 MagicMock 有什么区别?

MagicMock 是 Mock 的子类,额外支持 Python 的魔法方法(如 __str__、__enter__、__iter__ 等)。大多数情况下推荐使用 MagicMock。

如何防止 Mock 被滥用?

使用 spec 或 autospec 参数限制 Mock 只能模拟指定类的属性。避免 Mock 被测试代码内部的逻辑。只在需要隔离外部依赖时使用 Mock。

pytest-mock 和 unittest.mock 该如何选择?

如果使用 pytest,推荐使用 pytest-mock(提供 mocker fixture,语法更简洁)。如果使用 unittest 或需要标准库方案,使用 unittest.mock。两者底层是相同的实现。

标签: Mock对象 unittest.mock patch 模拟测试 pytest-mock 单元测试

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: Python 参数化测试完整教程 - 数据驱动高效测试 下一篇: Python 代码覆盖率完全指南 - 衡量测试质量黄金标准

poll相关推荐