4. Python unittest.mock 入门:MagicMock 与 Mock 对象
在单元测试中,被测函数或方法往往依赖外部服务、数据库、网络请求或复杂对象。unittest.mock 模块提供了一套强大的工具,用于创建“替身对象”来隔离这些依赖,让测试聚焦于被测代码自身的逻辑。本章将系统讲解 Mock 与 MagicMock 的核心概念、基本用法以及两个关键参数:return_value 与 side_effect。
4.1 Mock 与 MagicMock 的核心区别
Mock 是 mock 模块的基础类,它可以模拟任意对象,并记录对它的所有调用。MagicMock 是 Mock 的子类,它预定义了大部分 Python 魔术方法(如 __len__、__iter__、__getitem__ 等),使得模拟对象在需要这些特殊行为时不会抛出异常。
| 特性 | Mock | MagicMock |
|---|---|---|
| 基础功能 | 属性访问、方法调用、调用记录 | 继承 Mock 全部功能 |
| 魔术方法支持 | 需要手动配置 | 默认支持大部分魔术方法 |
| 适用场景 | 普通对象、函数、类方法 | 需要迭代、上下文管理、容器行为的对象 |
| 使用频率 | 高(通用场景) | 高(简化魔术方法模拟) |
推荐原则:当你不确定是否需要魔术方法时,优先使用 MagicMock。它不会带来额外负担,但能避免因缺少
__len__ 或 __iter__ 而导致的意外失败。
4.2 基本用法:创建与配置 Mock
创建 Mock 对象非常简单,直接实例化即可。之后可以通过赋值属性或调用方法来模拟真实对象的行为。
from unittest.mock import Mock, MagicMock
# 创建基本 Mock
mock_obj = Mock()
# 模拟属性
mock_obj.name = "test_service"
mock_obj.version = "2.1.0"
# 模拟方法返回值
mock_obj.get_data.return_value = {"id": 1, "status": "ok"}
# 调用模拟方法
result = mock_obj.get_data()
print(result) # 输出: {'id': 1, 'status': 'ok'}
# 验证调用
mock_obj.get_data.assert_called_once()
MagicMock 的用法与 Mock 完全一致,区别在于它已经内置了魔术方法的行为:
from unittest.mock import MagicMock
magic = MagicMock()
# 无需手动配置 __len__
magic.__len__.return_value = 5
print(len(magic)) # 输出: 5
# 无需手动配置 __iter__
magic.__iter__.return_value = iter([10, 20, 30])
for item in magic:
print(item) # 输出: 10, 20, 30
4.3 return_value:固定返回值
return_value 用于指定 Mock 对象被调用时返回的固定值。它适用于模拟那些返回确定结果的依赖,例如配置查询、状态检查等。
from unittest.mock import Mock
# 模拟一个数据库查询函数
db_query = Mock()
db_query.return_value = [{"user": "alice"}, {"user": "bob"}]
# 调用模拟函数
users = db_query("SELECT * FROM users")
print(users) # 输出: [{'user': 'alice'}, {'user': 'bob'}]
# 多次调用返回相同值
users_again = db_query("SELECT * FROM users WHERE id=1")
print(users_again) # 输出: [{'user': 'alice'}, {'user': 'bob'}]
关键点:
return_value 一旦设置,无论调用参数如何,Mock 对象始终返回该值。如果需要根据输入参数返回不同结果,应使用 side_effect。
4.4 side_effect:动态行为与异常模拟
side_effect 比 return_value 更灵活,它可以接受三种类型:
- 可调用对象(函数):根据输入参数动态计算返回值。
- 可迭代对象:每次调用依次返回迭代中的下一个值。
- 异常类或异常实例:调用时抛出指定异常。
| side_effect 类型 | 行为描述 | 典型场景 |
|---|---|---|
| 函数 | 调用时执行该函数,返回值作为 Mock 的返回值 | 根据参数返回不同结果 |
| 列表/元组/生成器 | 每次调用取下一个元素作为返回值 | 模拟多次调用返回不同状态 |
| 异常 | 调用时抛出异常 | 模拟网络超时、数据库连接失败 |
from unittest.mock import Mock
# 1. 使用函数动态计算返回值
def calculate_discount(price):
return price * 0.9 if price > 100 else price
mock_discount = Mock()
mock_discount.side_effect = calculate_discount
print(mock_discount(200)) # 输出: 180.0
print(mock_discount(50)) # 输出: 50
# 2. 使用可迭代对象模拟多次调用
mock_status = Mock()
mock_status.side_effect = ["pending", "running", "completed"]
print(mock_status()) # 输出: pending
print(mock_status()) # 输出: running
print(mock_status()) # 输出: completed
# 3. 模拟异常
mock_network = Mock()
mock_network.side_effect = ConnectionError("Network timeout")
try:
mock_network()
except ConnectionError as e:
print(e) # 输出: Network timeout
注意:当
side_effect 设置为可迭代对象时,如果调用次数超过迭代器长度,会抛出 StopIteration 异常。建议在测试中控制调用次数或使用无限迭代器(如 itertools.cycle)。
4.5 return_value 与 side_effect 的优先级
当同时设置 return_value 和 side_effect 时,side_effect 具有更高优先级。如果 side_effect 是 None 或可迭代对象已耗尽,则回退到 return_value。
from unittest.mock import Mock
mock = Mock()
mock.return_value = "default"
mock.side_effect = ["first", "second"]
print(mock()) # 输出: first(side_effect 优先)
print(mock()) # 输出: second
print(mock()) # 输出: default(side_effect 耗尽,回退 return_value)
4.6 核心概念关系图
4.7 实战案例:模拟外部 API 调用
以下示例展示如何在一个典型的用户服务测试中,使用 MagicMock 隔离外部 HTTP 请求依赖。
import requests
from unittest.mock import MagicMock, patch
class UserService:
def __init__(self, base_url):
self.base_url = base_url
def get_user(self, user_id):
response = requests.get(f"{self.base_url}/users/{user_id}")
if response.status_code == 200:
return response.json()
return None
# 测试代码
def test_get_user_success():
# 创建 Mock 响应对象
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "name": "Alice"}
# 使用 patch 替换 requests.get
with patch("requests.get", return_value=mock_response) as mock_get:
service = UserService("https://api.example.com")
user = service.get_user(1)
assert user == {"id": 1, "name": "Alice"}
mock_get.assert_called_once_with("https://api.example.com/users/1")
def test_get_user_not_found():
mock_response = MagicMock()
mock_response.status_code = 404
with patch("requests.get", return_value=mock_response) as mock_get:
service = UserService("https://api.example.com")
user = service.get_user(999)
assert user is None
mock_get.assert_called_once_with("https://api.example.com/users/999")
测试要点:
- 使用
MagicMock模拟响应对象,同时设置status_code属性和json方法的返回值。 - 使用
patch上下文管理器替换requests.get,确保测试不发起真实网络请求。 - 通过
assert_called_once_with验证调用参数,确保被测代码按预期传递了 URL。
4.8 常见陷阱与最佳实践
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 忘记设置 return_value | 调用 Mock 对象返回新的 Mock 对象,而非预期值 | 显式设置 return_value 或 side_effect |
| side_effect 与 return_value 混淆 | 同时设置时 side_effect 优先,可能导致意外行为 | 明确区分场景,避免同时设置 |
| 未验证调用次数 | 依赖可能被多次调用或未被调用 | 使用 assert_called_once / assert_called_with |
| 过度使用 Mock | Mock 过多导致测试与实现耦合 | 只 Mock 外部依赖,内部逻辑尽量使用真实对象 |
最佳实践:始终从测试意图出发选择 Mock 策略。如果依赖返回固定值,用
return_value;如果需要根据输入动态响应或模拟异常,用 side_effect。保持测试简洁、可读、可维护。