🛠️ 第10章 场景二:修复 bug、补测试、做回归
📄 一句话概括

三段三种标准:复现看「有没有一条红」,修复看「那条绿没绿」,回归看「全量还是不是绿」。 合成一步就会分不清「没修好」和「修好了但弄坏了别的」。 回归按依赖图选,条数能降一个量级;但时间降不下来——启动开销占了大头。

📝 本章和前面几章的关系

前三篇分别讲了:怎么说清 bug、怎么锁住范围、怎么让测试真管用。 这一篇把三件事串成一条链。 为什么「闭环」值得单独讲? 因为三段各自都对,串起来的顺序错了照样出问题。 最常见的是把修复和回归合成一步:改完直接跑全量。 绿了皆大欢喜,红了就分不清是「没修好」还是「修好但弄坏了别的」。 Demo 真跑一遍三段:先让它红、再让它绿、再跑全量, 最后对比「全量」和「按依赖图选」的条数与耗时。

一、三段各自的通过标准

段 通过标准 没过说明什么
复现 有一条测试红 还没找到真问题
修复 那条测试绿 改对了,但可能弄坏了别的
回归 全量还是绿 确认没牵连

三段的判据不一样,所以要分开看。 第一段的标准是「红」——这听起来反直觉,但确实如此。 一条能红的测试,才证明你真的找到了那个问题。 一开始就绿的测试,只证明「这个场景本来就没问题」。 第二段只看那一条,不看别的。 这一段的窄,是有意的:它只回答「这个 bug 修好了吗」。 如果这一段就跑去跑全量,红了你分不清是哪一种红。 第三段才是全量。 它回答的是另一个问题:「修这个的过程中,有没有碰坏别的」。 两个问题必须分开问,因为它们的修法完全不同——前者是继续找问题,后者是回退这次改动。

二、回归要跑哪些

全量最保险,但跑得慢。 慢的代价不是等待,是「你不会每次都跑」——而一个不是每次都跑的检查,等于没有。

策略 漏掉什么
全量跑 什么都不漏,但慢
只跑改过的文件 依赖它的模块没人看
按依赖图选 只要图是全的,什么都不漏

第二行是常见的错,而且错得很自然: 我改了 svc/handler.py,那就跑 test_handler。 但如果 svc/routes.py 也 import 它,那部分就没人测了。 第三行的前提是「图是全的」。 不全的依赖图比没有图更危险——它会给你一个「我选过了」的错觉,而你其实漏了一片。 Demo 里两个数要一起看:条数从三十五降到二,差十八倍; 墙钟从八百多毫秒降到七百多,只差一点二倍。 只报时间会得出「选不选都一样」的错误结论。 为什么时间降不下来? 因为启动解释器就要四百多毫秒,而三十几条空测试几乎不花时间。 真实项目里单条测试重起来,条数差才会变成时间差。 所以「按图选」现在的价值不是省时间,是「能跑得更频繁」—— 每次提交都跑得起,这件事本身就改变了工作方式。

三、闭环的标志

看起来闭环了 真闭环
本机跑通了 干净环境跑通
这一条过了 全量还是绿
手动点了一遍 有命令能重复

左边三样都是「这一次对了」,右边三样是「下次还会对」。 这个区别在一个人干活时看不出来,在两个人接手时立刻显形。 判据可以写得很硬:换个人、换台机器、一个月之后再跑,结论一样。 三样里任何一样变了,闭环就没闭上。 「一个月之后」这一条最容易被忽略,也最能暴露问题。 依赖没锁版本、环境变量没写进文档、测试依赖了某个本地文件——这些毛病当天都不显形。 所以这一章四篇连起来是一条线: 说清问题、锁住范围、让测试真管用、把三段分开跑。 四步里没有一步是「写代码」,但没有一步可以跳过。

四、本章小结

问题 答案在哪一节 一句话
怎么把吐槽变成任务 10.1 五个字段,四条能执行的步骤
怎么防顺手改 10.2 范围写进任务,脚本量 diff
怎么让测试管用 10.3 断言带数字,手动做变异
怎么确认修好了 10.4 三段分开,标准各不相同

四节的共同点是「把模糊的东西变成可判定的」。 bug 报告变成四条命令,范围变成一个集合,测试变成能抓住变异的断言,闭环变成三种通过标准。 可判定,才谈得上交给 Agent 去做。

五、动手 Demo:验证闭环:复现、修复、回归

一个真项目、三十五条测试、一个空名字导致的 500。 走一遍三段:先让它红、再让它绿、再跑全量。 最后对比「全量 discover」和「按依赖图选中两条」的条数与耗时。 两次都是真跑 unittest,时间是 perf_counter 量的。

# ① 复现:先让它红
c, o = run(root, [PY, "-m", "unittest", "tests.test_handler", "-v"])
assert c != 0, "没红,说明没找到真问题"

# ② 修复:只让它绿
put(root, "svc/handler.py", fixed)
c, o = run(root, [PY, "-m", "unittest", "tests.test_handler"])
assert c == 0

# ③ 回归:全量还是绿
c, o = run(root, [PY, "-m", "unittest", "discover", "-s", "tests", "-t", "."])
print("回归", "绿" if c == 0 else "红")

