4、创建第一个Mock对象:基本用法和常见属性
4.1 什么是Mock对象
简单说,Mock 就是一个“假货”。它能模仿真实对象的行为,但不会真的去执行那些重活——比如发网络请求、读写数据库、调用第三方服务。我在接手一个老项目时,发现测试一跑就要连线上数据库,慢得让人崩溃。后来把数据库操作全部换成 Mock,测试时间从5分钟缩到几秒。说白了,Mock 就是替身演员,让你能专注测试自己的逻辑。
在 Python 里,最常用的就是 unittest.mock 模块。它内置了一套完整的 Mock 体系,不用装第三方库。你想想看,写单元测试时最烦什么?——外部依赖。Mock 专治这个。
4.2 创建你的第一个Mock
创建 Mock 对象真的很简单,一行代码而已:
from unittest.mock import Mock
mock_obj = Mock()
没错,就这么简单。现在 mock_obj 就是一个什么都能“演”的替身。你可以调用它的任何方法,它不会报错:
mock_obj.任意方法名() # 不会报错
mock_obj.随便写的属性 # 也不会报错
我第一次用的时候也愣了一下:还能这样?嗯,这就是 Mock 的“宽容”之处——它默认会返回一个新的 Mock 对象,让你接着往下链。但实际测试中,我们更常用的是给它指定行为。
4.3 设置返回值与控制行为
Mock 的核心价值在于“可控”。你可以让它在特定条件下返回特定结果,或者触发特定异常。
设置返回值,两种常用方式:
# 方式一:构造时指定
mock_func = Mock(return_value=42)
# 方式二:创建后赋值
mock_func.return_value = 42
# 调用
result = mock_func()
print(result) # 输出 42
我在项目中犯过一个低级错误:忘了设置 return_value,结果测试里拿到的是个 Mock 对象而不是我预期的数字,一顿排查才发现。后来养成了习惯:创建 Mock 后立刻给它赋值行为。
side_effect 是个更强大的武器。
side_effect 可以是一个函数、一个异常、或者一个可迭代对象。用在需要动态返回值或模拟异常的场景。
# 模拟抛出异常
mock_func = Mock(side_effect=ValueError("数据非法"))
mock_func() # 抛出 ValueError
# 模拟每次调用返回不同值
mock_func = Mock(side_effect=[10, 20, 30])
print(mock_func()) # 10
print(mock_func()) # 20
print(mock_func()) # 30
# 第四次调用会抛出 StopIteration
我曾经用 side_effect 模拟网络超时重试逻辑,单次测试就能覆盖超时、成功、再次超时三种情况,一步到位。
4.4 常见属性:用数据说话
Mock 对象在每次调用后都会记录下自己的“行为日志”。这些日志就是常见属性,用来做断言。
| 属性名 | 含义 | 示例 |
|---|---|---|
called |
是否被调用过(至少一次) | assert mock.called |
call_count |
被调用的次数 | self.assertEqual(mock.call_count, 3) |
call_args |
最近一次调用的参数 | mock('a', k=1); mock.call_args == call('a', k=1) |
call_args_list |
所有调用参数的列表 | mock.call_args_list == [call(1), call(2)] |
method_calls |
该 Mock 对象上所有方法调用的记录 | 适合于检查链式调用 |
reset_mock() |
清空所有调用记录 | 在多个测试用例间复用同一 Mock 对象 |
这些属性是写断言的核心。你想想看,测试不仅仅是验证返回值,更要验证“调用过程”是否正确。比如:是否传了正确的参数?调用了几次?顺序对不对?用这些属性一查便知。
我的习惯:在每个 mock 依赖的测试方法后,我都会加一行 mock.reset_mock() 或重新创建 Mock,防止不同用例之间相互污染。毕竟测试的隔离性很重要。
4.5 实际案例:模拟数据库查询
假设你有一个函数 get_user_name(user_id),内部调用了 db.query("select name from users where id=?", user_id)。你想测试函数在数据库返回不同情况下的行为,但又不想真的连数据库。
from unittest.mock import Mock
# 第一步:创建一个 Mock 替代 db
db_mock = Mock()
# 第二步:设置 query 方法的行为
# 情况1:正常返回用户名
db_mock.query.return_value = [("Alice",)]
name = get_user_name(db_mock, 1)
assert name == "Alice"
# 情况2:查询不到用户
db_mock.query.return_value = []
name = get_user_name(db_mock, 999)
assert name is None
# 情况3:验证调用参数
db_mock.query.assert_called_once_with("select name from users where id=?", 1)
你看到了吗?只用了 4 行就完成了原本需要准备数据库、插入数据、清理数据的一系列操作。这就是 Mock 的威力。
注意:assert_called_once_with 和 call_args 等断言,默认只比较位置参数和关键字参数。如果你传的是 *args 或 **kwargs,记得检查一下参数展开后的实际值——我踩过这个坑,看起来参数一样,实际展开后顺序不同,断言一直不通过。
4.6 一张图理清Mock对象的核心
下面这张图帮你把上面讲的知识点串起来。核心就是:创建 → 配置行为 → 执行测试 → 断言验证。
你看,整个流程形成了一个循环:创建 → 配置 → 调用 → 断言 → 复位 → 复用。这和我们平时写单元测试的节奏一模一样。
4.7 避坑指南
- 不要忘记配置 return_value:否则 Mock 返回的是另一个 Mock,而不是你期望的数据。我曾经在测试中忘了配,结果断言死活无法通过。
- side_effect 和 return_value 同时设置:
side_effect优先级更高。如果既想返回固定值,又想动态控制,可以用side_effect的函数形式。 - 调用记录只记录在具体对象上:如果你创建了
mock_a和mock_b,它们彼此不干扰。但记得不要误传了同一个 Mock 给多个被测函数,不然调用记录会乱。 - 不要过度 Mock:过于精细的 Mock 会让测试和实现强耦合,一改代码就全挂。我建议只 Mock 外部边界(网络、数据库、文件系统),内部逻辑该实测就实测。
好了,现在你应该已经掌握了创建 Mock 对象的基本操作和常用属性。接下来的章节,我们会在实际项目中反复用到这些技能。