pin_drop当前位置:知识文库 ❯ 图文
Python Mock 对象完全教程 - 模拟依赖的测试利器
一、Mock 对象概述
Mock 对象是单元测试中用于模拟真实依赖的替代对象。当你需要测试的代码依赖于外部服务(如数据库、API、文件系统)时,Mock 对象可以模拟这些依赖的行为,使测试变得快速、可靠且独立。
使用 Mock 的主要场景:
-
外部 API 调用:模拟 HTTP 请求,避免网络依赖
-
数据库操作:模拟数据库查询,避免测试数据污染
-
文件系统操作:模拟文件读写,避免真实文件操作
-
时间相关:模拟当前时间,测试时间敏感逻辑
-
第三方库:模拟复杂第三方库的行为
二、unittest.mock 核心组件
Python 标准库提供了 unittest.mock 模块,包含以下核心组件:
三、基本用法与实战示例
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)) # 62. 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。两者底层是相同的实现。
本文涉及AI创作
内容由AI创作,请仔细甄别