基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-09 01:34

重构前锁定行为,再用变异验证测试

dev.to · AI#重构#回归测试#变异测试
Editor brief · 编辑速览

文章建议先用接近真实业务的 JSONL 输入建立固定行为快照,再故意引入缺陷,确认测试能够识别变化后才开始小步重构。对于依赖时间、网络或随机数的函数,应先隔离这些依赖,避免快照失去稳定性。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

重构老代码,最难的往往不是改写逻辑,而是回答:那些没人写进测试的行为,到底有没有变?作者给出的办法是先冻结一组输入及其输出,再故意制造错误,验证这套检查确实能报警,最后才动代码。

这套流程适合处理输入可复现、职责较小的函数。它的价值不只在于保存快照,更在于用变异测试检查快照的盲区。不过,原文提供的是方法和示例,没有实测对照数据;附带的 Python 脚本也有需要补齐的地方。

先把旧行为钉住,再讨论怎么改

作者把第一步限定得很窄:选一个函数,而不是一个文件。目标最好输入面小、没有隐藏全局变量,能够从脚本直接调用,并把选择理由写进提交信息。

如果函数依赖当前时间、网络或随机数,应先建立可替换的依赖入口。比如把时间作为参数注入,再记录包装函数的行为。否则第二次运行就可能得到不同结果,快照无法区分环境波动和代码变化。

输入用 corpus.jsonl 保存,每行一个案例,包含稳定的 id 和关键字参数 kwargs。来源可以是已有 fixture、仓库调用点,以及已经脱敏的日志载荷。不能带入客户数据、token 或邮箱。

作者认为,许多小函数可以从 20–40 个案例起步。这是经验建议,不是覆盖率保证。空字符串、零、负数、缺失字段都应纳入,因为重构同样可能改变错误路径。原文还提到重复 id,但后面的实现并不能可靠保留这种情况,后面会谈到。

输出也不能直接拿来比较。作者的 normalize 会按字符串形式的键排序字典,把列表和元组统一成列表,把集合和不可变集合排序,并将浮点数舍入到小数点后 9 位。普通对象尝试通过 vars(value) 提取属性;失败时,退回 __type__ 和空的 __attrs__。

这些规则是在定义「哪些差异可以忽略」。浮点数舍入会吞掉微小变化;只有 slots、没有实例属性字典的对象可能丢失全部内部状态。作者要求把这些取舍写进测试脚本头部,这一点很实用:后来维护的人需要知道,绿灯究竟覆盖了什么。

快照要保存异常,也要证明自己能报警

下面这段是检查的核心:调用目标函数,把正常结果或异常转换为稳定载荷,再计算指纹。

def fingerprint(mod, fn_name, case):
    fn = getattr(mod, fn_name)
    try:
        payload = {"ok": True, "out": normalize(fn(**case["kwargs"]))}
    except Exception as exc:
        payload = {"ok": False, "err": type(exc).__name__,
                   "msg": str(exc)}
    blob = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(blob.encode()).hexdigest(), payload

要关注 normalize(fn(**case["kwargs"])) 这一行:比较对象是归一化后的结果。异常则固定类名和 str(exc);如果消息含时间、路径或动态标识,仍会漂移。原文后面建议归一化异常消息,但这段代码尚未实现。

完整脚本通过 importlib.util.spec_from_file_location 加载模块,把每个案例的 sha256 和 payload 一起写入 golden.json。保留载荷方便排查,哈希用于判断变化。

命令接口包含必填的 --target、--function,以及 --corpus、--golden、--mode。其中语料和基线默认是 corpus.jsonl、golden.json,模式可选 write、check,默认 check。

作者要求先写入,再立即检查一次,确认基线能够稳定复现:

python char_harness.py --target ./src/pricing.py --function normalize_offer \
  --corpus corpus.jsonl --golden golden.json --mode write
python char_harness.py --target ./src/pricing.py --function normalize_offer --mode check

第二条命令使用了默认语料和基线路径。第一次运行成功,只说明文件写出来了;第二次仍然一致,才说明这批输入至少没有立即暴露不稳定因素。

接下来故意改坏代码。例如把 if qty >= 1: 改成 if qty > 1:,只改变数量等于 1 时的边界行为。每个变异保存成独立的小补丁,逐个应用、检查、恢复。

