遗留代码接缝与测试替身

共 18 题
📑 题目列表 18 题
#
★★★

1. 接缝的"类型"(type)中 preprocessing seam、link seam、object seam

遗留代码接缝(seam)的"类型"有哪几种?preprocessing seam、link seam、object seam 分别是什么?

  • 接缝的定义:能改变行为而不修改该处代码的位置
  • 三种接缝类型的含义与适用语言环境
  • 各类接缝在测试注入中的作用

接缝(seam)是"在不修改某处代码的前提下改变其行为的位置",是进攻遗留代码的插入点。三种类型:preprocessing seam(预处理接缝)在编译前通过预处理指令(如 C 的 #ifdef)替换代码,使测试能编译进不同的实现;link seam(链接接缝)在链接期通过替换被链接的实现(如动态链接库、stub 库)来替换依赖;object seam(对象接缝)在运行时通过替换对象实例(如依赖注入、子类覆写、mock)来改变协作对象的行为。现代语言(Java、JS)中 object seam 最常用,古老/系统代码中前两者更常见。

接缝类型决定了"在哪个阶段、用什么机制注入测试替身"。理解接缝类型是遗留代码测试的第一步:识别出代码中存在哪种接缝,就能选择对应的测试替身注入方式。通常 object seam 最灵活、最主流,preprocessing 和 link seam 出现在系统级或非 OO 语言中。

#
★★★

2. 接缝的"识别"(identify)中哪些点可注入测试替身

如何识别遗留代码中的接缝点?哪些位置可以注入测试替身?

  • 接缝识别的目标:找到可替换依赖的插入点
  • 可注入替身的典型位置:接口、构造器、方法调用、工厂
  • 识别方法:从依赖出发逆向追踪

识别接缝就是从"被测代码对外部依赖的调用点"出发,找到能在不修改该处代码的前提下替换依赖的位置。可注入替身的典型位置包括:被依赖对象的接口或抽象类(可注入 mock/stub)、构造器参数(构造注入)、方法参数(方法注入)、工厂/工厂方法(可替换为返回替身的工厂)、setter 或属性(setter 注入)、以及全局变量间接通过参数传递的位置。识别方法是先画出被测对象与依赖的调用关系,再逐点判断"这里能否替换成替身"。

接缝识别的本质是"找到行为分离点"。现代测试替身的注入依赖接口、注入点(构造器、方法、工厂)这些"缝隙"。若代码没有接口、直接 new 具体类,则没有接缝,需要先引入接口或参数注入制造接缝。识别接缝是"遗留代码可测性改造"的前置步骤。

#
★★★

3. 接缝(seam)的"位置"(location)中编译期、链接期、对象构造期、方法调用期、拦截期

接缝(seam)可以位于哪些"位置"(location)?编译期、链接期、对象构造期、方法调用期、拦截期分别指什么?

  • 接缝位置的五个阶段划分
  • 各位置对应的注入机制
  • 各位置在测试中的适用场景

接缝的位置指"行为被替换时所处的阶段":编译期接缝(如预处理宏、条件编译、源码替换)在编译时决定使用哪份实现;链接期接缝(link seam)在链接时用不同的实现库替换;对象构造期接缝(构造器、工厂、依赖注入)在对象创建时注入替身;方法调用期接缝(方法参数、函数指针、回调)在调用时传入替身;拦截期接缝(AOP、代理、装饰器、mock 框架)在运行时拦截原始调用并替换行为。位置越靠后越灵活,但往往引入更多间接与运行时开销。

接缝位置决定了测试替身注入的时机与机制。编译期/链接期接缝适合系统级、静态链接场景,对象构造期与方法调用期接缝是日常 OO 测试的主流,拦截期接缝(mock 框架、代理)最灵活但需注意是否过度干预。识别代码使用哪种接缝位置,就能选择对应的注入策略。

#
★★★

4. 测试替身(test double)的五类型中 dummy、stub、spy、mock、fake

测试替身(test double)的五个类型是什么?dummy、stub、spy、mock、fake 分别有何区别?

  • 五类测试替身的定义与用途
  • 各类型之间的区别(返回值、行为、验证、状态)
  • 选型依据

测试替身是替代真实依赖的测试对象,五类为:dummy(哑元)只用于传递参数、从不被使用,其方法返回空或抛异常;stub(桩)为被测代码提供预设的返回值,只关心"输入→输出",不验证调用;spy(间谍)记录调用信息以便事后断言,是"有记录的 stub";mock(模拟对象)由测试预先设定期望,验证"被调用是否正确"(行为验证);fake(假对象)是真实依赖的简化但可工作的实现(如内存数据库),用于真实逻辑模拟。核心区别在于:stub 提供数据、spy 记录调用、mock 验证调用、fake 提供可运行的真实逻辑。

选型关键看"测试要验证什么":只需要返回数据用 stub;需要记录是否被调用用 spy;需要断言"以特定方式被调用"用 mock;需要真实但简化的行为用 fake;仅占位用 dummy。过度使用 mock 会让测试脆弱、与实现耦合,应优先用 fake 或真实实现,必要时才用 mock。

#
★★★

5. 遗留代码的"依赖隔离"(dependency breaking)中打破紧耦合的工程手法

什么是遗留代码的依赖隔离(dependency breaking)?有哪些打破紧耦合的工程手法?

  • 依赖隔离的目标:让遗留代码可被独立测试
  • 打破紧耦合的手法:提取接口、参数注入、构造注入、工厂、封装调用
  • 手法与接缝的关系

依赖隔离是指打破遗留代码与外部依赖(数据库、网络、文件、时钟、全局状态)之间的紧耦合,使其能被独立测试。工程手法包括:提取接口(Extension of Interface)——让被测代码依赖接口而非具体类;参数注入——把依赖作为方法参数传入;构造注入——通过构造器传入依赖;引入工厂/工厂方法——用工厂替换直接 new;封装调用——把直接调用外部依赖的代码提取成可覆写的方法(subclass-and-override);以及用属性/setter 注入。这些手法本质是在代码中制造"接缝",使测试可注入替身。

依赖隔离是遗留代码可测性改造的核心。选择手法要考虑改动最小、风险最低:优先用"提取方法+参数注入"这类小改动,逐步引入接口和 DI。依赖隔离不是一次性完成,而是小步推进,每步配合测试确保行为不变。

#
★★★

6. 遗留代码接缝的引入顺序中面对无法快速重构的巨型函数,如何先用「提取函数 + 参数注入」制造最小接缝,再逐步放大可测试面?

面对无法快速重构的巨型函数,如何先通过"提取函数 + 参数注入"制造最小接缝,再逐步放大可测试面?

  • 最小接缝的引入顺序与步骤
  • 提取函数 + 参数注入的机制
  • 放大可测试面的渐进策略

面对巨型函数,一次性重构风险极高,应先用最小改动制造接缝:第一步用 Extract Method 把函数中一段相对独立、依赖外部资源的逻辑提取成独立方法;第二步用参数注入把这个方法的外部依赖(如数据库、时钟)作为参数传入,或用覆写方法(subclass-and-override)注入;这样就有了一个"最小接缝",可针对该提取方法写测试。验证通过后,再逐步把其他片段按同样方式提取、注入、测试,逐步放大可测试面,直到整个巨型函数被拆成多个可测的小方法。

这个策略的核心是"小步、单点、即时验证"。先制造一个最小、低风险的接缝建立测试,再借助测试作为安全网逐步扩展。参数注入比引入完整 DI 更轻量,适合作为第一步。每扩大一步都先确保测试通过,从而把"攻克巨型函数"分解为一系列可验证的小步骤。

#
★★★

7. 遗留系统解耦的切入点中数据访问、全局状态与长方法三条路径如何选择第一个接缝,降低首次改造风险?

遗留系统解耦时,数据访问、全局状态与长方法三条路径,如何选择第一个接缝以降低首次改造风险?

  • 三条解耦切入路径:数据访问、全局状态、长方法
  • 选择第一个接缝的原则:风险低、收益高、独立可测
  • 降低首次改造风险的策略

三条路径各有特点:数据访问(数据库/存储)解耦能让逻辑独立于数据源,收益高但引入替身成本中等;全局状态(静态变量、单例、时钟)解耦能消除测试间的互相污染,但全局状态往往散落各处、改动面大;长方法(Extract Method)切分风险最低、最局部,能立刻建立第一个测试点。选择第一个接缝的原则是"风险最低、改动最小、能独立验证":通常优先选长方法中相对独立的一段做提取+参数注入,因为它不触碰全局数据、改动局部、失败影响面小;待建立测试安全网后,再逐步处理数据访问与全局状态。首次改造应避免一上来就动全局状态这类波及面大的目标。

首次接缝的目标是"用最小风险建立可测试基础",而不是"一次解耦所有依赖"。长方法提取风险最低、见效最快,是稳妥的起点;数据访问可借用 fake/内存替身;全局状态应放在最后处理,因为它牵涉面广、易引发回归。优先级排序:先长方法建立测试,再数据访问,最后攻全局状态。

#
★★

8. intercept seam(AOP、Decorator、Proxy、动态代理)的横切关注点

什么是 intercept seam?AOP、Decorator、Proxy、动态代理等截获式接缝如何处理横切关注点?

  • intercept seam 的定义:在运行时拦截调用并改变行为
  • 横切关注点的概念
  • AOP、Decorator、Proxy、动态代理的机制与区别

intercept seam 指在运行时拦截某个方法调用,在不修改原代码的前提下改变其行为,是对象/new 层面的接缝。横切关注点(cross-cutting concern)指日志、事务、权限、缓存、埋点等散落在多个模块中、横切业务逻辑的关注点。AOP 通过切面(aspect)统一织入横切逻辑;Decorator(装饰器)通过包装对象在调用前后附加行为;Proxy(代理)通过代理对象控制对目标的访问;动态代理(Java 的 JDK 动态代理、CGLIB)在运行时生成代理类拦截方法。它们都能在拦截点注入测试替身或附加行为。

四种机制都通过"拦截"实现横切关注点,但粒度不同:AOP 面向切面、声明式,适合日志/事务等系统级关注点;Decorator 面向对象组合、显式,适合需要精心控制行为的场景;Proxy 面向访问控制;动态代理是上述机制的实现基础。作为接缝,它们允许在拦截时替换真实实现,便于测试注入。

#
★★

9. link seam(动态链接库替换)的工程应用

什么是 link seam?动态链接库替换在工程中如何应用?

  • link seam 的定义:在链接期替换被链接的实现
  • 动态链接库替换的机制
  • 在测试与系统集成中的应用

link seam 指在链接期通过链接不同的实现库来替换依赖,从而在编译后、运行前改变行为。动态链接库替换是 link seam 的典型工程应用:被测代码链接某共享库,测试时通过链接到承载测试替身的替代库(stub 库、fake 库),让符号解析到替身实现。它常用于 C/C++ 等系统级语言的测试,以及替换第三方库、替换系统调用、实现 A/B 或灰度切换。

link seam 的优势是无需改源码即可替换依赖,适合难以注入的全局/系统依赖;代价是链接期技术栈相关、配置复杂、粒度较粗(按库粒度)。在现代 OO 语言中,link seam 已不如 object seam 常用,但在系统级、嵌入式或测试非 OO 代码时仍是有效手段。

#
★★

10. method seam(template method、Strategy)的运行时替换

什么是 method seam?Template Method 与 Strategy 如何实现运行时替换?

  • method seam 的定义:在方法调用层面替换行为
  • Template Method 与 Strategy 模式
  • 两种模式的取舍

method seam 指在方法调用层面提供行为替换点,通过覆写或策略注入改变方法行为。Template Method(模板方法)在父类方法中定义算法骨架,把可变步骤留给子类覆写,测试时通过覆写子类替换某些步骤;Strategy(策略)把算法封装为独立对象,通过组合注入不同策略,测试时注入替身策略。两者都提供运行时替换能力,但 Template Method 用继承、Strategy 用组合。

method seam 是常见的对象接缝,把"可变行为"显式暴露为可替换点。Template Method 适合"算法骨架固定、部分步骤可变"的场景,但继承绑定较强;Strategy 更灵活、更利于测试与复用,符合组合优先原则。选择时优先考虑 Strategy,若骨架清晰且稳定再用 Template Method。

#
★★

11. object seam(依赖注入、工厂、抽象工厂)的 DI 应用

什么是 object seam?依赖注入、工厂、抽象工厂如何应用以支持 DI?

  • object seam 的定义:在运行时替换对象实例
  • 依赖注入(构造器、方法、setter)
  • 工厂与抽象工厂的解耦价值

object seam 指在运行时通过替换对象实例来改变行为,是 OO 测试中最常用的接缝。DI 把依赖从代码内部直接创建改为外部传入(构造器、方法、setter 注入);工厂把创建逻辑封装起来,便于返回替身;抽象工厂用于创建一组相关对象,便于整套替换。它们都让被测对象依赖抽象而非具体实现,测试时注入 mock/fake。

object seam 的核心价值是"面向抽象编程":依赖注入法、工厂、抽象工厂都让被测代码依赖接口而非具体类,从而在测试时替换实例。选择注入方式时,构造器注入最清晰、最利于不可变与可测性,方法注入灵活,setter 注入较宽松;工厂与抽象工厂解决"创建逻辑"的解耦。它们共同构成现代 DI 的基础。

#
★★

12. 特征测试的输入覆盖设计中对行为未明的遗留代码,如何选择边界值与错误路径使特征化测试能在重构后提供可信的行为对比?

对行为未明的遗留代码,如何设计特征测试的输入覆盖(边界值与错误路径),使重构后能提供可信的行为对比?

  • 特征测试输入覆盖的重要性
  • 边界值与错误路径的选择策略
  • 行为对比可信度的保障

特征测试要发挥"行为对比"作用,输入覆盖必须够广够有代表性。设计策略包括:选取边界值(如 0、1、最大值、数组为空、长度边界、数值上下界),覆盖错误路径(异常输入、空值、非法状态、越界),以及正常路径与典型业务场景。覆盖越全面,重构后若行为被意外改变,越容易被测试捕获,可信度越高。对行为未明的代码,可先记录当前对不同输入的实际输出,再据此固化为特征测试,并辅以随机/模糊输入的差分对比。

特征测试的可信度取决于它"捕捉行为变化"的能力,而输入覆盖直接决定这一点。边界与错误路径最能暴露行为差异,是覆盖重点。同时要保证测试输入可复现、断言基于"重构前观察到的实际输出",而非主观设定。这样重构后测试通过即证明行为未变,失败则提示需甄别是否回归。

#
★★

13. 接缝的"成本评估"中引入接缝的复杂度与收益如何权衡,哪些遗留代码不值得为其引入测试替身?

如何评估引入接缝的成本与收益?哪些遗留代码不值得为其引入测试替身?

  • 接缝成本评估:改动复杂度、风险、维护负担
  • 收益评估:测试价值、未来改动频率
  • 不值得引入替身的场景

引入接缝的成本包括:需要改动源码(提取接口、注入参数)、改动本身可能引入回归、增加间接层与维护负担、以及为接缝写测试替身的投入。收益包括:该代码获得可测试性、未来改动更安全、缺陷更易定位。权衡时考虑代码的变更频率——高频变更、支撑核心逻辑的代码值得投入接缝;稳定、低频、一次性、且结构封死难以改造的代码,可能不值得为其引入替身,接受其不可测或直接弃用/重写更划算。

接缝不是免费的,它本质是"为可测性付出的改造成本"。评估要看"投入产出比":接缝改造成本 × 未来改动频率 vs 测试带来的保障。对高风险、高频、核心代码,接缝是必要投资;对边缘、稳定、无未来变化的代码,引入接缝不划算,可记录为债或接受。这与"重构成本边界"一致。

#
★★

14. 遗留代码测试替身的选型边界中数据库、文件系统与时钟等依赖何时用真实实现、何时用替身?

数据库、文件系统与时钟等依赖,在遗留代码测试中何时用真实实现、何时用替身?

  • 不同类型依赖的替身选择
  • 真实实现 vs 替身的取舍
  • 集成测试与单元测试的分工

选择边界取决于"测试想验证什么"与"依赖的可控性/速度"。数据库:追求速度与隔离、验证业务逻辑时用替身(fake 内存库或 mock),需要验证 SQL、事务、约束等真实行为时用真实库(或 Testcontainers 容器库)跑集成测试;文件系统:逻辑测试用临时目录或内存文件系统替身,需要验证真实 IO 行为时用真实临时文件;时钟:几乎总是用替身(注入可控时钟),因为时钟不可控会导致测试不稳定。原则是:验证"与依赖交互的逻辑"用替身,验证"依赖本身的行为"用真实实现,并分别归入单元测试与集成测试。

替身带来速度与隔离,真实实现带来真实行为的保障。稳定可控的依赖(时钟、当前时间)应注入替身;需要真实行为验证的依赖(数据库、文件系统)用真实实现做集成测试。过度替身会让测试与真实环境脱节,过度真实会让测试变慢、不稳定。分层测试(单元用替身、集成用真实)是常见做法。

#

15. preprocessing seam(C 的 #ifdef)的边界

什么是 preprocessing seam?C 的 #ifdef 作为预处理接缝有哪些边界?

  • preprocessing seam 的定义与机制
  • #ifdef 接缝的边界与局限
  • 适用场景与替代方案

preprocessing seam 指在编译前通过预处理指令(如 C 的 #ifdef)替换代码,使测试能编译进不同的实现。#ifdef 通过宏定义控制是否包含某段代码,可在测试构建时定义宏以启用测试替身或桩实现。其边界包括:只能作用于编译期而非运行时,替换粒度受宏控制、较粗,宏定义散落各处易导致"条件编译地狱",难以维护,且会改变编译产物,实际运行与测试运行的代码可能不一致。

preprocessing seam 适合系统级、嵌入式中难以运行时注入的场景,但代价是让代码被条件编译污染、可读性下降、测试与生产行为可能不一致。现代 OO 语言中更倾向用 object seam(DI、接口)替代,因为后者更灵活、更清晰。#ifdef 应尽量收敛、局部化,避免全项目泛滥。

#

16. 遗留代码的测试策略中特征测试与安全网?

遗留代码的测试策略是什么?特征测试(characterization test)如何构成安全网?

  • 遗留代码测试的整体策略
  • 特征测试作为安全网的作用
  • 安全网与重构的配合

遗留代码的测试策略核心是"先建立安全网再动手":对无测试的遗留代码,先用特征测试把当前行为固化下来,形成可回归的基线,再在其保护下进行重构。特征测试记录"现状行为"而非"期望行为",重构后跑同一批测试,行为变化即被捕获,从而防止重构悄悄改变行为。安全网建立后,每步重构都小步进行、测试通过再继续,逐步扩大覆盖。

遗留代码最大风险是"改了不知道对不对",特征测试正是解决这个信息缺失的钥匙。它把不可验证的现状变成可验证的契约。策略顺序是:补特征测试→小步重构→同步或补充有意义的测试→扩大覆盖。安全网让重构从"冒险"变为"可控",是遗留代码演进的前提。

#

17. 接缝设计中依赖注入与接口提取?

接缝设计中的依赖注入与接口提取是什么?两者如何配合制造接缝?

  • 接口提取的步骤与价值
  • 依赖注入的方式与作用
  • 两者配合制造接缝

接口提取(Extract Interface)是把具体类的依赖抽象为接口,让被测代码依赖抽象而非具体实现;依赖注入(DI)是把依赖从代码内部创建改为外部传入,两者配合就制造出可替换的接缝:先提取接口,再通过构造器或方法注入该接口,测试时注入接口的替身实现。这是为遗留代码制造 object seam 的标准做法,改动相对小、易于测试。

接口提取与依赖注入是制造接缝的两步曲:接口提供了"抽象层",注入提供了"替换点"。配合后,被测代码与具体实现解耦,可注入 mock/fake。相比直接 new 具体类,这一组合让测试真正可控。实践中可先提取接口(改一行声明),再小步把 new 改为注入,逐步完成。

#

18. 遗留代码改进的预算化中如何为高价值遗留模块设定持续改进配额,避免不敢动或大爆炸两个极端?

如何为高价值遗留模块设定持续改进配额(budget),避免"不敢动"和"大爆炸"两个极端?

  • 改进配额(budget)的概念与设定
  • 持续改进小步推进的机制
  • 两个极端的风险与规避

改进配额是为高价值遗留模块设定"持续、小步、固定投入"的改进预算,例如规定每个迭代/版本投入一定比例的时间(如 10%-20%)专门用于该模块的测试补充与重构。它把"改进"常态化、预算化,避免两个极端:一是"不敢动"——因为怕风险而长期冻结模块,导致债越积越多;二是"大爆炸"——一次性投入大量时间大改,风险高、易回归。配额模式让小步改进、持续验证、风险可控地推进。

配额化的核心是"制度化地持续改进":既有硬性投入(不因日常任务挤占而消失),又有硬性边界(不无限膨胀)。每次配额内只做小步、可验证的改进(如补几个特征测试、拆一个方法),配合测试与安全网,长期积累下来模块质量稳步提升。这既保护了业务节奏,又避免了技术债失控。