1. 调用统计基础
1.1 统计调用次数与基础断言
调用次数是评估被测模块是否被正确触发的核心指标,通常与断言结合使用以确保逻辑正确性。
在使用 Mock 时,最常见的做法是通过 call_count、called 与各类断言方法来验证调用情况。
from unittest.mock import Mock
# 创建一个 Mock 对象
m = Mock()
# 触发一些调用
m('alpha')
m(1, 2)
# 统计调用次数
print(m.call_count) # 2
# 基本断言
assert m.called # 至少被调用过一次
assert m.call_count == 2
核心要点包括:call_count、called、以及利用断言方法如 assert_called、assert_called_once、assert_called_with 等快速验证调用。
1.2 访问调用历史与参数
除了计数,查看具体的调用历史(包含参数)同样重要。call_args、call_args_list 以及 mock_calls 能帮助你回溯最近一次调用、全部调用序列以及逐次调用的参数。
通过这些属性,可以在测试中定位是否存在参数传递错误,以及调用的顺序是否符合预期。
from unittest.mock import Mock
m = Mock()
m('A')
m(1, 2)
m(x=3)
print(m.call_args) # ('x=3' 的上一轮调用的最后一个参数)
print(m.call_args_list) # [call('A'), call(1, 2), call(x=3)]
print(m.mock_calls) # [call('A'), call(1, 2), call(x=3)]
要点提示:call_args 给出最后一次调用的参数,call_args_list 提供所有调用的参数清单,mock_calls 展示与调用相关的全部记录(包括属性访问、方法调用等)。
2. 参数分布的收集与分析
2.1 通过 side_effect 收集参数分布
当需要了解“被调用时传入的参数分布”时,可以利用 side_effect 捕获每一次调用的参数并追加到一个统计容器中,从而得到参数分布信息。
使用 Counter 或自定义统计结构,可以将参数作为键,调用次数作为值,形成分布直方图。
from unittest.mock import Mock
from collections import Counter
records = []
def capture(*args, **kwargs):
records.append((args, tuple(sorted(kwargs.items()))))
return 'ok'
m = Mock(side_effect=capture)
# 触发多次调用
m(1, 2)
m(1, a=3)
m(2, 2)
# 统计分布
dist = Counter(records)
print(dist)
实践要点:通过把参数元组和排序后的关键字参数作为键,Counter 能快速给出分布数量,便于后续分析。
2.2 结合 mock_calls 与 call_args_list 统计参数分布
另一种常见做法是直接遍历 mock_calls 或 call_args_list,将每次调用的 args 与 kwargs 作为组合键,统计出现频次。
这种方式无需额外写 side_effect,更直观地看到每次调用的具体参数。
from collections import Counter
from unittest.mock import Mock, call
m = Mock()
m(1, 2)
m(1, 3, z=9)
m(2, 2)
# 统计每组参数的分布
dist = Counter()
for c in m.mock_calls:
if hasattr(c, 'args'):
dist[(c.args, tuple(sorted(c.kwargs.items()))) ] += 1
print(dist)
要点聚焦:mock_calls 包含了调用的完整序列,args 与 kwargs 的组合是构建分布的关键基础。
3. 返回值的操控与验证
3.1 设置 return_value 与 side_effect 的技巧
返回值是测试被调用函数对外暴露的关键输出之一。通过 return_value 可以让 Mock 始终返回同一个结果;通过 side_effect 则可以按调用序列返回不同值,甚至抛出异常。
在实战中,return_value 适合简单场景,而 side_effect 更灵活,既能模拟复杂返回逻辑,也能快速抛出异常来测试错误路径。
from unittest.mock import Mock
# 始终返回固定值
m1 = Mock(return_value=42)
assert m1() == 42
assert m1() == 42
# 根据调用顺序返回不同值
m2 = Mock(side_effect=[1, 2, 3])
print(m2()) # 1
print(m2()) # 2
print(m2()) # 3
# 也可抛出异常
def raise_error():
raise ValueError("boom")
m3 = Mock(side_effect=raise_error)
try:
m3()
except ValueError as e:
print("caught:", e)
实战要点:return_value 适用于确定性返回,side_effect 适用于序列化返回、条件分支和异常模拟。
3.2 在测试用例中验证返回值的影响
使用 Mock 的返回值来驱动被测函数的后续分支,是单元测试中的常见模式。你可以通过断言来确认返回值对下游逻辑的影响,以及被测对象如何正确地消费返回值。
示例中,assert_called_with、assert_called_once_with 可用于确保被测对象在调用 Mock 时传入了正确的返回值相关参数。
from unittest.mock import Mock
def compose(fetcher):
data = fetcher()
if data > 10:
return "high"
return "low"
mock_fetch = Mock(return_value=15)
result = compose(mock_fetch)
print(result) # high
mock_fetch.assert_called_once()
小结要点:通过控制返回值,可以稳定测试分支逻辑;再通过断言来核验被测模块对返回值的消费是否符合预期。
4. 实战技巧与注意事项
4.1 使用 wraps 保留原始行为并统计调用
有时你希望既统计调用信息,又保留原始函数的真实行为。这时可以使用 wraps 将一个真实对象包装到 Mock 中,同时获取统计信息。
通过 wraps,调用 Mock 时会实际执行被包装的对象,返回值和副作用与真实对象一致,统计信息也会完整记录。
from unittest.mock import Mock
def real_function(a, b):
return a + b
wrapped = Mock(wraps=real_function)
print(wrapped(2, 3)) # 5
print(wrapped.call_count) # 1
wrapped.assert_called_once_with(2, 3)
要点:wraps 提供了无侵入的「观测 + 保真」能力,适用于需要完整调用信息和真实行为的场景。
4.2 组合使用子模块/类的 Mock 进行统计
在结构较复杂的系统中,常需要对某个类或模块的某个方法进行统计。通过对目标属性进行 patch 或直接创建 Mock 对象,可以实现全局或局部的统计收集。
下面的示例展示了对一个方法的拦截统计,以及如何确保原始程序逻辑保持可测试性。
from unittest.mock import patch
class Service:
def process(self, value):
return value * 2
with patch('__main__.Service.process', wraps=Service.process) as mocked:
s = Service()
print(s.process(5)) # 10
mocked.assert_called_once_with(5)
实用要点:对模块或类方法进行 patch,能在不中改动代码实现的前提下,对调用次数、参数分布和返回值进行系统性统计与断言。
本文从调用次数、参数分布与返回值三大维度系统拆解 Python 中 Mock 的统计方法,提供了可落地的实战技巧与代码示例,帮助你在单元测试中实现高可观测性与可验证性。通过灵活运用 call_count、call_args、mock_calls、side_effect、return_value 与 wraps 等特性,可以在不同场景下快速定位问题、优化测试用例并提升测试稳定性。


