大模型应用
无测试遗留代码安全变更:用接缝…
当需要在无测试、难测试或继承来的混乱代码库中安全修 bug、加功能或重构,且不能直接大重写时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
无测试遗留代码安全变更:用接缝、特征测试通用
解决思路
- 核心定义:把 legacy code 当作没有测试的代码,而不是旧代码或丑代码;没有测试就无法知道变更是否保持行为,因此每次编辑都是赌博。
- 执行 cover and modify,绝不 edit and pray:先刚好打破依赖把测试放进去,再修改。
- 按 0-10 给未测试代码变更评分,报告当前分数和达到 10/10 的具体步骤;目标 9-10 分标准是:变更点在任何编辑前被 characterization tests 覆盖,行为变更和重构作为分开的已验证步骤发布,依赖用最小侵入技术打破。
- 运行五步遗留代码变更算法:识别 change points,找到 test points,打破依赖,写测试,然后做变更并重构。
- 让测试前的编辑保守且机械;真正的变更只在安全网内进行。
- 区分两类变更:改变行为(feature、bug fix)和改进结构(refactoring);不要在同一步混合,否则失败无法诊断。
- 把重构和行为变更拆成单独 commits,并确保每一步之间测试变绿。
- 记住 test points 很少等于 change points:效果会传播,通常要在变更效果显现处测试,而不是编辑发生处。
- 在测试存在前做 dependency-breaking edits 时,必须完全保持签名,并依赖编译器找出每个受影响位置。
- 让覆盖沿你实际修改的路径增长,而不是等待一个专门且永远拿不到经费的 testing project。
- 遵循一次只做一件事的编程方式,让算法每一步都可独立验证。
- 在未测试模块修 bug 时,先运行五步再碰 bug:用测试 pin 住 parseInvoice(),然后修复 rounding error。
- PR 混合 cleanup 和 feature 时,拆成 structure-only 和 behavior-only commits:先 extract 和 rename,测试变绿,再加 discount rule。
- 面对“只是一行改动”时,先找最近的 test point:在调用你编辑的 private 方法的 public 方法上写一个 pin test。
- 寻找 seam:seam 是可以在不编辑该处的情况下改变程序行为的位置;每个 seam 都有 enabling point,即决定运行哪段行为的位置。
- 在 OO 代码中默认找 object seams:每个可覆盖调用都是 seam,其 enabling point 在对象被创建或传入处。
- 在构建或加载时替换实现时使用 link 和 import seams;jest.mock 和 unittest.mock.patch 是现代形式的 link seams。
- 把 preprocessing seams(C/C++ macros)作为最钝的工具,最后再考虑。
- 识别 seam 死亡处:方法体内 new Database()、构造函数做实际工作、globals、statics、hard-wired I/O。
- 记住没有可达 enabling point 的 seam 没用:如果测试无法做出选择,就继续找。
- 在动态语言中,几乎每个名称查找都是 seam;这很便宜,但 patch internals 会让测试耦合到文件布局。
- 类自己构造 DB client 时,用构造函数参数做 object seam:constructor(db: Db = new ProdDb()),测试传入 fake。
- 模块调用顶层 send_email() 时,用 import/link seam:mocker.patch("billing.send_email") 或 jest.mock("./mailer")。
- 逻辑直接读墙钟时,在 clock 处开 seam:注入 now() provider,测试冻结时间。
- 写 characterization tests:记录代码当前实际做什么,而不是 spec、注释或任何人记忆中说它应该做什么。
- 使用配方:在 harness 中调用代码,先断言荒谬值(例如 expect(total).toBe(-1)),读失败信息暴露真实行为,然后把断言改成固定观察到的值。
- 知道打破依赖有两个原因:separation 让代码进入 harness,sensing 让断言看到代码计算出的结果。
- 对复杂输出(reports、generated files、大 JSON)使用 golden master:捕获完整输出一次,之后永久 diff 对比。
- 知道 snapshot tests 是 golden masters:第一次 snapshot 要像审查代码一样审查,并规范化易变数据,否则只是在固定噪声。
- characterizing 时发现 bug:用注释和 ticket 固定它,因为 downstream code 可能依赖错误行为,之后作为独立的有意变更修复。
- 只 characterization 你的变更会触及的分支,不要试图 characterization 整个系统;覆盖跟随变更。
- 重构 tax calculator 时,为代表性输入固定输出:跑 20 个 cases,逐个断言记录结果。
- 遇到 legacy report generator,用 golden master diff:生成报告,与 checked-in master file 对比。
- 在 pinning 时发现 off-by-one,固定错误值并记录:assert days == 30 # BUG? expected 31 — TICKET-482。
- 当今天确实无法让区域可测时,不要把新逻辑织进未测试的大块代码;用 Sprout Method 或 Sprout Class,把新行为写成全新的、完全测试的代码,并从 legacy 位置用一行调用它。
- 新逻辑只在一个点接入时用 Sprout Method;host class 在测试 harness 中甚至无法实例化时用 Sprout Class。
- 当新增行为围绕旧代码(logging、notification、metering)而不是混入旧代码时,用 Wrap Method:把 pay() 重命名为 rawPay(),重建 pay() 作为 wrapper。
- 当多个调用点需要新增行为,或类已经臃肿时,用 Wrap Class,也就是 Decorator pattern。
- 诚实看待取舍:host 仍未测试,你只是把好代码加到了坏邻居旁边。
- 将 sprouts 作为 debt 跟踪并偿还:host 下次发生变更时覆盖它。
- 把 sprouting 当作变更算法内部的战术动作,而不是让代码进入测试的永久替代方案。
- 在 400 行 process() 中加 late-fee rule 时,用 Sprout Method 和一行调用 total += lateFee(order),其中 lateFee() 测试先行。
- 围绕 legacy pay() 加 audit logging 时,用 Wrap Method:新 pay() 先 log,调用 rawPay(),再 log。
- 新增 validation 且类无法实例化时,用 Sprout Class:从 legacy code 调用 new OrderValidator().validate(data)。
- 从依赖打破技术目录选招:Extract Interface、Parameterize Constructor、Parameterize Method、Extract and Override Factory Method or Getter、Introduce Instance Delegator、Adapt Parameter、Break Out Method Object、Subclass and Override Method。
- 因为这些技术在没有测试时运行,总是选择能解除阻塞的最小侵入技术。
- 把 Parameterize Constructor with production default 当作主力:现有 callers 不改也能编译,测试注入 fakes。
- 把 Extract Interface 当作书中最安全动作:引入 interface 不会改变行为,只会放松类型。
- 把 Subclass and Override Method 当作半个目录的基础:用 testing subclass 对危险部分打桩是合法工具,不是 hack。
- 处理 statics 和 singletons 时,用 Introduce Instance Delegator 交给 callers 一个可替换实例;在测试中也可用 static setter 取代 singleton。
- Adapt Parameter 优于与不可 fake 的 framework types 斗争:把 HttpServletRequest 包装成自己的窄接口,并针对它测试。
- 动态语言有更便宜的 seams:unittest.mock.patch 或 jest.mock 可以代替多种技术,但 parameterizing 会留下更好的设计。
- 构造函数打开 DB connection 时,用 Parameterize Constructor:def __init__(self, conn=None): self.conn = conn or connect()。
- 到处调用 static Billing.charge() 时,用 Introduce Instance Delegator:实例 charge() 委托给 static,测试 override 它。
- 900 行方法囤积 locals 时,用 Break Out Method Object:new RateCalculation(order, rates).run(),locals 变成 fields。
- 在修改不理解的代码前,投资便宜的理解手段:effect sketches 追踪变更可能影响什么,feature sketches 显示 god class 内方法和字段如何聚类,scratch refactoring 随意重构以学习然后丢弃编辑,telling the story of the system 强制做简化总结。
- 用 effect sketch:每个变量或方法一个 bubble,每个 affects 一条 arrow,从 change point 向前追踪到行为可能泄漏出的每个地方。
- 寻找 pinch point:effect sketch 中的收窄处;在那里测试,其上游所有内容都被覆盖。
- 把 scratch refactoring 当作阅读技术:extract、rename、simplify 一小时,然后 revert;洞察会在 checkout 后保留。
- 处理 monster method 的策略:在 pinch point 做 golden-master,Break Out Method Object,然后在新类内部重构。
- 处理 god class 的策略:先 feature-sketch 聚类,再沿它们之间的自然边界 extract。
- 没时间时做 triage:只改一次的地方用 sprout 或 wrap;同一地方再次变更就有资格写测试。
- 面对“改这个 field 会破坏什么?”时,从 field 向外画 effect sketch;找到 readers 后,用少量 pinch-point tests 覆盖它们。
- 面对 5,000 行类里要交付 feature 时,先写 pinch-point tests,再 sprout;覆盖 postInvoice(),把新规则 sprout 成类。
- 面对团队没人理解的代码时,在 branch 上 scratch refactor:extract 和 rename 以学习,revert,然后计划真正的动作。
- 避免在同一步骤中重构和改变行为;修复方式:分开 commits,每步之间测试变绿。
- 避免在 legacy code 上写 should tests;修复方式:characterize 代码实际行为,bug 单独 file。
- 避免 mock 眼前一切;修复方式:只 fake 那些阻塞 instantiation 或 sensing 的东西。
- 避免 big-bang rewrite 替代增量覆盖;修复方式:cover and modify,一块一块来。
- 避免静默修复 characterizing 时发现的 bug;修复方式:pin 它、记录它,作为单独的、有意的变更修复。
- 避免在任何测试存在前做侵入式 cleanup;修复方式:用最小侵入技术,保持签名,依赖编译器。
- 避免永远 sprout 而不偿还;修复方式:跟踪 sprout debt,下次 touch 热点时覆盖。
- 避免等待专门的 testing project;修复方式:沿每次交付的变更增长覆盖。
- 做快速诊断:如果测试未覆盖你即将修改的代码,就运行 change algorithm,并在编辑前 pin behavior。
- 如果无法在测试 harness 中构造类,用 Parameterize Constructor、Extract Interface 或 Sprout Class。
- 如果测试无法感知变更效果,找 sensing point;用 Extract and Override Getter。
- 如果 commit 不是 behavior-only 或 structure-only,就拆分它,并在两步之间运行测试。
- 如果你不知道这次变更会影响的一切,画 effect sketch,并在 pinch points 测试。
- 如果你的断言没有陈述观察到的行为,就 probe、读失败信息、pin 实际值。
- 如果你选的 seam 不是可用的最便宜 seam,优先 constructor parameters 和 import seams。
- 如果变更后代码不会得到更好覆盖,至少在最近的 test point 留一个 pin test。
适用边界 · 注意事项
- 代码已经有测试且只是常规重构时,不用本技能,改用 refactoring-patterns。
- 日常代码质量/clean code 问题,不用本技能,改用 clean-code。
- 计划进行 big-bang rewrite 而不是增量补测和逐步修改时,不适用。