Skip to content

测试比实现还多之后:怎样让文档、代码和测试一起向前 ​

先看一个教学示例:一个订单列表有 50 条筛选结果,当前页显示 20 条。产品希望点击“导出 Excel”后,把这 50 条都带走。

开发者调整了导出逻辑,旧测试却失败了:它仍然要求导出的数量不能超过一页。有人可能会把实现改回去,让测试重新通过;也有人会直接把测试里的 20 改成 50。前者丢掉了新需求,后者可能只是换了一个硬编码数字。

如果后台测试通过,页面按钮却仍然把当前页的 20 条传给导出函数,用户拿到的文件依旧不完整。

同一次需求变化,需要处理规范里的旧承诺、代码中的实际路径,以及测试中仍然生效的旧判断。 每处该改什么、该保留什么,都要落到具体行为上,不能只在任务末尾留一句“同步文档,补齐测试”。

下面用这个虚构的订单系统贯穿说明。20 条、50 条、业务规则及文件名都是教学设定,不是 ETF Agent 发生过的业务案例;文中的项目统计和历史观察则来自实际材料。

一、测试已经超过实现,问题还会藏在哪里 ​

我们重新统计过 ETF Agent 直接指导当前研发的工作集。现行规范、活动设计、开发和运维指南、实际使用的模型指令计入文档;历史档案、旧交接稿、一次性报告、分享稿和测试夹具中的示例文本放在参考材料中。代码也扣除了已确认的构建副本。

截至 2026 年 10 月 27 日的已提交版本,按非空行统计:

材料非空行占比
研发文档与指令,包含现行规范49,4257.28%
实现代码与工程脚本248,63636.62%
测试与测试辅助代码380,85356.10%

其中,现行规范本身有 303 份文件、33,025 非空行,已经包含在第一项中。测试文本约为实现的 1.53 倍。代码注释和文档示例没有扣除,这些数字描述的是材料规模,不能直接换算成覆盖率、工作量或理想配比。

但它们让一个问题变得具体:当系统已经积累了这么多说明和保护,下一次修改时,开发者怎样知道哪些仍然有效?

测试越多,可能覆盖更多错误,也可能保存更多过期要求。文档越多,可能帮助理解设计,也可能让不同版本的说明同时出现在眼前。代码越完整,可能更容易增加功能,也可能让一个旧假设散落到页面、接口和导出模块里。

因此,已有材料如何演进,应该进入每一次需求调整。只关注新增功能,很容易把旧前提留给下一位维护者。

二、先把“导出全部”讲清楚,后面的工作才有依据 ​

回到订单导出的教学案例。

第一版功能只承诺导出当前页。这可能是一次合理的交付选择:页面已经拿到这些数据,复用它们就能先提供一个简单的导出功能。当时的说明写“导出当前页”,代码使用页面中的数据,测试验证最多 20 条,三者是相符的。

后来,使用者需要把“本月、已完成”的订单交给财务,一共 50 条,分布在三页。逐页导出再手工拼接开始影响使用,于是提出新需求:导出全部筛选结果。

如果任务只写这七个字,仍然留下不少歧义。“全部”是所有订单,还是筛选后的订单?页面显示哪些列,文件就导出哪些列吗?没有结果时怎么办?

在这个例子中,我们先作出明确约定:

点击导出后,文件包含本次筛选条件下、当前用户有权查看的全部订单,不受当前页码和每页条数限制。保留现有导出列及顺序;没有符合条件的数据时,提示“没有可导出的订单”。

这段话同时交代了变化与保护。变化是导出范围跨越分页;仍然保留的是筛选条件、用户可查看的范围和字段要求。

50 条只是本次样本,新规则是“全部符合条件的订单”。 如果把上限从 20 改成 50,下次有 51 条时,问题就会重新出现。

测试可以针对固定样本明确断言有 50 条,业务代码则不能把 50 当成新的导出上限。两处使用同一个数字,承担的含义并不相同。

这里不需要先写一份很长的设计文档。先把这一段写清楚,开发和评审就有了共同依据;如果这段仍有争议,应先解决争议,避免让代码和测试各自猜测“全部”的含义。

三、一次改动,具体要动哪些地方 ​

下面的文件名都是教学示意,用来展示一个普通项目里可执行的分工:

位置要做的事
docs/order-export.md把“导出当前页”更新为明确的新规则,去掉与其冲突的旧说明
src/orders/export-orders.ts根据筛选条件取得完整数据,不能只处理页面已经加载的那一页
页面上的导出按钮传递筛选条件,不能继续只把当前页数据交给导出函数
tests/order-export.test.ts调整旧数量断言,检查完整数据、字段、筛选范围和无数据情况
页面操作测试点击真实按钮,检查最后下载的文件,而不只调用后台函数

这张表的作用,是让“同步”变成可以检查的动作。评审时能够指出:旧说明是否还在,按钮是否仍然使用旧入口,测试是否只更换了一个数字。

