2. Python unittest.mock入门:安装与导入、Mock对象创建、基本断言方法
unittest.mock是Python标准库中用于测试替身的核心模块。通过在测试中替换真实依赖,让被测单元与外界解耦,从而实现快速、可控、可重复的单元测试。本章从零开始搭建mock环境,掌握Mock对象的创建与基础断言。
核心原则:Mock对象模拟真实对象的行为——记录调用、返回预设值、验证交互,而不执行真实逻辑。
2.1 安装与导入
Python 3.3+内置unittest.mock模块,无需额外安装。直接导入即可使用:
# 标准导入方式
from unittest.mock import Mock, MagicMock, patch, call, sentinel
# 或导入整个模块
import unittest.mock
若使用Python 3.2及更早版本,需通过pip安装第三方backport:
pip install mock
导入后,通过mock.__version__确认版本(仅对第三方mock包生效)。
2.2 Mock对象创建
Mock对象有两种创建方式:直接实例化或通过工厂方法。
| 方式 | 代码示例 | 说明 |
|---|---|---|
| 直接实例化 | m = Mock() | 创建无配置的Mock对象 |
| 带参数 | m = Mock(return_value=42) | 设置默认返回值 |
| MagicMock | mm = MagicMock() | 支持魔术方法(如__len__、__iter__) |
| 指定name | m = Mock(name='db_client') | 调试时显示名称 |
| 指定spec | m = Mock(spec=SomeClass) | 限制只有spec中存在的属性/方法可用 |
实战推荐:测试接口类时优先使用
spec参数,防止调用了真实对象不存在的方法而通过测试。
2.3 基本断言方法
Mock对象自动记录所有调用,通过断言方法验证调用是否如预期发生。主要断言方法见下表:
| 断言方法 | 作用 | 示例 |
|---|---|---|
assert_called() | 至少被调用一次 | mock.assert_called() |
assert_called_once() | 仅被调用一次 | mock.assert_called_once() |
assert_called_with(*args, **kwargs) | 最近一次调用使用指定参数 | mock.assert_called_with(1, key='val') |
assert_called_once_with(*args, **kwargs) | 仅调用一次且参数匹配 | mock.assert_called_once_with(42) |
assert_any_call(*args, **kwargs) | 曾经有某次调用匹配参数(不限顺序) | mock.assert_any_call('start') |
assert_not_called() | 从未被调用 | mock.assert_not_called() |
assert_has_calls(calls, any_order=False) | 按顺序(或任意顺序)包含指定调用列表 | mock.assert_has_calls([call(1), call(2)]) |
配合call对象可以构建调用列表进行比较:
from unittest.mock import call
mock = Mock()
mock(1)
mock(2, x=3)
mock(4)
# 获取所有调用记录
print(mock.call_args_list)
# 输出: [call(1), call(2, x=3), call(4)]
# 断言调用顺序
mock.assert_has_calls([call(1), call(2, x=3), call(4)])
2.4 设置返回值与副作用
除了断言,Mock对象还需模拟被替换对象的返回值。通过return_value和side_effect控制:
| 属性/方法 | 作用 | 示例 |
|---|---|---|
return_value | 固定返回值 | mock.return_value = 42 |
side_effect | 可迭代对象、异常或函数 | mock.side_effect = [1, 2, 3]或 mock.side_effect = ValueError('bad') |
side_effect为函数 | 根据输入参数动态返回 | mock.side_effect = lambda x: x * 2 |
# 场景:模拟数据库查询,第一次返回空列表,第二次返回一条记录
mock_query = Mock()
mock_query.side_effect = [[], [{'id': 1}]]
assert mock_query() == []
assert mock_query() == [{'id': 1}]
常见陷阱:
side_effect优先级高于return_value。若同时设置,side_effect生效。side_effect耗尽后若仍被调用,会触发StopIteration异常。
2.5 属性模拟与魔术方法
Mock对象可以动态添加属性,或使用MagicMock支持魔术方法:
# 普通Mock模拟属性
mock = Mock()
mock.some_attr = "hello"
mock.configure_mock(other_attr=100)
# MagicMock支持魔术方法
mm = MagicMock()
mm.__len__.return_value = 5
mm.__iter__.return_value = iter([1, 2, 3])
assert len(mm) == 5
assert list(mm) == [1, 2, 3]
2.6 完整实战示例
下面通过一个用户服务测试案例演示Mock的基本用法:
# 被测试的模块 user_service.py
class UserService:
def __init__(self, db_client):
self.db = db_client
def get_user(self, user_id):
result = self.db.query("SELECT * FROM users WHERE id=?", [user_id])
if result:
return {"id": result[0][0], "name": result[0][1]}
return None
# 测试代码 test_user_service.py
from unittest.mock import Mock
from user_service import UserService
def test_get_user_returns_user_dict():
# Arrange: 创建Mock数据库客户端,模拟查询返回
mock_db = Mock()
mock_db.query.return_value = [(1, "Alice")]
service = UserService(mock_db)
# Act
user = service.get_user(1)
# Assert
assert user == {"id": 1, "name": "Alice"}
mock_db.query.assert_called_once_with(
"SELECT * FROM users WHERE id=?", [1]
)
def test_get_user_returns_none_when_not_found():
mock_db = Mock()
mock_db.query.return_value = []
service = UserService(mock_db)
user = service.get_user(999)
assert user is None
mock_db.query.assert_called_once()
2.7 常见错误与调试
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| 断言通过但实际不调用 | 未正确注入Mock对象 | 打印mock.mock_calls查看实际调用 |
| AttributeError: Mock object has no attribute 'xxx' | 访问了未设置的属性,且spec限制 | 检查spec类是否包含该属性,或移除spec |
| 调用统计偏差 | Mock被意外复用 | 每个测试用例使用新的Mock实例 |
| side_effect耗尽后异常 | side_effect列表长度小于调用次数 | 改用side_effect函数动态生成 |
调试技巧:在测试临时添加
print(mock.method_calls)或print(mock.call_args_list),可以快速确认Mock的实际交互。
2.8 性能与稳定性要点
Mock对象本身轻量,但大规模使用时的注意事项:
- 避免过度Mock:只模拟直接依赖,不Mock内部实现细节,否则测试与实现耦合,重构时容易断裂。
- 使用
spec提升稳定性:若真实接口变更(如新增参数),Mock无spec则测试仍通过,产生假阳性。指定spec能让Mock在运行时校验接口一致性。 - 清理Mock避免泄漏:使用
patch时建议用上下文管理器或decorator,确保测试结束后自动恢复。 - 避免共享状态:每个测试用例创建独立的Mock对象,不要全局复用,防止调用计数污染。
2.9 本章小结
通过本章学习,你应该掌握:
- 导入unittest.mock模块(Python ≥ 3.3直接导入,老旧版本pip install mock)
- 使用
Mock()和MagicMock()创建测试替身,并通过return_value、side_effect、spec配置行为 - 运用
assert_called_with、assert_called_once_with、assert_has_calls等基本断言方法验证调用 - 结合
call对象分析调用历史 - 遵循最佳实践避免常见陷阱
下一阶段将在第3章深入patch函数的使用,实现依赖注入的自动化替换,进一步简化测试准备代码。