Python unittest.mock 可变参数多次调用断言异常求解
unittest.mock assert_has_calls 可变参数断言异常问题
编写单元测试过程中,针对同一方法的多次调用做断言时,unittest.mock的assert_has_calls方法会出现不符合预期的行为,该问题可通过最小示例稳定复现。
问题复现
class Foo(): def __init__(self): pass # 待测试方法 def bar(self, d): # 对字典执行业务逻辑 print(d) def baz(): f = Foo() d = {1: 2} # 第一次调用bar方法 f.bar(d) # 修改字典内容 d[3] = 4 # 字典修改后发起第二次调用 f.bar(d) @mock.patch('foo.Foo') def test_baz(foo_mock): baz() foo_mock.return_value.bar.assert_has_calls( [ mock.call({1: 2}), mock.call({1: 2, 3: 4}) ] )
上述测试用例运行失败,报错信息如下:
E AssertionError: Calls not found. E Expected: [call({1: 2}), call({1: 2, 3: 4})] E Actual: [call({1: 2, 3: 4}), call({1: 2, 3: 4})]
从报错可以看到,两次调用的参数记录均为字典修改后的最终状态,第一次调用时的{1:2}状态没有被正确捕获。
根因说明
该现象既非框架bug,也非使用方式错误,属于unittest.mock的默认设计行为:mock记录调用参数时,不会对传入的可变对象(字典、列表等)执行拷贝操作,仅保存对象的内存引用。当可变对象在两次调用间隔被修改时,mock存储的引用指向的对象内容会同步更新,最终所有历史调用记录中的参数都会展示为对象的最终状态,和调用发生时的实际入参不一致。
规避方案
无需修改业务代码在传参时额外拷贝字典,可按优先级选择以下无侵入方案:
- 方案1:通过
side_effect实时校验参数
不依赖事后的调用历史回溯,在mock方法每次被触发时立刻捕获/校验当时的参数状态,此时可变对象尚未被后续逻辑修改,可拿到真实的调用入参:
若字典体量较大,无需存储全量副本,可直接在side_effect函数内对当前入参做断言,不符合预期直接抛出断言错误即可,无额外内存开销。@mock.patch('foo.Foo') def test_baz(foo_mock): call_params = [] # 定义side_effect逻辑,每次调用时保存入参的副本 def save_param(d): call_params.append(d.copy()) foo_mock.return_value.bar.side_effect = save_param baz() # 对实时捕获的参数序列做断言 assert call_params == [{1: 2}, {1: 2, 3: 4}] - 方案2:封装通用mock工具自动拷贝入参
针对高频出现的可变入参测试场景,可封装通用的mock初始化工具,替换mock默认的调用记录逻辑,在每次调用被记录时自动对可变类型入参做拷贝,一次封装后所有用例可直接复用,无需重复编写side_effect。 - 不推荐方案:修改业务代码在传参前拷贝字典。该方案会侵入业务逻辑,为测试目的增加不必要的运行时开销,尤其大字典场景下性能损耗明显,无实施必要。
内容的提问来源于stack exchange,提问作者Wolf
相关产品推荐
相关产品推荐

