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对象的核心

下面这张图帮你把上面讲的知识点串起来。核心就是:创建 → 配置行为 → 执行测试 → 断言验证。

Mock 对象核心流程 创建 Mock 对象 配置行为 return_value / side_effect 调用 & 获取结果 断言验证 called / call_count / call_args 复位/复用 reset_mock() 核心思想:用 Mock 替代真实依赖,控制行为,验证交互。 属性如 call_args、call_count 就是你的“测试证据”。

你看,整个流程形成了一个循环:创建 → 配置 → 调用 → 断言 → 复位 → 复用。这和我们平时写单元测试的节奏一模一样。

4.7 避坑指南

  • 不要忘记配置 return_value:否则 Mock 返回的是另一个 Mock,而不是你期望的数据。我曾经在测试中忘了配,结果断言死活无法通过。
  • side_effect 和 return_value 同时设置:side_effect 优先级更高。如果既想返回固定值,又想动态控制,可以用 side_effect 的函数形式。
  • 调用记录只记录在具体对象上:如果你创建了 mock_a 和 mock_b,它们彼此不干扰。但记得不要误传了同一个 Mock 给多个被测函数,不然调用记录会乱。
  • 不要过度 Mock:过于精细的 Mock 会让测试和实现强耦合,一改代码就全挂。我建议只 Mock 外部边界(网络、数据库、文件系统),内部逻辑该实测就实测。

好了,现在你应该已经掌握了创建 Mock 对象的基本操作和常用属性。接下来的章节,我们会在实际项目中反复用到这些技能。