它也不是要求每次修改都把这些文件改一遍。如果导出模块早已支持完整数据,这次只是页面仍在传当前页,修复就应落在页面调用和相应测试上。如果新规则已经生效,只有旧测试还要求最多一页,就应引用当前规则修正测试,保留正确实现。

只有产品承诺真的改变,才需要修改相应规范。对已有正确行为做局部修复,可以沿用现行依据。核对过后明确“不需要改”,同样是有效的处理结果。

旧测试不能整组推倒,先分清它保护什么 ​

假设原有测试里有以下要求:

  • 只导出当前页,最多 20 条。
  • 只导出符合筛选条件的订单。
  • 不能导出当前用户无权查看的订单。
  • 列名与列顺序符合约定。

新需求直接替代的是第一条。其余要求仍然有效,甚至更值得注意:导出范围扩大后,如果筛选条件在翻页时丢失,错误可能会带入更多数据。

因此,这次应替换“只导出当前页”的断言,保留筛选、查看范围和字段保护,再补充跨页完整性的检查。删除旧数量限制,并不意味着可以一并删除整份测试。

Google 的 Andrew Trenk 在《Test Behavior, Not Implementation》中讨论了围绕外部行为组织测试的原则。这里还需要继续判断:测试所约束的行为,是否已经被本次获准的需求变化替代。

测试应该故意给实现一个出错的机会 ​

如果新用例只准备 10 条订单,新旧实现都可能导出成功,测试看不出这次功能究竟有没有变化。

更有辨别力的样本是:准备 50 条符合条件的订单,分成 20、20、10 三页;再放入不符合状态或时间筛选的订单,以及当前用户不能查看的订单。

然后检查:

场景应观察到的结果
当前页只有 20 条,符合条件的共有 50 条文件包含完整的 50 条
切换当前页或改变每页条数符合条件的数据范围不随之改变
数据中混有不符合筛选条件的订单文件不包含这些订单
数据中混有当前用户无权查看的订单文件不包含这些订单
没有符合条件的数据用户收到明确提示

只检查“文件有 50 行”还不够。重复前两页、漏掉最后一页,也可能凑出相同数量。应比较文件中的订单号,确认预先准备的那批订单都在,没有遗漏、重复或混入其他订单。

这些预期来自测试事先准备的业务样本。若直接调用被测导出函数内部的数据整理方法来生成预期,再与导出结果比较,两边可能一起遗漏第三页,测试仍然通过。

这就是测试需要提供的独立约束:它应该有机会识别实现的错误,而不只是跟着实现重复一次计算。

四、后台测试通过之后,还要走到用户拿文件的位置 ​

即使上面的模块测试通过,也不能立即宣布导出功能完成。

在这个模拟系统里,可以设想一种很常见的遗漏:后台已经能够导出全部筛选结果,页面按钮却仍然调用旧方法,把当前页的 20 条数据传进去。直接调用新导出函数的测试会通过,用户点击按钮拿到的仍然只有一页。

此时需要验证的路径很具体:

  1. 打开订单页面,设置“本月、已完成”的筛选条件。
  2. 确认页面有多页结果,当前只显示一页。
  3. 点击页面上的导出按钮。
  4. 读取实际下载文件,比较完整订单号集合与导出字段。

验证到这个位置,才能回答用户到底取得了什么。按钮出现成功提示、接口返回成功、文件确实存在,分别说明路径上的某个环节完成了,不能单独说明文件内容符合需求。

也不必因此要求所有测试都从浏览器开始。数据筛选、跨页汇总等组合可以在模块测试中快速验证;页面测试重点检查按钮、参数传递和最终文件是否接通。各层测试分担不同问题,避免在每个地方重复同一套庞大场景。

这个例子还说明,发现失败之后要先定位实际差异。如果文件只有一页,就检查页面传了什么、后台取了哪些页、文件最终写入了哪些数据;不要未经判断就把所有超时调大、重复请求,或者让模型再试几次。

一次修复应当改变造成失败的那个条件。 原问题是按钮继续调用旧入口,修复应让用户操作进入正确路径,并留下能够发现同类遗漏的检查。

五、三者怎样一轮一轮推动彼此 ​

把导出功能的变化连起来,就能看到文档、实现和测试之间的反馈关系。

第一轮,规范写清“导出当前页”,实现完成它,测试检查最多一页。它们满足当时的需求。

第二轮,实际使用提出“导出全部筛选结果”。这项新需要推动规范变化,也使旧测试中的数量限制失效。开发者需要更新取数范围与页面调用,测试增加跨页样本,同时保留筛选和查看范围的保护。

第三轮,模块测试表明新导出方法能处理完整数据,页面操作却暴露旧按钮没有接上。这个新观察推动调用路径修复,并补上最终文件的检查。

假设后续又有人提出“只导出勾选的订单”,也不能简单把“导出全部”的断言改掉。需要先决定它是新增一个可选操作,还是替代原功能,再明确两种入口分别应该导出哪一批数据。产品选择先清楚,代码和测试才有更新依据。

