不带 spec 的 MagicMock 不检查调用签名,被测代码传错参数时,替身照样返回你设好的值。Autarkia 里一段 prompt 上下文因此从来没被注入过:真实方法只收 self,调用方多传了 user_id,测试里的替身收下了,生产里的 TypeError 被外层 except 吞掉。
MagicMock() 上任何属性都存在,任何属性都能被调用,调用时传什么参数都行。你设了 return_value,它就返回那个值。所以只要被测代码调用的是替身,「参数传错了」这类错误在测试里就不存在。create_autospec 或 patch(..., autospec=True) 会照着真实对象的签名生成替身,参数不对直接抛 TypeError:
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 arguments01一段从没进过 prompt 的上下文
Autarkia 拼 prompt 时有一段「今日摘要」,调用方是这样写的:
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:
passget_today_summary 的签名是 (self),不收 user_id。每次调用都抛 TypeError,每次都被 except Exception: pass 吞掉,这一段从来没出现在 prompt 里。模型的表现只是「好像不知道这件事」,没有任何报错。这行在这个目录第一次进 git 时就已经是这样了。
测试是这样写的:
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 包住的整块逻辑,测试里至少要有一条用例走到真实对象,因为这种组合下签名错误在生产里不会有任何可见的症状。