实跑输出:

--------------------------------------------------------------------
验证闭环:复现、修复、回归
--------------------------------------------------------------------


  ① 三段,三种通过标准
  --------------------------------------------------------------------
| 段 | 通过标准 | 没过说明什么 |
|---|---|---|
| 复现 | 有一条测试红 | 还没找到真问题 |
| 修复 | 那条测试绿 | 改对了,但可能改坏了别的 |
| 回归 | 全量还是绿 | 确认没牵连 |

  三段的判据不一样,所以要分开看。
  最常见的错是把二三段合成一步——改完直接看全量,结果分不清「没修好」和「修好了但弄坏了别的」。

  ② 复现:先让它红
  --------------------------------------------------------------------
  跑 tests/test_handler:退出码 1,最后一行 FAILED (errors=1)
  两条里挂一条,挂的是 test_blank_returns_400——它抛的是 KeyError,不是返回 400。
  这就是「复现成功」的样子。

  ③ 修复:只让它绿
  --------------------------------------------------------------------
  再跑同一个文件:退出码 0,OK
  绿了。但这一步只证明「这一条过了」,不证明「别的没坏」。

  ④ 回归:跑哪些
  --------------------------------------------------------------------
| 策略 | 条数 | 漏掉什么 |
|---|---|---|
| 全量跑 | 35 | 什么都不漏,但慢 |
| 只跑改过的文件 | 2 | 依赖它的模块没人看 |
| 按依赖图选 | 2 | 只要图是全的,什么都不漏 |

  三种策略里,第二种是常见错:改 svc/handler.py 就只跑 test_handler。
  但如果 svc/routes.py 也 import 它,那部分就没人测了。
| 跑法 | 条数 | 墙钟 | 结果 |
|---|---:|---:|---|
| 全量 discover | 35 | 821 毫秒 | OK |
| 按图选中 2 条 | 2 | 713 毫秒 | OK |

  时间只差不到一倍——因为启动解释器就要四百多毫秒,三十几条空测试根本不算时间。
  ! 所以「按图选」省的不是时间,是「能跑得更频繁」。
  单条测试重起来的项目里,三十五条和两条的差别才是几秒对几十毫秒。
  这里两个数都要照实说:条数差 18 倍,时间只差 1.2 倍。只看时间会得出「选不选都一样」的错误结论。

  ⑤ 闭环的标志
  --------------------------------------------------------------------
| 看起来闭环了 | 真闭环 |
|---|---|
| 本机跑通了 | 干净环境跑通 |
| 这一条过了 | 全量还是绿 |
| 手动点了一遍 | 有命令能重复 |

  左边三样都是「这一次对了」,右边三样是「下次还会对」。
  ! 闭环的判据是「换个人、换台机器、一个月后再跑,结论一样」,不是「我这儿好了」。

三段跑完,红、绿、绿。 这三个字看着平淡,但它们分别证明了不同的东西: 第一条证明问题真的存在,第二条证明改对了,第三条证明没牵连。 如果合成一步——改完直接跑全量——你会得到一个「绿」,但那个绿同时含着两种可能:本来就没事, 或者修好了。 分不出来,就等于没验证。 最后那张对比表,两个数要一起读:条数差十八倍,时间差一点二倍。 只报时间的话,结论会变成「选不选都一样」,这是错的——因为启动开销四百多毫秒把差别盖住了。 这件事跟 9.1 那个「分区读省上下文不省时间」是同一个道理: 量出来的数要看清它被什么盖住了,否则会得出反的结论。

30 秒实验

  1. 下次修 bug,强制自己分三次跑:先跑出红、再跑出绿、再跑全量。 中间不许跳过第一段。
  2. 数一下你的全量测试跑一次要多久。 超过一分钟的,就值得算一算「按依赖图选」能省到多少条。
  3. 试着在干净目录里跑一次你的测试。 跑不起来,说明闭环还差一步。

常见误区

  1. 「改完直接跑全量,一次搞定。」 红了分不清「没修好」和「弄坏了别的」。 三段分开,就是为了让每个红都有确切的含义。
  2. 「按依赖图选测试能省很多时间。」 小项目上省不了,启动开销占大头。 它省的是「能跑得更频繁」,不是「每次跑得更快」。
  3. 「我这儿跑通了就是闭环。」 换台机器、一个月后再跑,结论一样才算。 「我这儿」不是判据。
自测三问(点开对答案) 1. 为什么复现段的通过标准是「红」? —— **因为一条能红的测试,才证明你真的找到了那个问题。** **一开始就绿的,只说明这个场景本来就没问题。** 2. 修复段为什么只看那一条测试? —— **因为它只回答「这个 bug 修好了吗」。** **跑去跑全量,红了就分不清是哪一种红。** 3. 「按图选」为什么没省下时间? —— **因为启动解释器四百多毫秒,三十几条空测试几乎不花时间。** **它省的是「能跑得更频繁」。**

第 10 章到这里收住。下一章进场景三:跨前后端功能开发。 从一个模糊需求怎么拆成可验证的小项开始。

相关笔记