祖传项目一行测试都没有、改一下全靠赌?我用 Cursor 逐步补出最小测试安全网

0 阅读13分钟

上一篇讲怎么用 Cursor 摸清陌生老代码、再安全改下第一刀,好几个卡在第四项:项目一行测试都没有,改完根本不知道对不对。这篇专门拆这个。

没测试的老项目,改代码不是工程,是赌博。你赌自己没碰坏别的地方,赌线上不出事,赌那个七八百行的函数在你动了两行之后还按原来的样子跑。补测试的目的,是在你动刀之前,先在最要命的地方垫一块垫子,改坏了能立刻被拦住,而不是等用户来报。

但补测试有个大坑:很多人装好 Cursor 就来一句"给这个项目补全测试",它一口气生成几百个,覆盖率看着是上去了,仔细一看全在测取值、赋值、拼字符串这种根本不会错的东西,真正算钱、管状态、出事故的核心逻辑一条没盖住,你还被它绕进了一个跑不起来的测试环境。这篇讲我用 Cursor 给老项目补测试的顺序:先测什么、怎么让它写得准、耦合到没法测的代码怎么办。测试框架、命令、菜单各个版本和项目差异挺大,涉及具体怎么跑,以你项目里已有的测试配置和官方当期文档为准。

一、别一上来追覆盖率,先分清哪些代码值得先测

老项目不可能、也没必要一开始就全测。测试是有成本的,要花时间写、要花时间维护,给一堆几乎不会动、也不太会出错的样板代码补一堆测试,是纯粹的浪费。

先测的不是"行数最多的代码",而是"最要命"和"你马上要动"这两块的交集。我心里有个先后:

优先级代码类型为什么先测
1核心业务规则:算钱、金额、状态流转、权限判断错了直接出事故、赔钱或越权,代价最高
2你这次要改、要重构的那块动手前先锁住它现在的行为,防止改着改着偏了
3被很多地方调用的公共函数、工具函数一处错,处处错,杠杆最高
4历史上反复出过 bug、被修过好几轮的地方说明它本身脆弱,最容易回归

纯展示层、框架帮你生成的样板代码、简单的取值赋值,统统往后放。判断标准很简单:这段错了会不会有人找你?不会,就先别管。

覆盖率是结果,不是目标。等你把核心逻辑和即将改动的地方都锁住了,覆盖率自然就上来了;反过来,为了凑数字去测一堆边角料,除了让报表好看,没给你任何安全感。

二、给看不懂但不敢动的逻辑"拍快照":先把现状录下来

面对一坨复杂逻辑,你可能连它"应该怎样"都不确定,这种时候没法写"正确答案"的测试——你都不知道正确答案是什么。

换个思路:不判断对错,先把它现在的行为录下来当基准。这种做法叫特征测试,说人话就是给现状拍快照。不管现在的行为合不合理,先锁住它;等你以后重构、优化,只要保证行为没变,测试就应该一直绿。它把"我不敢动"变成了"我敢动,因为一动就知道有没有变"。

具体这么做:

  1. 找一段你要动的核心逻辑,比如一个算折扣、算运费的函数;
  2. 挑几个真实、有代表性的输入,最好直接从历史订单、线上日志里挑,别自己拍脑袋编;
  3. 手动跑一遍,把实际输出老老实实记下来,哪怕你觉得它算得怪、算得像个 bug;
  4. 把"输入 → 实际输出"写成测试断言。

拿一个算运费的函数举例,测试长这样(以你项目实际的测试框架为准):

# 特征测试:断言里的数字不是猜的,是从现有行为录下来的
def test_shipping_fee_current_behavior():
    # 历史真实订单:偏远地区、超重、加急
    assert calc_shipping(region="remote", weight=12.0, urgent=True) == 38
    # 普通地区、标准重量
    assert calc_shipping(region="normal", weight=1.5, urgent=False) == 8
    # 临界:刚好卡在免运费门槛
    assert calc_shipping(region="normal", weight=0.0, urgent=False) == 0

这些数字不是你期望的,是代码现在实际跑出来的,测试只是负责把它盯住。

