working-with-legacy-code
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWorking Effectively with Legacy Code
《修改既有代码的有效方法》(Working Effectively with Legacy Code)
A field manual for changing code that has no tests, distilled from Michael C. Feathers' Working Effectively with Legacy Code. Use it to get untestable classes into a harness, pin down current behavior with characterization tests, and make changes one safe, verifiable step at a time — without resorting to a rewrite.
这份实用指南提炼自Michael C. Feathers的《修改既有代码的有效方法》,专为修改无测试代码而打造。你可以用它将无法测试的类纳入测试框架、通过特征测试锁定当前行为,并以安全、可验证的步骤逐步修改代码——无需全盘重写。
Core Principle
核心原则
Legacy code is simply code without tests. Not old code, not ugly code — untested code: without tests you cannot know whether a change preserves behavior, so every edit is a gamble. The craft is breaking dependencies just enough to get tests in place before changing anything — cover and modify, never edit and pray.
遗留代码指的就是没有测试的代码。 不是老旧代码,也不是丑陋代码——是未测试的代码:没有测试的话,你无法确认修改是否保留原有行为,因此每一次编辑都是一场赌博。关键技巧是在修改前仅打破必要的依赖以引入测试——先覆盖再修改,绝不能直接编辑后寄希望于不出问题。
Scoring
评分标准
Goal: 10/10. Rate changes to untested code 0-10 against the principles below. Report the current score and the specific steps needed to reach 10/10.
- 9-10: Change points covered by characterization tests before any edit; behavior changes and refactoring shipped as separate verified steps; dependencies broken with the least invasive technique
- 7-8: Tests at most change points, but occasional mixed refactor-plus-behavior commits or heavier dependency surgery than needed
- 5-6: Some characterization tests, yet key paths still changed on faith; sprouted code accumulating with no payback plan
- 3-4: Edit-and-pray with manual verification; tests written after the change, asserting whatever the new code happens to do
- 0-2: Untested edits straight into tangled code, refactoring and behavior change mixed in one commit, rewrite proposed instead of tests
目标:10/10。 按照以下原则对未测试代码的修改进行0-10分评分,并报告当前分数以及达到10/10所需的具体步骤。
- 9-10分: 修改点在编辑前已被特征测试覆盖;行为变更与重构作为独立的已验证步骤交付;使用侵入性最低的技术打破依赖
- 7-8分: 大部分修改点有测试,但偶尔会出现重构与行为变更混合的提交,或使用了过度复杂的依赖处理方式
- 5-6分: 有部分特征测试,但关键路径仍凭经验修改;新增的萌芽代码不断累积却无后续优化计划
- 3-4分: 采用“编辑后祈祷”模式并手动验证;修改完成后才编写测试,仅断言新代码的当前行为
- 0-2分: 直接在混乱代码中进行未测试编辑,重构与行为变更混合在一个提交中,提议重写而非添加测试
Framework
框架
1. The Legacy Code Dilemma and Change Algorithm
1. 遗留代码困境与修改算法
Core concept: The dilemma: to change code safely we need tests, but to get tests in place we have to change code. The way out is a fixed sequence — identify change points, find test points, break dependencies, write tests, then make changes and refactor — where the pre-test edits are conservative and mechanical, and the real change happens only inside the safety net.
Why it works: Edit-and-pray substitutes care for feedback, and care doesn't scale to code you don't fully understand. Cover-and-modify clamps existing behavior in a vise of tests, so any unintended change announces itself immediately on your machine instead of later in production.
Key insights:
- There are two reasons to change code — changing behavior (feature, bug fix) and improving structure (refactoring) — and mixing them in one step makes failures undiagnosable
- Test points are rarely the change points: effects propagate, so you often test where the change's effects surface, not where the edit happens
- Dependency-breaking edits made before tests exist must preserve signatures exactly and lean on the compiler to find every affected site
- Coverage grows along the paths you actually change — that beats any dedicated "testing project" that never gets funded
- "Programming is the art of doing one thing at a time": each step of the algorithm is separately verifiable
Applications:
| Context | Application | Example |
|---|---|---|
| Bug fix in an untested module | Run the five steps before touching the bug | Pin |
| PR mixing cleanup and a feature | Split into structure-only and behavior-only commits | Extract and rename first, tests green, then add the discount rule |
| "It's just a one-line change" | Find the nearest test point first | One pin test at the public method that calls the private one you edit |
See references/change-algorithm.md when running the five steps on a real change — the algorithm as a working procedure with change-point/test-point checklists and triage for "no time" situations.
核心概念: 困境在于:要安全修改代码需要测试,但要添加测试又必须修改代码。解决之道是遵循固定流程——确定修改点、找到测试点、打破依赖、编写测试,然后进行修改与重构——其中测试前的编辑需保守且机械,真正的修改仅在测试的安全保障下进行。
为何有效: “编辑后祈祷”模式用谨慎替代反馈,但谨慎在你不完全理解的代码中无法规模化。“先覆盖再修改”通过测试牢牢锁定现有行为,任何意外变更都会立即在你的机器上显现,而非等到上线后才暴露。
关键见解:
- 修改代码有两个原因——变更行为(功能开发、bug修复)和优化结构(重构),将两者混为一步会导致故障无法排查
- 测试点很少是修改点:效果会传播,因此你通常测试的是修改效果显现的位置,而非编辑发生的位置
- 在添加测试前进行的打破依赖编辑必须完全保留签名,并依靠编译器找到所有受影响的位置
- 测试覆盖范围会沿着你实际修改的路径扩展——这比任何从未获得资源支持的“专项测试项目”更有效
- “编程是一次只做一件事的艺术”:算法的每一步都可单独验证
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
| 未测试模块中的bug修复 | 在修复bug前执行五步流程 | 用测试锁定 |
| 混合了代码清理与功能开发的PR | 拆分为仅修改结构和仅修改行为的提交 | 先提取并重命名,确保测试通过,再添加折扣规则 |
| “这只是一行小修改” | 先找到最近的测试点 | 在调用你要编辑的私有方法的公共方法处添加一个锁定测试 |
如需在实际修改中执行五步流程,请查看references/change-algorithm.md——该文档包含修改点/测试点检查表以及“时间紧张”情况下的优先级处理,将算法转化为可操作的流程。
2. Seams: Where to Pry Code Apart
2. 接缝:代码的拆解点
Core concept: A seam is a place where you can alter behavior in your program without editing in that place. Every seam has an enabling point — where you decide which behavior runs. Getting legacy code under test is largely a hunt for seams: spots where a test can substitute a slow, global, or external dependency while the production source stays untouched.
Why it works: If you must edit code to test it, you risk changing the very behavior you are trying to pin down. Seams move the substitution to a distance — a subclass, an import, a build flag — so the code under test runs exactly as in production while the test controls its dependencies from the enabling point.
Key insights:
- Object seams are the default in OO code: every overridable call is a seam, and its enabling point is wherever the object is created or passed in
- Link and import seams swap implementations at build or load time — and
jest.mockare link seams in modern clothingunittest.mock.patch - Preprocessing seams (C/C++ macros) are the bluntest instrument; reach for them last
- inside a method body is a seam that never got built — constructors doing real work, globals, statics, and hard-wired I/O are where seams die
new Database() - A seam without a reachable enabling point is useless: if the test can't make the decision, keep hunting
- Dynamic languages make nearly every name lookup a seam — cheap, but patching internals couples tests to file layout
Applications:
| Context | Application | Example |
|---|---|---|
| Class constructs its own DB client | Object seam via constructor parameter | |
Module calls a top-level | Import/link seam | |
| Logic reads the wall clock directly | Seam at the clock | Inject a |
See references/seams.md when hunting a seam in a specific stack — the seam catalog with code, enabling points, and seams in modern tooling (DI containers, jest.mock, pytest monkeypatch, clock and config seams).
核心概念: 接缝是你无需修改代码本身就能改变程序行为的位置。每个接缝都有一个启用点——即你决定运行哪种行为的位置。让遗留代码可测试很大程度上就是寻找接缝:找到测试可以替换缓慢、全局或外部依赖的位置,同时保持生产代码完全不变。
为何有效: 如果必须修改代码才能测试它,你就有可能改变你试图锁定的行为。接缝将替换操作转移到其他位置——子类、导入项、构建标志——因此测试中的代码与生产环境中运行的代码完全一致,同时测试可从启用点控制其依赖。
关键见解:
- 对象接缝是面向对象代码中的默认选择:每个可重写的调用都是一个接缝,其启用点是对象被创建或传入的任何位置
- 链接与导入接缝在构建或加载时替换实现——和
jest.mock就是现代工具中的链接接缝unittest.mock.patch - 预处理接缝(C/C++宏)是最生硬的工具,应作为最后选择
- 方法体内的是一个未被设计的接缝——执行实际操作的构造函数、全局变量、静态方法和硬编码I/O都会导致接缝失效
new Database() - 没有可访问启用点的接缝毫无用处:如果测试无法做出选择,就继续寻找
- 动态语言几乎让每个名称查找都成为接缝——成本低,但修补内部会让测试与文件结构耦合
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
| 类自行构造DB客户端 | 通过构造函数参数实现对象接缝 | |
模块调用顶级 | 使用导入/链接接缝 | |
| 逻辑直接读取系统时间 | 在时钟处设置接缝 | 注入 |
如需在特定技术栈中寻找接缝,请查看references/seams.md——该文档包含接缝目录、代码示例、启用点以及现代工具(DI容器、jest.mock、pytest monkeypatch、时钟与配置接缝)中的接缝用法。
3. Characterization Tests
3. 特征测试
Core concept: A characterization test documents what the code actually does right now — not what the spec, the comments, or anyone's memory says it should do. Write a probe you know will fail, let the failure message reveal the real behavior, then change the assertion to pin that behavior in place.
Why it works: In legacy systems the actual behavior is the de facto spec: callers, reports, and customers may depend on it, quirks included. Tests written from imagined requirements fail for reasons that tell you nothing, while characterization tests fail during refactoring precisely when — and where — you changed existing behavior.
Key insights:
- The recipe: call the code in a harness, assert something absurd (), read the failure, pin the observed value
expect(total).toBe(-1) - Sensing and separation are the two reasons to break dependencies: separation gets code into a harness, sensing lets assertions see what it computed
- For complex output (reports, generated files, large JSON) use a golden master: capture the full output once, diff against it forever
- Snapshot tests are golden masters — review the first snapshot like code and normalize volatile data, or you are pinning noise
- Found a bug while characterizing? Pin it with a comment and a ticket — downstream code may depend on the wrong behavior; fix it later as a deliberate, separate change
- Characterize the branches your change will touch, not the whole system — coverage follows change
Applications:
| Context | Application | Example |
|---|---|---|
| Refactoring a tax calculator | Pin outputs for representative inputs | Run 20 cases through, assert each recorded result |
| Legacy report generator | Golden master diff | Generate the report, compare to a checked-in master file |
| Off-by-one found while pinning | Pin the wrong value, document it | |
See references/characterization-tests.md when writing your first probe through to a pinned suite — golden masters, snapshot tests done right, and a worked before/after refactor.
核心概念: 特征测试记录代码当前的实际行为——而非规格说明、注释或任何人记忆中它应该有的行为。编写一个你知道会失败的探测用例,让失败信息揭示真实行为,然后修改断言以锁定该行为。
为何有效: 在遗留系统中,实际行为就是事实上的规格:调用方、报告和客户可能依赖于它,包括各种奇奇怪怪的行为。基于想象需求编写的测试失败时无法提供有效信息,而特征测试会在重构期间准确地在你修改了现有行为的位置失败。
关键见解:
- 流程:在测试框架中调用代码,断言一个明显错误的值(如),读取失败信息,锁定观察到的值
expect(total).toBe(-1) - 打破依赖有两个原因:分离(将代码纳入测试框架)和感知(让断言能看到代码的计算结果)
- 对于复杂输出(报告、生成文件、大型JSON),使用黄金基准:一次性捕获完整输出,之后永远与该基准对比
- 快照测试就是黄金基准——像审查代码一样审查第一个快照,并标准化易变数据,否则你锁定的只是无用信息
- 在特征测试过程中发现bug?用注释和工单锁定它——下游代码可能依赖于错误行为;之后再作为单独的、有计划的变更进行修复
- 仅对你要修改的分支进行特征测试,而非整个系统——测试覆盖跟随修改路径
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
| 重构税务计算器 | 为代表性输入锁定输出 | 运行20个测试用例,断言每个记录的结果 |
| 遗留报告生成器 | 黄金基准对比 | 生成报告,与已提交的基准文件对比 |
| 锁定时发现差一错误 | 锁定错误值并记录 | |
如需从编写第一个探测用例到完成锁定测试套件的完整流程,请查看references/characterization-tests.md——包含黄金基准、正确的快照测试用法以及重构前后的完整示例。
4. Sprout and Wrap: Changing Without Tests First
4. 萌芽与包装:无需先添加测试即可修改代码
Core concept: When you genuinely cannot get the area under test today, don't weave new logic into the untested mass. Sprout Method or Sprout Class: write the new behavior as fresh, fully tested code and call it from a single line in the legacy spot. Wrap Method or Wrap Class: rename the old code aside and add behavior before or after the call to it, decorator-style.
Why it works: New code in a fresh method or class can be test-driven even when its host can't be instantiated in a harness — testability no longer waits on getting the host into a harness. The untested host changes by exactly one call site, so the unverified blast radius is a single line instead of the whole method.
Key insights:
- Sprout Method when new logic plugs in at one point; Sprout Class when the host class won't even instantiate in a test harness
- Wrap Method suits behavior that surrounds the old code (logging, notification, metering) rather than mixes with it: rename to
pay(), recreaterawPay()as the wrapperpay() - Wrap Class is the Decorator pattern — use it when several call sites need the added behavior or the class is already bloated
- Be honest about the trade-off: the host stays untested; you have added good code to a bad neighborhood
- Track sprouts as debt and pay them back — cover the host the next time a change lands there
- Sprouting is a tactical move inside the change algorithm, not a permanent substitute for getting code under test
Applications:
| Context | Application | Example |
|---|---|---|
Late-fee rule in a 400-line | Sprout Method, one call line | |
Audit logging around legacy | Wrap Method | New |
| New validation, class won't instantiate | Sprout Class | |
核心概念: 当你确实无法在当前为目标代码添加测试时,不要将新逻辑嵌入未测试的代码中。使用萌芽方法(Sprout Method)或萌芽类(Sprout Class):将新行为编写为全新的、经过充分测试的代码,然后在遗留代码中的某个位置通过一行代码调用它。使用包装方法(Wrap Method)或包装类(Wrap Class):将旧代码重命名,然后在调用它之前或之后添加行为,类似于装饰器模式。
为何有效: 即使宿主代码无法在测试框架中实例化,新代码在全新方法或类中也可进行测试驱动开发——可测试性不再依赖于将宿主代码纳入测试框架。未测试的宿主代码仅变更一个调用点,因此未验证的影响范围仅为一行代码,而非整个方法。
关键见解:
- 当新逻辑在一个点插入时使用萌芽方法;当宿主类甚至无法在测试框架中实例化时使用萌芽类
- 包装方法适合围绕旧代码的行为(日志、通知、计量),而非与旧代码混合:将重命名为
pay(),重新创建rawPay()作为包装器pay() - 包装类就是装饰器模式——当多个调用点需要添加行为,或类已过于臃肿时使用
- 诚实地看待权衡:宿主代码仍未测试;你在糟糕的代码环境中添加了优质代码
- 将萌芽代码视为技术债务并逐步偿还——下次修改宿主代码时为其添加测试
- 萌芽是修改算法中的战术手段,而非替代为代码添加测试的永久方案
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
400行 | 萌芽方法,仅一行调用代码 | |
遗留 | 包装方法 | 新的 |
| 新增验证逻辑,但类无法实例化 | 萌芽类 | 在遗留代码中调用 |
5. Dependency-Breaking Techniques
5. 打破依赖的技巧
Core concept: A catalog of mechanical, low-risk moves that sever whatever blocks instantiation or sensing: 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. Because they run before tests exist, always pick the least invasive technique that unblocks you.
Why it works: Code resists testing for a small set of recurring reasons — constructors doing real work, statics and singletons, parameters you can't construct, monster methods. Each blocker has a named, practiced counter-move, so you execute a known maneuver instead of improvising surgery on code that has no safety net.
Key insights:
- Parameterize Constructor with a production default is the workhorse: existing callers compile untouched while tests inject fakes
- Extract Interface is the safest move in the book — introducing an interface can't change behavior, only loosen a type
- Subclass and Override Method underlies half the catalog: a testing subclass that stubs the dangerous parts is a legitimate tool, not a hack
- For statics and singletons, Introduce Instance Delegator hands callers an instance they can swap; a static setter can supersede a singleton in tests
- Adapt Parameter beats fighting unfakeable framework types — wrap in your own narrow interface and test against that
HttpServletRequest - Dynamic languages have cheaper seams: or
unittest.mock.patchcan stand in for several techniques, but parameterizing leaves better design behindjest.mock
Applications:
| Context | Application | Example |
|---|---|---|
| Constructor opens a DB connection | Parameterize Constructor | |
Static | Introduce Instance Delegator | Instance |
| 900-line method hoarding locals | Break Out Method Object | |
See references/dependency-breaking.md when a specific blocker stops instantiation or sensing — before/after code for each technique plus a decision table mapping blockers to the right move.
核心概念: 一套机械、低风险的操作目录,用于解决阻碍实例化或感知的问题:提取接口(Extract Interface)、参数化构造函数(Parameterize Constructor)、参数化方法(Parameterize Method)、提取并覆盖工厂方法或Getter(Extract and Override Factory Method or Getter)、引入实例委托器(Introduce Instance Delegator)、适配参数(Adapt Parameter)、提取方法对象(Break Out Method Object)、子类化并覆盖方法(Subclass and Override Method)。由于这些操作在添加测试前执行,始终选择能解决问题的侵入性最低的技术。
为何有效: 代码抗拒测试的原因只有少数几种重复情况——执行实际操作的构造函数、静态方法和单例模式、无法构造的参数、巨型方法。每种障碍都有对应的、经过实践验证的应对方法,因此你可以执行已知操作,而非在没有安全保障的代码上即兴修改。
关键见解:
- 带有生产环境默认值的参数化构造函数是主力工具:现有调用方无需修改即可编译,同时测试可注入假对象
- 提取接口是最安全的操作——引入接口不会改变行为,只会放宽类型限制
- 子类化并覆盖方法是目录中半数技巧的基础:用于存危险部分的测试子类是合法工具,而非 hack
- 对于静态方法和单例模式,引入实例委托器为调用方提供可替换的实例;静态setter可在测试中替代单例
- 适配参数比对抗无法伪造的框架类型更有效——将包装在你自己的窄接口中,并针对该接口测试
HttpServletRequest - 动态语言有更廉价的接缝:或
unittest.mock.patch可替代多种技巧,但参数化能留下更好的设计jest.mock
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
| 构造函数打开DB连接 | 参数化构造函数 | |
到处调用静态 | 引入实例委托器 | 实例 |
| 900行方法包含大量局部变量 | 提取方法对象 | |
如需解决特定的实例化或感知障碍,请查看references/dependency-breaking.md——包含每种技巧的前后代码示例,以及将障碍映射到正确操作的决策表。
6. Untangling and Understanding
6. 梳理与理解
Core concept: Before changing code you don't understand, invest in cheap comprehension: effect sketches trace what a change can affect, feature sketches show how methods and fields cluster inside a god class, scratch refactoring means refactoring recklessly to learn and then throwing the edits away, and telling the story of the system forces a simplifying summary. The payoff is finding pinch points — narrow places where a few tests cover wide behavior.
Why it works: In legacy code the bottleneck is comprehension, not typing. An effect sketch turns "what could this break?" from anxiety into a finite list, and a pinch point lets a handful of tests act as a vise over an entire cluster of methods — often revealing where a hidden class boundary wants to be drawn.
Key insights:
- Effect sketch: a bubble per variable or method, an arrow per "affects" — trace forward from your change point to every place behavior can leak out
- A pinch point is a narrowing in the effect sketch; test there and everything upstream of it is covered
- Scratch refactoring is refactoring as a reading technique: extract, rename, and simplify for an hour, then revert — the insight survives the checkout
- Monster method strategy: golden-master it at a pinch point, Break Out Method Object, then refactor inside the new class
- God class strategy: feature-sketch the clusters, then extract along the natural boundaries between them
- Triage when there's no time: a spot changing once gets a sprout or wrap; the same spot changing again has earned its tests
Applications:
| Context | Application | Example |
|---|---|---|
| "What breaks if I change this field?" | Effect sketch from the field outward | Three readers found; two pinch-point tests cover them |
| Feature due in a 5,000-line class | Pinch-point tests, then sprout | Cover |
| Code nobody on the team understands | Scratch refactor on a branch | Extract and rename to learn, revert, plan the real moves |
See references/case-studies.md when you want a full worked walkthrough — three scenarios: a feature in an untested 800-line service, a singleton-ridden module brought under test, and a monster method tamed before a bug fix.
核心概念: 在修改你不理解的代码前,先投入低成本的理解工作:影响草图追踪修改可能影响的范围,功能草图展示巨型类中方法和字段的聚类,临时重构指通过肆意重构来学习然后丢弃编辑内容,讲述系统的故事则需要简化总结。回报是找到关键点——少量测试即可覆盖广泛行为的狭窄位置。
为何有效: 在遗留代码中,瓶颈是理解,而非编码。影响草图将“这会破坏什么?”从焦虑转化为有限列表,关键点则让少量测试像夹具一样控制整个方法集群——通常会揭示隐藏的类边界。
关键见解:
- 影响草图:每个变量或方法对应一个气泡,每个“影响”对应一条箭头——从你的修改点向前追踪到所有行为可能泄露的位置
- 关键点是影响草图中的狭窄处;在那里测试,其上游的所有内容都会被覆盖
- 临时重构是将重构作为阅读技巧:提取、重命名、简化一小时,然后回滚——洞察会保留下来
- 巨型方法策略:在关键点设置黄金基准,提取方法对象,然后在新类中重构
- 上帝类策略:绘制功能草图聚类,然后沿着自然边界提取
- 时间紧张时的优先级处理:仅修改一次的位置使用萌芽或包装;多次修改的位置值得添加测试
应用场景:
| 场景 | 应用方式 | 示例 |
|---|---|---|
| “修改这个字段会破坏什么?” | 从字段向外绘制影响草图 | 找到三个调用方;两个关键点测试即可覆盖它们 |
| 5000行类中的功能开发 | 关键点测试,然后使用萌芽 | 覆盖 |
| 团队无人理解的代码 | 在分支上进行临时重构 | 提取并重命名以学习,回滚,规划实际修改步骤 |
如需完整的实践演练,请查看references/case-studies.md——包含三个场景:未测试的800行服务中的功能开发、单例泛滥的模块纳入测试、巨型方法在bug修复前被驯服。
Common Mistakes
常见错误
| Mistake | Why It Fails | Fix |
|---|---|---|
| Refactoring and changing behavior in one step | When something breaks, you can't tell which edit did it | Separate commits; tests green between each step |
| Writing "should" tests on legacy code | Imagined specs fail noisily and you "fix" load-bearing behavior | Characterize what the code does; file bugs separately |
| Mocking everything in sight | Tests pin the implementation, so every refactor breaks them | Fake only what blocks instantiation or sensing |
| Big-bang rewrite instead of incremental coverage | The old system keeps moving; rewrites ship late and miss years of edge cases | Cover and modify piece by piece |
| Silently fixing bugs found while characterizing | Callers and reports may depend on the wrong behavior | Pin it, document it, fix it as a separate deliberate change |
| Invasive cleanup before any tests exist | Every manual edit risks behavior with no net underneath | Least invasive technique; preserve signatures; lean on the compiler |
| Sprouting forever without payback | The host stays untested and sprouts ossify into the next legacy layer | Track sprout debt; cover hot spots on the next touch |
| Waiting for a dedicated "testing project" | That project never gets funded; coverage never appears | Grow coverage along every change you ship |
| 错误 | 失败原因 | 修复方案 |
|---|---|---|
| 重构与行为变更混为一步 | 出现故障时无法判断是哪次编辑导致的 | 拆分提交;每一步都确保测试通过 |
| 为遗留代码编写“应该”测试 | 基于想象的规格会频繁失败,你可能会“修复”必要的行为 | 记录代码的实际行为;单独提交bug工单 |
| 模拟所有依赖 | 测试锁定了实现,因此每次重构都会破坏测试 | 仅模拟阻碍实例化或感知的依赖 |
| 全盘重写而非增量覆盖 | 旧系统持续迭代;重写交付延迟且会遗漏多年积累的边缘情况 | 逐步覆盖并修改 |
| 在特征测试时悄悄修复bug | 调用方和报告可能依赖于错误行为 | 锁定该行为,记录下来,作为单独的有计划变更修复 |
| 在添加测试前进行侵入性清理 | 每次手动编辑都可能在无安全保障的情况下改变行为 | 使用侵入性最低的技术;保留签名;依靠编译器 |
| 永远使用萌芽而不偿还债务 | 宿主代码仍未测试,萌芽代码会固化为下一层遗留代码 | 追踪萌芽债务;下次修改热点位置时添加测试 |
| 等待专项“测试项目” | 该项目永远无法获得资源;测试覆盖永远无法实现 | 在每次交付的修改中扩展测试覆盖 |
Quick Diagnostic
快速诊断
| Question | If No | Action |
|---|---|---|
| Do tests cover the code you're about to change? | You're editing and praying | Run the change algorithm; pin behavior before editing |
| Can you construct the class in a test harness? | Dependencies block separation | Parameterize Constructor, Extract Interface, or Sprout Class |
| Can a test sense the effect of your change? | Effects are invisible to assertions | Find a sensing point; Extract and Override Getter |
| Is this commit behavior-only or structure-only? | Mixed | Split it; run the tests between the two |
| Do you know everything this change can affect? | Unknown blast radius | Draw an effect sketch; test at the pinch points |
| Do your assertions state observed behavior? | Testing wishes | Probe, read the failure, pin the actual value |
| Is the seam you chose the cheapest one available? | Needless surgery | Prefer constructor parameters and import seams first |
| Will the code be better covered after this change? | The next change costs as much as this one | Leave at least one pin test at the nearest test point |
| 问题 | 如果答案为否 | 行动 |
|---|---|---|
| 你即将修改的代码有测试覆盖吗? | 你正在采用“编辑后祈祷”模式 | 执行修改算法;在编辑前锁定行为 |
| 你能在测试框架中构造该类吗? | 依赖阻碍了分离 | 参数化构造函数、提取接口或使用萌芽类 |
| 测试能感知到你修改的效果吗? | 效果无法被断言捕获 | 找到感知点;提取并覆盖Getter |
| 本次提交仅修改行为或仅修改结构吗? | 混合修改 | 拆分提交;两步之间确保测试通过 |
| 你知道本次修改会影响所有内容吗? | 影响范围未知 | 绘制影响草图;在关键点测试 |
| 你的断言基于观察到的行为吗? | 测试的是期望 | 探测,读取失败信息,锁定实际值 |
| 你选择的接缝是成本最低的吗? | 进行了不必要的修改 | 优先选择构造函数参数和导入接缝 |
| 本次修改后代码的测试覆盖会更好吗? | 下次修改成本与本次相同 | 在最近的测试点至少留下一个锁定测试 |
Further Reading
延伸阅读
- "Working Effectively with Legacy Code" by Michael C. Feathers
- "Refactoring: Improving the Design of Existing Code" by Martin Fowler
- "Tidy First?: A Personal Exercise in Empirical Software Design" by Kent Beck
- "Kill It with Fire: Manage Aging Computer Systems (and Future Proof Modern Ones)" by Marianne Bellotti
- Michael C. Feathers所著的《修改既有代码的有效方法》(Working Effectively with Legacy Code)
- Martin Fowler所著的《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)
- Kent Beck所著的《先整理?:实证软件设计的个人实践》(Tidy First?: A Personal Exercise in Empirical Software Design)
- Marianne Bellotti所著的《用火杀死它:管理老化计算机系统(并使现代系统面向未来)》(Kill It with Fire: Manage Aging Computer Systems (and Future Proof Modern Ones))
About the Author
关于作者
Michael C. Feathers is the founder of R7K Research & Conveyance, a consultancy focused on software design and the rehabilitation of aging systems. A long-time consultant and conference speaker on legacy code, he wrote Working Effectively with Legacy Code (2004) and gave the field its working definition: legacy code is simply code without tests.
Michael C. Feathers是R7K Research & Conveyance的创始人,该咨询公司专注于软件设计和老化系统的修复。他是长期从事遗留代码咨询的专家和会议演讲者,著有《修改既有代码的有效方法》(2004年),并为该领域给出了实用定义:遗留代码指的就是没有测试的代码。",