Mock对象的方法与属性配置:别让测试卡死在假对象上
各位好,这一节我们继续啃Mock框架的核心部分。上一章我们聊了Mock的基本概念和创建方式,说白了就是造个替身演员。但替身演员不能只会傻站着——你得告诉他「当别人调用这个API时,该返回什么」,对吧?
今天重点就是两样东西:return_value 和 side_effect。这两个是Mock对象最常用的属性配置方法,也是咱在CI自动化测试里最常打交道的。我自己的经验是,把这两个搞透了,Mock基本就玩转了七八成。
return_value:给Mock设定固定返回值
举一个最简单的例子。假设我有一个网络请求函数 fetch_user_data(user_id),测试的时候我不想真的发HTTP请求,就用Mock代替它。这时候return_value就派上用场了:
from unittest.mock import Mock
# 创建一个Mock对象,设定返回值
mock_fetch = Mock(return_value={"id": 42, "name": "张三"})
# 调用Mock对象,看看返回什么
result = mock_fetch("123")
print(result) # 输出: {"id": 42, "name": "张三"}
看到了吧?你调用 mock_fetch("123"),不管传什么参数,它都返回同一个字典。这很合逻辑——因为我们想让测试环境完全可控,不被真实网络干扰。
小技巧: 我个人习惯在setup方法里统一配置Mock的return_value,方便测试用例复用。比如 self.mock_redis.get.return_value = "cached_value",这样所有用例都能用这个预设值。
但问题来了:现实场景里,同一个Mock在不同调用下需要返回不同的东西。举个栗子——模拟一个数据库游标,第一次调用 fetchone() 要返回第一行数据,第二次返回第二行,第三次返回None。光靠return_value就搞不定了。
side_effect:让Mock有「副作用」
side_effect可以替代return_value,并且功能更强大。它可以是一个可调用对象(比如函数)、一个可迭代对象(比如列表),甚至是一个异常类。我建议你把它理解为「Mock的行为脚本」。
场景一:用可迭代对象实现依次返回不同值
from unittest.mock import Mock
mock_db = Mock()
mock_db.fetchone.side_effect = [{"name": "Alice"}, {"name": "Bob"}, None]
print(mock_db.fetchone()) # {"name": "Alice"}
print(mock_db.fetchone()) # {"name": "Bob"}
print(mock_db.fetchone()) # None
print(mock_db.fetchone()) # 抛出 StopIteration 异常
场景二:用函数实现动态返回值
def calculate_price(item_type):
if item_type == "VIP":
return 100
elif item_type == "普通":
return 50
else:
raise ValueError("未知类型")
mock_price = Mock()
mock_price.side_effect = calculate_price
print(mock_price("VIP")) # 100
print(mock_price("普通")) # 50
print(mock_price("未知")) # 抛出 ValueError
场景三:模拟异常抛出
mock_api = Mock()
mock_api.post.side_effect = ConnectionError("网络连接失败")
try:
mock_api.post("/data", json={"key": "value"})
except ConnectionError as e:
print(f"捕获异常: {e}")
踩坑提醒: 我曾经在项目里忘记side_effect和return_value不能同时用。如果你同时设置了这两个,side_effect优先级更高,return_value会被忽略。我的教训是——写代码前先想清楚:是「每次返回固定值」还是「每次行为不同」,别混着用。
方法和属性配置的完整列表
除了return_value和side_effect,Mock对象还有几个常用配置项。我整理了个表格,方便你查阅:
| 配置项 | 类型 | 作用 | 典型场景 |
|---|---|---|---|
| return_value | 任意 | 固定返回值 | 模拟接口返回固定JSON |
| side_effect | 可调用对象 / 可迭代对象 / 异常类 | 定义动态行为 | 模拟游标迭代、动态计算 |
| spec | 类或对象 | 限制Mock只能有指定属性/方法 | 类型安全检查 |
| autospec | 布尔值 / 可用对象 | 自动根据真实对象生成spec | 保证Mock签名与被模拟对象一致 |
| name | 字符串 | 给Mock命名,便于调试 | 多Mock场景下定位问题 |
| wraps | 真实对象 | 包装真实对象,未mock的方法走真实逻辑 | 部分模拟(partial mock) |
这里重点说下 spec 和 autospec。在CI自动化测试里,团队成员水平参差不齐,经常有人写Mock时漏掉某个参数,导致测试通过但集成时挂掉。用 spec 可以帮我们提前发现这类问题:
class UserService:
def get_user(self, user_id, include_deleted=False):
pass
# 不带spec:调用任意属性都不会报错
mock_bad = Mock()
mock_bad.get_user(42) # 正常,但丢失了include_deleted参数
# 带spec:只能调用UserService中定义的方法和属性
mock_good = Mock(spec=UserService)
mock_good.get_user(42) # OK
mock_good.get_user(42, include_deleted=True) # OK
mock_good.fake_method() # 抛出 AttributeError
个人经验: 我在团队推行Mock规范时,强制要求所有Mock必须带 spec=True 或 autospec=True。虽然刚开始大家觉得麻烦,但后来发现跑CI时因为接口变更导致的测试遗漏大大减少。所以,这点投入值得。
属性模拟:不只是方法调用
Mock不仅能模拟方法,还能模拟属性。比如一个配置类 Config.,你可以在测试里这样搞:
mock_config = Mock()
mock_config. = "test_key_123"
mock_config.TIMEOUT = 30
print(mock_config.) # test_key_123
print(mock_config.TIMEOUT) # 30
当你需要模拟属性链的时候(比如 obj.a.b.c),Mock会自动创建嵌套的Mock对象:
mock_obj = Mock()
mock_obj.a.b.c = "deep_value"
print(mock_obj.a.b.c) # deep_value
# 注意:mock_obj.a 和 mock_obj.a.b 都是Mock对象
为什么这样设计?因为Mock框架假设你在测试时不会关心中间对象的细节,只关心最终值。但这也带来一个问题——如果你不小心写错了属性名,它不会报错,而是直接创建一个新的Mock。这就是我之前说的「silent failure」,调试起来特别头疼。
避坑指南: 我曾经debug了一个小时,最后发现是 mock_redis.get.return_value 写成了 mock_redis.gett.return_value —— 多打了一个t,Mock默默创建了 gett 这个新属性。所以我的建议是:用 spec,或者至少用 assert_has_calls 验证调用路径。
实战案例:用一个模拟HTTP客户端串联核心知识点
我们把前面讲的东西串起来。假设我们有一个函数 process_orders(api_client, order_ids),它会对每个订单ID调用 api_client.get_order(id),然后做处理。我们需要写测试,但不想真发HTTP请求。
from unittest.mock import Mock
def process_orders(api_client, order_ids):
results = []
for oid in order_ids:
order = api_client.get_order(oid)
if order["status"] == "paid":
results.append(order)
return results
# 测试代码
def test_process_orders():
# 创建Mock客户端
mock_api = Mock()
# 用side_effect模拟依次返回不同订单
def mock_get_order(order_id):
orders = {
1: {"id": 1, "status": "paid"},
2: {"id": 2, "status": "pending"},
3: {"id": 3, "status": "paid"}
}
if order_id in orders:
return orders[order_id]
return None
mock_api.get_order.side_effect = mock_get_order
# 调用被测试函数
result = process_orders(mock_api, [1, 2, 3])
# 验证结果
assert len(result) == 2
assert result[0]["id"] == 1
assert result[1]["id"] == 3
# 验证调用次数和参数
assert mock_api.get_order.call_count == 3
mock_api.get_order.assert_any_call(1)
mock_api.get_order.assert_any_call(2)
mock_api.get_order.assert_any_call(3)
这个例子覆盖了:side_effect 用函数动态生成返回值、call_count 和 assert_any_call 做验证。整个测试不需要网络,不需要数据库,纯内存操作,跑得飞快——这就是Mock在CI里的核心价值。
本章核心技术图谱
我画了一张图,把Mock方法配置和属性配置的核心路径梳理了一下。你一眼就能看清:先选返回方式,再决定是否要动态行为,最后别忘了验证。
总结一下关键点
- return_value 适合「每次调用都一样」的场景,简单直接;side_effect 适合「每次行为不同」的场景,灵活多变。
- 两者冲突时,side_effect 优先。别同时设,否则return_value等于白写。
- 用
spec或autospec防止手滑写错方法名,这是CI里保障测试质量的手段。 - 属性模拟默认是「自动创建嵌套Mock」,小心静默失败。建议用
assert_has_calls做最终核对。
这一节的内容偏实操,我希望你动手敲一下代码。自己创建一个Mock,先设return_value看看反应,再换成side_effect体验下动态效果,最后再故意写错个属性名——嗯,亲自踩一次坑比看十遍文档都管用。