← 文章

MagicMock 接受任意实参:调用方多传一个参数,测试照样通过,生产抛 TypeError 被 except 吞掉

约 3 分钟#Python#测试

不带 spec 的 MagicMock 不检查调用签名,被测代码传错参数时,替身照样返回你设好的值。Autarkia 里一段 prompt 上下文因此从来没被注入过:真实方法只收 self,调用方多传了 user_id,测试里的替身收下了,生产里的 TypeError 被外层 except 吞掉。

目录

MagicMock() 上任何属性都存在,任何属性都能被调用,调用时传什么参数都行。你设了 return_value,它就返回那个值。所以只要被测代码调用的是替身,「参数传错了」这类错误在测试里就不存在。create_autospec 或 patch(..., autospec=True) 会照着真实对象的签名生成替身,参数不对直接抛 TypeError:

python
from unittest.mock import MagicMock, create_autospec

class DailyManager:
    def get_today_summary(self):
        ...

m = MagicMock()
m.get_today_summary.return_value = "摘要"
m.get_today_summary("user-1")          # 返回 "摘要"

a = create_autospec(DailyManager, instance=True)
a.get_today_summary.return_value = "摘要"
a.get_today_summary("user-1")          # TypeError: too many positional arguments

01一段从没进过 prompt 的上下文

Autarkia 拼 prompt 时有一段「今日摘要」,调用方是这样写的:

python
try:
    from core.services.daily.manager import get_daily_manager

    daily_sum = get_daily_manager().get_today_summary(user_id)
    if daily_sum:
        txt += f"\n【今日摘要】\n{daily_sum}\n"
        ...
except Exception:
    pass

get_today_summary 的签名是 (self),不收 user_id。每次调用都抛 TypeError,每次都被 except Exception: pass 吞掉,这一段从来没出现在 prompt 里。模型的表现只是「好像不知道这件事」,没有任何报错。这行在这个目录第一次进 git 时就已经是这样了。

测试是这样写的:

python
daily_manager = MagicMock()
daily_manager.get_today_summary.return_value = "【今日画像】\n- ..."

with patch(
    "core.services.daily.manager.get_daily_manager",
    return_value=daily_manager,
):
    text = build_user_bio_context_for_chat("test-user")

assert "【今日摘要】" in text

替身收下了多余的 user_id,返回了设好的摘要,断言通过。这个测试保证的是「如果拿到了摘要,会把它拼对」,而前提「能拿到摘要」恰好是生产里不成立的那一步。

02它是怎么被发现的

后来给同一个验证脚本补了一个用真实数据跑的用例,真实输出里这一段不见了。即便那时,脚本也只打了一行 [WARN],因为同样的信息另有一条无参调用的路径在注入,线上没有完全丢。这个脚本还是手动跑的验证脚本,不在 CI 的 pytest 收集范围里,[WARN] 打出来也只有跑的人看得到。

修法是去掉多余的实参,把 [WARN] 改成硬断言,再给这类整块上下文的 except 补上 logger.warning(..., exc_info=True)。两个月前日记工具出过一模一样的问题:调用 get_entries(dt, scope=scope),而签名是 (self, date),那次是用户看到了「读取日记失败」才发现的。

03现在仓库里

我数了一下,测试目录里有 279 处裸的 MagicMock(),带 spec 或 autospec 的只有 3 处。不是每一处都有风险:替身要是代表外部服务、而被测代码对它的调用方式本来就由协议约束,问题不大。危险的是替身代表项目里自己的类,而调用方和被调方各自在改。

我给主项目的修法是:项目内部的类一律用 create_autospec(真实类, instance=True);新增的不带 spec 的 MagicMock() 让检查直接失败,存量先列白名单。被 except Exception 包住的整块逻辑,测试里至少要有一条用例走到真实对象,因为这种组合下签名错误在生产里不会有任何可见的症状。