第4章 Python Mock库:unittest.mock模块介绍与安装

做状态栏逻辑的回放验证,绕不开一个东西——Mock。说白了,Mock就是替身演员。让你的被测代码在测试环境里“演”得像真的一样,但它背后其实什么都没做。我最早接触这个库是在一个通信协议栈的测试项目里,当时要模拟几百种异常帧,要是没用Mock,光搭环境就能把项目周期拖垮。

4.1 为什么我们需要Mock

想象一下这个场景:你正在测试状态栏的SDK,里面有个fetch_remote_config()方法,正常跑会去请求后端API。但问题来了——你写单元测试时,后端还没开发完,或者网络不稳定、或者测试环境数据库不能随便改。没有Mock,你的测试要么跑不过,要么变得非常慢。

核心痛点:测试要快、要隔离、要可控。Mock帮你把外部依赖“替换”成可控的替身。

unittest.mock是Python标准库的一部分,从Python 3.3开始就内置了。你不需要额外pip install。我遇到过有些同事还去装第三方Mock库,其实没必要——标准库已经足够强大,而且维护零成本。

4.2 unittest.mock的核心组件

这个库最核心的几个东西,我用一张图来展示它们之间的关系。

unittest.mock核心组件结构图 unittest.mock 核心组件关系图 unittest.mock 模块 Mock 类 MagicMock 类 patch / patch.object MagicMock 继承自 Mock,自动支持魔法方法 patch 装饰器/上下文管理器 临时替换目标为 Mock 对象 Mock 常用方法/属性 assert_called_with / return_value side_effect / call_count spec / wraps / name assert_has_calls / reset_mock 注:所有深色节点内文字均为白色,连接线使用 #7f8c8d

看到这张图,大概就明白了: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 常见避坑指南

我曾经历过的一个bug:在patch的上下文管理器里忘记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的基础已经搭好了。接下来几章,我们会用这些工具真正搭建状态栏回放验证的测试框架。