输出分为三类:CAUGHT 表示检查失败,错误被发现;MISSED 表示检查仍通过,语料可能没覆盖对应行为;SKIP 表示补丁无法应用或变异后无法导入,不能算覆盖证据。

快照能通过,只说明当前结果一致;故意改坏之后能失败,才证明它对这类错误有反应。

作者建议用 MISSED 驱动新增案例,再重新固定基线和探测。仍然存活的变异,需要人工判断是否与原实现等价,不能仅凭测试通过下结论。

重构期间保持零漂移,完成后再测一次变异

建立基线并验证报警能力后,才开始重命名、提取、内联或移动代码。每次提交后运行检查;如果哈希变化,作者建议回退、拆小,再尝试。

这里的「零漂移」,准确范围是已记录案例经过归一化后的行为。它无法保证所有输入都不变。

原文给了 ok: 34 cases unchanged 的输出示例,并提到 34 个案例、9 个变异的展示规模,明确说明这不是实测基准。不能拿它推断实际项目的覆盖率或耗时。

作者还用 git diff --numstat | awk '{a+=$1; d+=$2} END {print "changed:", a+d; exit ((a+d) > 40)}' 限制一次修改的新增与删除总行数不超过 40。这个阈值用于控制审查范围,并不代表 40 行以内就安全。

另一条约束更关键:凡是更新 golden.json 的提交,提交信息必须包含 BEHAVIOR:,解释为什么接受行为变化。否则,开发者可能顺手刷新快照,把回归一起保存下来。

重构完成后,还要重跑变异探测,原先被捕获的变异仍应被捕获。不过移动或重命名可能让旧补丁失效,出现 SKIP 时需要维护补丁,不能把它当作通过。

我的判断

我认同作者把「验证测试会失败」放在重构之前。和继续补手写断言相比,这种方法更容易快速记录复杂返回值及异常;手写测试则更适合表达明确的业务规则,两者可以配合使用。

它也有一个天然代价:旧行为中的 bug 会一起被固定。快照回答的是「是否发生变化」,正确性仍要由需求、业务断言和人工审查决定。

示例脚本还有几处边界。结果按 id 建字典,重复编号会覆盖前面的案例;检查只遍历已有基线,不会拒绝新增但尚未固定的案例。变异循环则把检查命令的非零退出都计为 CAUGHT,检查工具自身报错也可能制造假阳性。实际接入时,应分别处理输入校验、工具故障和行为差异。

原文披露了 MonkeyCode 产品推广合作。「免费模型访问」和「免费服务器」属于提供方的可用性声明,没有性能或成本对照。让模型起草归一化分支、比较运算符、默认值、分支及校验路径变异,是合理用途;让模型判断门禁是否通过,则缺少可靠依据。

最后,函数不能无副作用地导入、输入无法合法冻结,或者主要风险来自安全与并发时,这套方案并不合适。没有人持续维护变异集,也很难长期相信它的结果。

能拿来做什么

  • 整理 Node 项目中的纯业务函数时,可以先从参数转换、价格规则或配置归一化里挑一个小目标,收集真实形状的脱敏输入,同时固定成功与异常结果。
  • 使用 AI 辅助重构时,可以让模型提出边界条件和候选变异,再由人确认每个变异确实改变行为,并检查它能否被现有语料捕获。
  • 把基线更新纳入代码审查,要求写明 BEHAVIOR:;同时检查重复编号、新增案例,以及归一化是否隐藏了本次关注的差异。
  • 保存 MISSED 清单,用它安排下一批案例;对补丁失效、导入失败和检查工具报错分别处理,避免用一串绿灯替代有效证据。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
AI 补丁挤满 CI,如何筛选必跑测试
下一篇
AI 删掉输入校验后,先查清信任边界

资讯速览

dev.to · AI · 2026-11-09 01:34

AI 评分 8.0 / 10

相关标签

#重构#回归测试#变异测试

继续阅读

  • OpenMausBot:在聊天界面调度本地编码代理
  • AI 补丁挤满 CI,如何筛选必跑测试
  • AI 删掉输入校验后,先查清信任边界
  • 用 Claude 从千万条社区评论筛选视频
  • AutoDNA 用多智能体打通实验与仪器控制