You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法每次被触发时立刻捕获/校验当时的参数状态,此时可变对象尚未被后续逻辑修改,可拿到真实的调用入参:
    @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}]
    
    若字典体量较大,无需存储全量副本,可直接在side_effect函数内对当前入参做断言,不符合预期直接抛出断言错误即可,无额外内存开销。
  • 方案2:封装通用mock工具自动拷贝入参
    针对高频出现的可变入参测试场景,可封装通用的mock初始化工具,替换mock默认的调用记录逻辑,在每次调用被记录时自动对可变类型入参做拷贝,一次封装后所有用例可直接复用,无需重复编写side_effect。
  • 不推荐方案:修改业务代码在传参前拷贝字典。该方案会侵入业务逻辑,为测试目的增加不必要的运行时开销,尤其大字典场景下性能损耗明显,无实施必要。

内容的提问来源于stack exchange,提问作者Wolf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:09:49