有个细节要留心:如果你录下来的行为本身就疑似 bug,比如运费怎么算都觉得不对,先别急着在补测试这一步改。给这条用例单独打个标记,写清楚"疑似问题、待业务确认",然后找懂的人确认;确认是 bug 了再改,改的时候这条测试正好跟着改成正确值,改动记录一目了然。最怕的是你一边"补测试"一边顺手"修 bug",两件事搅在一起,出了问题都说不清。

三、从一次真实改动倒推:给要改的那块补最小回归测试

特征测试是保现状,真正驱动你补测试的,最好是一个具体的改动需求——你本来就要改某个功能,那就顺手在改之前把它锁住。

动手前先问自己一句:改完之后,我怎么证明没改坏? 答不上来,就说明安全网还没铺好。

围绕这一块,补三类测试就够兜住大部分回归,不用多:

  • 正常路径:典型输入,得到预期结果,保证主干没断;
  • 边界情况:空值、0、最大值、列表为空 / 单条 / 多条、临界日期、跨月跨年;
  • 易错分支:历史上踩过坑、或者条件特别绕的那几个判断。

一个功能点,几条关键用例往往就够。测试不是越多越好,是每条都要测在点上。

让 Cursor 补这些测试时,顺序千万别反:先让它读懂这块逻辑、复述清楚在干嘛,再让它列出"应该测哪些 case",你确认这个用例清单靠谱、没有漏掉关键边界,最后才让它动手写测试。它一上来就写,最容易写出一堆看着热闹、实际测了空气的用例。让它列测试点的指令我一般这么写:

【背景】我要改【某功能】,相关文件已 @ 进来。先不要写任何测试代码。

【第一步:理解】用你自己的话讲清楚这块逻辑现在的行为:
输入是什么、有哪些分支、输出是什么、依赖哪些外部东西。

【第二步:列测试点】根据这块逻辑,列出你认为应该覆盖的测试用例,
分三类:正常路径、边界情况、容易出错的分支。每条写清输入和预期。
预期如果是从现有行为来的,标注"需先录现状",不要替我猜正确值。

【第三步等我确认】我确认这份用例清单之后,你再写测试。
写的时候只加测试,不许改任何业务实现;
不许连接真实数据库和第三方接口,依赖一律用桩或替身。

四、让它写得准的关键:喂上下文、隔离依赖、别碰真实环境

AI 写测试最容易翻车的不是语法,是"测了个假的东西"——测试绿着,但它压根没验证到你关心的逻辑。几个关键动作:

把上下文喂全。 待测函数、它依赖的数据结构和类型、项目里已经有的测试写法,都 @ 进去。如果项目本来就有测试,明确要求它模仿现有测试框架和命名风格,别自己引入一套新框架、新依赖。一个老项目里突然冒出第二种测试体系,后人维护起来很痛苦。

把外部依赖隔离掉。 单元测试要快、要稳,不能依赖真实数据库、真实第三方接口、当前时间、随机数。这些东西一进来,测试就变得慢、脆弱、还可能污染数据。

依赖处理方式
数据库用内存库、测试库或桩替掉,测试数据自己造、用完清理
第三方接口/外部服务用替身返回固定结果,别让测试真的发网络请求
当前时间把时间做成可注入、可固定,别让用例依赖"今天是几号"
随机数、随机 ID测试里固定种子,保证每次跑出来一致
文件系统用临时目录,跑完清掉,别读写固定路径
全局状态、缓存每个用例前后重置,避免用例之间互相串味

一条硬标准:测试必须能反复跑、结果完全一致。 如果你有个测试时过时不过、跑两遍结果不一样,优先把它修掉,别让它赖在那。不可信的测试比没有测试更糟——它绿了你不敢信,红了你也不知道是真有问题还是它又犯病,最后大家干脆无视测试,安全网就白搭了。

五、代码耦合太重、根本没法测怎么办:先开一个最小的接缝

老代码经常长这样:一个函数几百行,里面既连数据库、又发网络请求、又读全局配置,最后顺手算个业务结果。纯计算和读写外部环境死死缠在一起,你想单测里面那段逻辑,根本没法把它单独拎出来。

这时候别硬测,也别借机大重构。先做一点点"纯粹为了能测"的小改造,行话叫在代码里开接缝:

  • 把那段纯计算、纯判断的逻辑,从大函数里抽出来,变成一个不依赖任何外部环境的小函数,输入输出清清楚楚;
  • 连库、发请求这些读写外部的部分留在外层,并且让它们可以被替换;
  • 抽出来的纯逻辑,不连数据库不发请求,自然就能直接喂输入、断言输出了。