这就是本文所说的螺旋更新:每一轮由实际需要或反例改变一个判断,再把变化落到相关材料与执行路径。它允许新经验修正规范,也允许测试发现实现没有接上,最后由用户实际取得的结果检验这次修改。

三者还可能共同保存一个错误假设。例如规范笼统地说“导出成功”,实现把“文件生成了”当成成功,测试也只检查文件存在。它们内部一致,仍然没有回答“文件是否包含用户需要的那批订单”。

因此,保持一致之外,还要保持提出不同问题的能力:规范追问用户要什么,测试用具体样本检验承诺,实现负责真正产生结果。三者相互纠正,才能把一份看似完成的功能推进到可用。

六、同步工作为什么也会拖后腿 ​

解决了功能问题,如果维护方式没有调整,下一次修改仍然可能很费劲。

继续用这个模拟项目说明。导出列名分别写在页面配置、后台导出代码和几份说明里。新增一列时,每处都要手工修改;只要遗漏一处,就可能出现页面和文件不一致。

对于这种必须一致的结构,可以维护一份生产字段定义,让页面和导出程序读取它。确实需要多份文件时,用构建生成副本并检查结果。人只修改约定的来源,避免几份副本各自演进。

但测试仍需要保留独立判断。如果测试直接把同一份字段定义拿来当预期,那份定义漏掉了产品要求的一列,页面、导出和测试就可能一起漏掉。合适的做法是共享生产定义,同时在关键业务样本中独立检查约定的列名、顺序和内容。

有些重复则应该改成引用。例如某次导出验证的实际结果,不必在设计、任务、总结和历史里抄写四遍。结果留在负责记录观察的位置,任务表说明是否完成,其他页面需要时链接过去。

Tom Manshreck 在《Software Engineering at Google》的文档章节中讨论过把文档纳入工程工作流。具体到日常维护,除了要求更新,还需要减少人工同步点,并明确哪些内容可以生成、哪些内容需要独立核对。

历史材料也有自己的位置。值得留下的是“为什么曾经只导出当前页”“什么使用需求推动了变化”“为什么只检查行数仍会漏错”。这些判断能够帮助后来者理解取舍。每次重复记录“导出测试通过”,会增加材料,却未必增加同样的帮助。

旧检查还需要退出条件。如果原项目有一套检查,只用于确认旧导出库已经移除,那么迁移完成后,应核对旧调用是否消失、当前导出行为是否已有保护,再决定这套检查是否仍需进入日常回归。测试慢或失败,不能单独成为删除理由;已经失效的要求也不应永久占据回归集合。

七、明天接到需求,可以怎样开始 ​

把“导出当前页”改成“导出全部筛选结果”,可以从下面这段任务描述开始。它仍然属于教学示例:

调整订单导出范围,使其覆盖当前筛选条件下、当前用户可查看的全部订单,不受页码影响。先更新原说明中的导出范围,保留筛选、查看范围和字段要求。核对页面按钮与后台导出方法,找出仍只处理当前页的位置。用跨三页的数据替换旧数量断言,检查完整订单号集合、无遗漏和重复,并保留不符合条件及无查看权限的排除用例。最后从页面点击按钮,读取实际下载文件验证结果。已有正确实现保留,只修改实际失步的部分。

这段话明确了目标、需要保留的保护、可能遗漏的位置、测试样本和完成条件。开发者可以从中定位工作,评审者也能据此判断结果。

实际开始时,不必先创建一套新表格。就在原任务中写清三件事:本次行为从什么变成什么;哪些旧要求继续有效、哪些被替代;用什么输入、从哪里观察结果。涉及新的接口、权限或数据约定,再按项目已有流程完成相应设计和验证。

完成时,也要给出能复查的结论。这个例子应能说明:跨页数据已经完整导出,筛选与查看范围没有扩大,页面按钮进入正确路径,下载内容与约定一致。某一层没有验证,就保留那一层的缺口。代码和文档文件都改过,不足以单独支持“完成”。

ETF Agent 的真实材料中,同样出现过配置已经变化、测试仍保存旧金额,代码切换之后 19 份当前文档需要追赶,以及测试没有经过用户实际使用的运行路径等失步。订单教学案例把这些问题转成普通研发熟悉的场景,便于识别相同的失步。

回到最初那个导出按钮,下一次遇到失败测试,我们需要判断旧数量限制是否仍然有效;看到成功提示,还需要确认文件里到底有哪些订单;要求同步文档时,应指出哪一句旧说明已经不再成立。

让三者一起向前,最终要表现为:用户需要的数据真正导出了,仍有效的保护没有丢失,过期要求也不再迫使实现退回去。 这种能够落实到输入、文件和结果的判断,才会让积累起来的文档、代码和测试继续帮助研发。

延伸阅读与交流 ​

欢迎在 GitHub 文集交流;如果这些工程案例对你有帮助,也欢迎点一个 Star。

与个人博客并行:这里按问题连续阅读,博客保留时间线。 · 联系邮箱:hpoenixf@foxmail.com · 微信:hpoenixf0510