优质同款

断言反证法

Jason 发布于 码

断言反证法(让断言真的会咬人)
何时用

  • 写/改守护类断言:「文档 == 实现」「A 与 B 必须一致」「配置里不得出现 X」;
  • 写阴性断言:「某方法没有被调用」「某事不会发生」「列表为空」;
  • 审查已有测试,怀疑它「永远绿」;
  • 重构后要确认原有断言仍然咬合(重构常常悄悄让断言失效)。
    核心判据一句话:「改完之后是绿的」不算验证;「改坏了它会红」才算。
    标准流程
    1. 先写断言,再故意破坏被测行为,确认断言失败;
      • 代码路径可关掉:把分支改成 if False: ...(临时);
      • 参数可加坏:给协议串加上被禁止的那一项、把返回值的字段改名;
      • 只能拿到新版本时:git show HEAD: > /tmp/before.ext,用断言的同一套取值/切片逻辑
        对旧版本求值,期望「不在/不满足」。
    2. 读失败输出本身,确认报错指向你预期的差异(红得对地方,而不是别的错误);
    3. 还原临时改动;
    4. 用 git diff --stat / git diff <该文件> 确认临时改动已彻底还原(尤其是 print 探针);
    5. 再跑一次完整套件,确认绿。
      临时破坏不落库:不要把它写进 commit。若破坏方式本身值得留档,写在测试 docstring 里
      (例:「本用例的反证已跑过:把 X 改成 if False,本用例确实失败」)。
      恒真断言的三种成因(症状相同:永远绿)
      ① 切片 / 字段名写错
      从大文本里取一段来断言时,切片锚点写错 → 断言恒真。
  • 对策:切片取窄(明确起止章节),且章节找不到时必须显式失败,不要静默跳过;
  • 文件整体缺失可 skip,章节缺失必须 fail。
    ② 空集合 / 空循环
    「遍历某集合、逐个断言每个元素满足 P」——集合为空时循环体一次不执行、断言恒真。
  • 对策:显式断言基数 assert len(xs) > 0(或 assert xs);
  • 典型先例:框架升级后 app.routes 被包了一层包装对象(如 FastAPI 的 _IncludedRouter),
    不递归取真实路由就会枚举到 0 条,闸门用例差点静默通过。
    ③ 断言挂错对象(最隐蔽)
    断言「某方法没被调用」时打了钩子(spy),但补丁打在了错误的类/对象上 → 钩子永不触发
    → 记录恒空 → 断言恒真。
  • 先例:patch(FastAPI 的 UploadFile, "read", spy),而路由实参实测是
    starlette.datastructures.UploadFile(两者各自都定义了 read,不是同一层)→ spy 从未触发。
  • 对策:
    a. 先实测对象身份:print(type(obj).mro)、print(type(obj).method.qualname);
    b. 打补丁打到实例真正的类上(不是名字相近的那个);
    c. 阳性对照:断言 spy 在「应该被调用」的路径上确实被调用(同一条用例里加一次正常路径),
    使「记录为空」不再可能由「补丁无效」造成。
    附加:子串断言 → 恒真
    assert "a 'b'" in s 会被 "a 'b' 'bad'" 满足 → 要「不得出现 bad」的规则根本守不住。
  • 对策:断言整条指令/整段取值相等(先按分隔符切出那一项,再 ==),而不是 in。
    实用配方
  • 阴性断言(没被调用)最少要两条对照:坏路径「没调用」+ 好路径「调用了」;
  • 精确断言优先于子串断言,尤其当被守护的是一份「不允许被加料的名单」(CSP 指令、
    请求头清单、白名单);
  • 断言引用被守护的实现常量,别在测试里再抄一遍数字/字面量(否则常量改了测试仍绿);
  • 断言若依赖第三方文案(校验库的错误消息),在用例里注明「升级该库需同步改断言」。
    反模式
  • 「跑了一遍是绿的」当作验证完成;
  • 为了让测试通过而放宽断言(把 == 改成 in、把精确值改成 startswith)——
    这是把守护拆掉换绿灯;
  • 破坏性验证的临时改动留在工作区(下一个 commit 夹带 if False 或 print 探针)。
    与「真连接验证」的配合
    单元/进程内测试(如 ASGI TestClient)覆盖不到传输层差异:客户端总会带上某个头、
    body 总是一次性喂进去。凡断言涉及传输层行为(Content-Length 有无、chunked、流式读取、
    超时、代理头),都要另外做一次真连接验证(起一个独立端口的实例跑 curl/httpx,
    用完关掉,不要打断正在使用的那个实例)。
avatar
Jason
1 个月前
不想看系列
回复