最关键的原则:这种重构只做"搬运"、不改行为。 抽出来的代码逻辑原封不动,连变量名都尽量别改,只是换个位置。抽完先让前面写好的特征测试跑一遍,全绿、行为没变,再进行下一步。顺序是:先有特征测试锁住现状 → 再抽出纯逻辑 → 给纯逻辑补测试 → 这时候你才敢真正改业务。

比如一个函数又查订单、又算运费、又落库,你就把"算运费"这段不碰数据库的纯计算整段抽出来,单独成一个函数,运费的各种规则就能直接测了;查订单和落库还在外面,不受影响。

管住手,一次只开一个接缝、抽一个函数,改完立刻跑测试,稳了再动下一个。别被"反正都要整理"诱惑着来一次全面重构,老项目没有测试兜底的时候,大重构就是大型事故现场。

六、补测顺序清单 + 常见坑对照表

给老项目补测试,照着这张清单从头走,你就有了一张越织越密的安全网:

  • 圈定了优先要测的范围:核心业务规则、马上要改的、被多处调用的、历史常出 bug 的
  • 给看不懂但不敢动的核心逻辑录了特征测试,用的是真实输入,不是猜的数字
  • 疑似 bug 的用例单独标记,准备找业务确认,没有在补测试时偷偷改
  • 围绕要改的功能,列了正常路径 / 边界 / 易错分支三类测试点
  • 让 Cursor 先复述逻辑、列测试点,你确认后才让它写测试
  • 测试不连真实数据库和第三方接口,时间、随机数、全局状态都能固定
  • 测试能反复跑、结果一致
  • 缠在一起的代码先开接缝、抽纯逻辑,且只搬运不改行为,抽完特征测试仍绿
  • 新测试用的是项目现有框架和风格,没有引入新依赖
  • 把测试接进了项目的测试命令 / 提交前的验证流程,改完会真的跑

容易踩的坑,也对号入座:

你遇到的情况多半是怎么回事怎么办
让 AI 全量补测试,覆盖率虚高没聚焦核心,测了一堆 getter/setter按优先级,只先锁核心和要改的
测试跑得慢、还老挂连了真实数据库/第三方接口用桩或测试库替掉外部依赖
同一个测试时过时不过时间、随机数、共享状态没固定固定时间和种子,用例前后重置
测试把明显的 bug 也断言成"正确"录现状时没核对,直接锁了疑似问题先标记,确认后再改测试
一上来就大重构让代码可测没特征测试兜底,越改越慌先锁现状,再开接缝,只搬运不改行为
AI 写了一套新测试框架没让它模仿现有项目任务里写死沿用现有框架、禁加依赖
只测了正常输入,上线还是崩漏了边界和易错分支强制补空值、临界、历史踩坑分支
测试写了但没人跑没接进验证流程接进测试命令,改完必须先跑

写在最后

测试是给你壮胆用的,不是给覆盖率报表看的。它不追求好看,追求的是你敢动核心逻辑、而且改坏了能立刻被拦住。

对一个没测试的老项目,最小可行的策略不是全覆盖,而是三步走:先用特征测试锁住现状,再给马上要改的核心补几条最小回归,碰到缠死的代码就开一个接缝、抽一点纯逻辑。改一处、测一处、放心一处,几个迭代下来,最要命的地方就都垫上了。你不需要一口气把项目测完,你只需要让下一次改代码不再是赌。

评论区留个数字,你给老项目补测试最卡在哪:1、不知道先测什么、无从下手;2、代码耦合太重,根本没法单独测;3、测试环境和依赖死活跑不起来;4、让 AI 补了一堆,全是没用的假测试。 票数最高的那档,我下一篇往细里拆。

排期里还压着一篇"怎么判断一个接入渠道靠不靠谱"的避坑清单,只讲怎么分辨、不推任何东西;Cursor 常见报错排查合集也在攒。如果你还想看怎么把这套补测接进提交前的自动检查,也评论区说一声。

觉得有用,点个赞、收藏起来,下次对着不敢动的老代码,照着清单先把安全网立起来;顺手关注一下,这个系列我一篇篇往下更。