第4章 Python Mock库:unittest.mock模块介绍与安装
做状态栏逻辑的回放验证,绕不开一个东西——Mock。说白了,Mock就是替身演员。让你的被测代码在测试环境里“演”得像真的一样,但它背后其实什么都没做。我最早接触这个库是在一个通信协议栈的测试项目里,当时要模拟几百种异常帧,要是没用Mock,光搭环境就能把项目周期拖垮。
4.1 为什么我们需要Mock
想象一下这个场景:你正在测试状态栏的SDK,里面有个fetch_remote_config()方法,正常跑会去请求后端API。但问题来了——你写单元测试时,后端还没开发完,或者网络不稳定、或者测试环境数据库不能随便改。没有Mock,你的测试要么跑不过,要么变得非常慢。
unittest.mock是Python标准库的一部分,从Python 3.3开始就内置了。你不需要额外pip install。我遇到过有些同事还去装第三方Mock库,其实没必要——标准库已经足够强大,而且维护零成本。
4.2 unittest.mock的核心组件
这个库最核心的几个东西,我用一张图来展示它们之间的关系。
看到这张图,大概就明白了:Mock类是根基;MagicMock是它的增强版,自动帮你处理了__len__、__iter__这类魔法方法;而patch则是“注射器”,能把Mock对象注入到指定位置。
4.3 安装与引入
安装?不用装。Python 3.3以上版本自带了。你在代码里这样引用就行:
from unittest.mock import Mock, MagicMock, patch, call
如果你还在用Python 2.7(嗯…我建议尽快迁移),需要单独装pip install mock。但咱们这门课全部以Python 3.8+为准,所以直接import就完事。
from unittest.mock import Mock as Mock,其实没必要显式重命名,当然如果你觉得unittest.mock路径太长,可以import unittest.mock as mock。
4.4 Mock对象的基本创建
创建一个Mock对象就跟实例化一个空壳一样简单:
from unittest.mock import Mock
m = Mock()
print(m) # 输出: <Mock id='140244234567856'>
你可以调用任何方法,它都不会报错:
m.any_method()
m.any_other_method(1, 2, key='value')
print(m.any_method.called) # True
print(m.any_method.call_count) # 1
我记得第一次用Mock时,试了一下m.foo.bar.baz(),居然也能跑通。当时觉得这玩意儿太“流氓”了——任何属性访问都会给你返回一个新的Mock对象。这种自动链式访问特别适合深度嵌套的场景,但也容易让人搞不清到底调用到了哪里。所以,后期建议用spec约束行为。
4.5 配置返回值(return_value)
Mock要有用,就得能控制它返回什么。用return_value:
m = Mock()
m.get_status.return_value = {'battery': 85, 'network': 'wifi'}
result = m.get_status()
print(result) # {'battery': 85, 'network': 'wifi'}
也可以动态切换,用side_effect:
def dynamic_status(*args, **kwargs):
import time
return {'timestamp': time.time()}
m.get_status.side_effect = dynamic_status
print(m.get_status()) # 每次返回不同的时间戳
side_effect可以被赋值为可调用对象、异常实例或可迭代对象。如果赋值为异常,调用时就会抛出异常,非常适合模拟异常场景。
4.6 patch的使用方法
patch是用得最多的功能。它可以把一个名字临时替换成Mock对象。我曾经因为搞错patch的路径,排查了一个上午。这个经验给大家避坑——patch的路径必须指向被测试代码中的引用位置,而不是定义位置。
举个例子,你有一个模块state_manager.py,里面导入了requests并调用requests.get。那么patch时应该写:
# 错误:patch('requests.get')
# 正确:patch('state_manager.requests.get')
from unittest.mock import patch
from state_manager import fetch_config
with patch('state_manager.requests.get') as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'version': 'v2'}
result = fetch_config()
assert result == {'version': 'v2'}
也可以当装饰器用:
@patch('state_manager.requests.get')
def test_fetch(mock_get):
mock_get.return_value.status_code = 200
# ... 测试逻辑
pass
4.7 断言方法速查表
Mock对象自带了一套断言方法,用于验证调用行为。这部分在状态栏回放验证中会频繁使用。我整理了一个表格:
| 断言方法 | 作用 |
|---|---|
assert_called() | 至少被调用一次 |
assert_called_once() | 恰好被调用一次 |
assert_called_with(*args, **kwargs) | 最近一次调用使用了指定的参数 |
assert_called_once_with(*args, **kwargs) | 恰好调用一次且使用指定的参数 |
assert_any_call(*args, **kwargs) | 历史上某个时刻调用过这些参数 |
assert_has_calls(calls, any_order=False) | 按顺序(或任意顺序)包含指定的调用列表 |
assert_not_called() | 从未被调用 |
assert_called_once()确保关键方法没有被重复触发,然后结合call_args属性检查传入的参数是否正确。
4.8 MagicMock与Mock的区别
简单说:MagicMock = Mock + 魔法方法支持。如果你的被测代码会用到__len__、__iter__、__getitem__等魔术方法,直接上MagicMock。否则Mock就够用。
比如状态栏的某个组件可能会遍历某个列表:
# 如果用Mock,需要手动设 __iter__
from unittest.mock import Mock
m = Mock()
m.__iter__.return_value = iter([1, 2, 3])
# 如果用MagicMock,它自动返回一个MagicMock且可以迭代
from unittest.mock import MagicMock
mm = MagicMock()
mm.__iter__.return_value = iter([4, 5, 6])
我建议:当你不太确定的时候,直接用MagicMock。它不会比Mock慢多少,但少很多麻烦。
4.9 常见避坑指南
yield(如果是装饰器写法不存在此问题)。导致测试跑完后Mock没有被卸载,影响了后续用例。解决方案:始终使用with语句或@patch装饰器,手动start()/stop()要特别注意成对调用。
还有一个容易忽视的点:Mock对象默认是callable的。如果你把Mock赋值给一个本来不是callable的属性,调用它时不会报错。有时候这会让测试“虚假通过”。解决方案:创建Mock时指定spec参数,让Mock只暴露真实对象里有的属性方法。比如:
from unittest.mock import Mock
import os
mock_os = Mock(spec=os) # 只允许调用os模块有的方法和属性
mock_os.nonexist() # 会抛AttributeError
4.10 快速上手:一个状态栏Mock示例
最后给一个咱们课程会用到的小例子:
from unittest.mock import MagicMock, patch
# 假设我们有一个 StatusBar 类
class StatusBar:
def __init__(self, network_client):
self.client = network_client
def refresh_status(self):
data = self.client.fetch_status()
return data.get('signal', 0)
# 测试
def test_refresh_status():
mock_client = MagicMock()
mock_client.fetch_status.return_value = {'signal': -65, 'bars': 3}
bar = StatusBar(mock_client)
result = bar.refresh_status()
assert result == -65
mock_client.fetch_status.assert_called_once()
这个例子里,mock_client完全替代了真正的网络客户端。我们测试refresh_status时,只关心它是否正确调用了fetch_status以及返回值处理逻辑。至于网络是通还是断,我们根本不在乎——这就是Mock的魅力。
好了,unittest.mock的基础已经搭好了。接下来几章,我们会用这些工具真正搭建状态栏回放验证的测试框架。