前阵子有个做独立产品的朋友突然找我,说技术栈终于定完了,CI 也搭了,但有个问题卡了他很久:测试到底写不写?他一个人维护三个项目,白天写业务、晚上被用户追着改 bug,怎么看都觉得写测试是在浪费时间。这个问题我太有共鸣了——离开团队单干的第一年,我一行测试都没写过,当时觉得“代码是我自己写的,出了问题我还能不知道?”直到某天凌晨三点被自己的旧代码坑到怀疑人生,才彻底想明白一件事:对独立开发者来说,测试的本质不是质量保障,而是风险控制,是在你改代码的时候保护自己的安全网。
这篇文章想聊聊“技术栈选型之后”这个系列里逃不开的话题:独立开发者需不需要写测试?需要的话,写到什么程度才算合适?我会直接给结论:要写,但不能按大厂的标准写,更不能为了覆盖率而写。你需要的是一套尽量轻、尽量省时间、只保护关键命脉的测试策略。我会把“哪些代码值得测、怎么判断写到什么程度、实际用什么工具、哪些情况果断别写”一次性说清楚,你也可以直接把这套思路搬到你自己的项目里。
1. 为什么你会纠结要不要写测试:被大厂经验绑架的选择题
很多独立开发者(尤其是大厂出来单干的)对测试的态度非常两极分化:要么觉得“没有测试我不敢改代码”,要么觉得“我就是一个人,写什么测试”。这两种想法其实都是被同一种经验绑架了——习惯了团队协作里的测试规范,却忘了测试在大厂里的真实作用,和独立项目里是完全不同的两件事。
1.1 大厂测试体系真正解决的问题:协作成本,而不是质量
我先说个可能反直觉的判断:大厂里那套测试规范,表面上是质量保障,本质上是降低大规模协作沟通成本的工具。你不认识隔壁组的同事,不知道他为什么这么改,也不敢问,你唯一能信任的就是自动化测试。代码交给你之前,跑一遍全量回归,绿了就敢合,红了就找当事人。覆盖率卡在 80%,不是为了好看,是为了保证“任何人接手任何模块,底线测试是存在的”。
但这个前提在独立开发里完全不存在。整个项目都是你的,你心里清楚每一行代码为什么要这么写,改坏了第二天自己就能发现,没有跨部门扯皮,没有交接风险。这时候你再拿大厂那套标准来要求自己,就会发现写测试的时间成本变得特别扎眼——你不是在投资质量,你是在为自己的心理安全感买单。
我第一年单干时就是典型反面教材。从大厂带出来的习惯让我把早期项目的核心模块测了个遍,覆盖率刷到 85% 以上,结果呢?产品方向三天两头调整,很多模块写完就被弃用,测试也跟着作废。三个月后我意识到一件事:在需求还在快速漂移的阶段,给一个可能明天就被删掉的功能写测试,等于给一个还没出生的孩子买人寿保险。
1.2 独立开发者的真实处境:没有 QA、没有 review、用户比测试环境狠
一个人的项目里,质量防线就三条:你的脑子、线上监控、用户的骂声。第一条会疲劳,第二条需要时间去建设,第三条最真实但也最贵——用户帮你发现 bug 的时间,往往是你产品口碑流失的时间。
我那个朋友后来给我讲了个细节:他的应用上线半年,最怕的不是功能缺失,而是“改一个老功能的时候把另一个老功能弄坏了”。用户不会关心你重构得多优雅,他们只会在群里问“为什么我的会员过期时间不对了”。这类问题一旦发生,你要在恐慌中回忆三个月前自己写了什么,那种心理压力比写测试大十倍。
所以我对“独立开发者要不要写测试”这个问题的回答,从来不是“要”,而是:你要不要在未来半年、一年里反复修改这个项目的代码?只要答案是“要”,测试就不是可选项,而是你给自己买的保险。区别只在于:买多少保额、保哪些风险、保费怎么控制到最低。这几件事,我在后面几章里给出特别具体的拆法。
1.3 一个真实事故:时间格式化改动引发的连环翻车
讲一个我自己踩过的、典型到可以写进教材的坑,帮你直观理解没有测试时“改代码”有多危险。
2022 年底我做了一个会员体系,用户会绑定到期时间,后台看板要按周汇总过期用户。当时时间存储用的是一套旧逻辑:前端传时间戳,后端转成日期字符串存进数据库。过了一段时间,我决定把时间统一改成 ISO 8601 格式,理由是为了兼容几个新接口。当时我特别自信,把后端所有date('Y-m-d')的格式刷成了toISOString(),跑了一遍手工冒烟,看起来没问题,就上线了。
上线的第二天,运营反馈:好几个用户的会员到期时间比实际晚了一天。排查过程是这样的:
- 第一步,拉日志。发现到期时间在数据库里确实是对的,但接口返回给前端的时间比库里多了一天。
- 第二步,翻代码。接口层有个
strtotime()的老调用,它把 ISO 字符串按本地时区解析,然后格式化时又按 UTC 输出,一天就凭空多出来了。 - 第三步,找根因。这个
strtotime()是我三个月前写的一个工具函数里的遗留逻辑,当时只测试了它接收时间戳的情况,根本没测过 ISO 字符串。
整个排查花了四个小时,其中三个半小时都花在“回想自己当时为什么这么写”上。如果当时我在那个工具函数旁边留了一条测试,断言“传入 ISO 字符串时输出正确”,这个 bug 会在上线的瞬间被逮住,而不是被用户发现后由我在凌晨两点跪着修。
那次之后我彻底明白了一个道理:独立开发者写测试,不是为了证明代码正确,是为了在下一次修改代码时,能对旧逻辑放心得足够快。测试其实是写给未来那个健忘的自己看的。
2. 按风险定价:只有这两类代码,独立开发者必须写测试
测试不是越多越好,这句话我说得毫不犹豫。真正要回答的问题是:到底哪些代码值得写测试?我的判断方法很简单——不要按代码量或者函数个数来分配测试精力,而是按“风险”定价。一个模块出错后造成的损失越大,越值得写测试;损失几乎为零的,直接跳过。
2.1 先搞清楚哪些代码通常不需要测试
很多独立开发者一提到“写测试”,第一反应就是给所有函数都配上单元测试。这其实是最大的时间黑洞。先列几个我的个人判断,你大概率能直接对号入座:
- 一次性脚本:比如数据导出、批量改字段、临时清理脏数据。这种脚本跑一次就扔,出错顶多重跑一遍,没必要写测试。但要注意:如果脚本操作的是线上真实数据,你可以写一个“预检查”而不是“测试”,先把影响范围列出来人工过一眼。
- 纯展示组件:前端里那些只负责渲染、没有状态逻辑的组件。它们最多输出长什么样,不会破坏数据,错了用户也看得出来。这类用 Storybook 之类的工具做视觉检查就够了,别写单元测试。
- UI 像素级调整:CSS 间距、颜色、圆角,测试写了就是给自己添堵。改一次样式,测试就得跟着改一次,纯粹是负资产。
- 探索性实验代码:还没想清楚方案,先写出来跑跑看。这时候写测试等于给草稿排版,纯属浪费时间。
判断标准就一条:如果这段代码出错,用户能立刻发现并且损失极小,那它就不值得写自动化测试。因为这类错误即使发生了,你修复的成本也很低,而写测试的维护成本反而是持续累积的。
2.2 第一类必须测的代码:核心业务状态与不可逆操作
第一类必须测的代码,特征是出错后会造成不可逆损失,或者逻辑涉及复杂的状态流转。最常见的三个场景:涉及金额的计算、涉及时间的换算、涉及权限的判断。这些逻辑通常不会频繁改动,但一旦改错,影响的是所有存量用户的数据。
举一个特别典型的例子:分销佣金结算。假设你的平台有用户、订单、佣金三个实体,佣金按订单金额的 10% 计算,用户退款时要扣回已发放的佣金。看起来很简单对吧?但只要你加了“部分退款”“重复退款”“退款时订单已完成结算”这些分支,状态组合就会爆炸。
我当时用纯函数的方式把佣金计算抽出来,写了这样几条测试(伪代码逻辑):
test('正常订单,佣金=订单金额*10%', () => { const commission = calcCommission({ amount: 100, status: 'paid' }); expect(commission).toBe(10); }); test('部分退款,佣金按实际收款金额重新计算', () => { const commission = calcCommission({ amount: 100, refunded: 40, status: 'refunded' }); expect(commission).toBe(60 * 0.1); }); test('重复退款请求,不二次扣减佣金', () => { const commission = calcCommission({ amount: 100, refunded: 100, status: 'refunded' }); expect(commission).toBe(0); });这种测试的价值不在于“覆盖率”,而在于把你的业务规则变成可执行的可信文档。下次任何人(包括三个月后的你自己)想改佣金逻辑,先跑一遍这些测试,就能知道原来的规则是什么、会不会把边界情况弄坏。
核心业务的测试优先级,我再给一个排序参考:
| 风险场景 | 例子 | 是否值得写测试 |
|---|---|---|
| 金额计算 | 佣金、分成、支付金额换算 | 必须写,且要覆盖边界 |
| 时间处理 | 到期时间、活动开始/结束、时区转换 | 必须写,专治“差一天” |
| 权限控制 | 角色判断、接口鉴权 | 必须写,一个漏洞吃大亏 |
| 状态流转 | 订单状态机、任务状态 | 必须写,分支组合越多越需要 |
| 纯展示 | 列表渲染、样式 | 可以完全不写 |
| 一次性迁移 | 数据清洗、字段重命名 | 不写,但必须有人工预检 |
2.3 第二类必须测的代码:对外接口与数据迁移
第二类必须写测试的,是对外接口和数据迁移脚本。原因很直接:用户的存量数据一旦被写坏,是救不回来的,而接口一变更,所有调用方都会立刻、同时爆炸。
先说话外接口。所有被其他模块调用、或者被第三方服务调用的入口,都应该有至少一条“成功路径 + 一条失败路径”的测试。失败路径往往比成功路径更重要——比如参数校验不通过时,接口要返回 4xx,而不是直接抛 500。写这种测试不是为了测正确性,是为了防止以后有人给接口加参数时,顺手把校验逻辑弄坏了。
再说数据迁移。这可能是独立开发里最容易被忽视的高风险区。我见过太多同行“迁移脚本跑完了,然后呢?”——没有任何验证,直接上生产。正确的做法是给迁移脚本配一套预检查 SQL:迁移前统计原数据、迁移后统计新数据,做抽样对比和全量计数对比。如果迁移逻辑是纯 SQL,可以这样组织:
-- 迁移前:统计原表数据量 SELECT COUNT(*) FROM legacy_users; -- 迁移后:统计新表数据量 + 校验关键字段完整率 SELECT COUNT(*) AS total, SUM(CASE WHEN email IS NOT NULL THEN 1 ELSE 0 END) AS valid_email_cnt FROM new_users;不要小看这种最朴素的“对账”。很多迁移脚本在开发环境跑得好好的,到生产环境因为脏数据炸了。一条简单的字段完整率检测,就能在几分钟内拦住大部分问题。
2.4 用“如果这里错了,用户会遭遇什么”来反向推导
说完了“哪些要测、哪些不测”,你可能会觉得还是抽象。没关系,我有一个特别好用的反向推导方法:站在用户角度,问自己一句话——“如果这个模块出错了,用户会损失什么?我的修复成本又是什么?”
把每个模块过一遍这个疑问,你会得到三种答案:
- 用户会损失钱、数据、隐私,或者修复时我只能靠猜。必须写测试,而且要写多层。
- 用户会发现功能异常,但数据还在,我能快速定位。写几条关键路径的测试就够了。
- 用户根本察觉不到,或者发现了也无所谓。直接跳过,把时间花在别的地方。
这个方法特别适合独立开发者随手用,原因在于它会强迫你从“写代码的人”切换到“使用产品的人”的视角。你很快会发现,项目里值得你认真写测试的模块,其实一只手就数得过来。剩下的精力,应该留给功能迭代和用户反馈。
3. 写到什么程度算够:用“四问判断法”替代覆盖率焦虑
确定完“哪些代码要测”,下一个问题就是“写多少算够”。这可能是独立开发者最焦虑的点:测少了怕漏,测多了怕累,看覆盖率数字又不放心。说实话,覆盖率这个指标本身就是陷阱,它只会让你在不该花时间的地方消耗精力。
3.1 为什么覆盖率是陷阱:100% 覆盖也会漏掉最关键的 bug
覆盖率只能证明“代码被执行过”,不能证明“代码在正确的输入下输出了正确的结果”。你完全可以在一个函数里写二十条毫无意义的断言来刷覆盖率,但唯独漏掉那个最危险的状态组合。
举个例子。你有一个支付回调接口,测试覆盖了“支付成功时更新订单状态”“签名校验失败时返回错误”“金额不一致时告警”。三条加起来,覆盖率可能到了 80%。结果真正上线时,用户拿了两张优惠券叠加支付,回调里出现了一个你从没测过的“部分退款后再支付剩余金额”的分支,直接导致订单状态卡死。覆盖率 80% 又如何?你没有测那个真正会出事的输入组合。
覆盖率只适合当“心理安慰剂”,不适合当质量标准。我更希望你用它来看“有没有完全没被测过的文件”,而不是追求数字。比如,如果你有一个核心计算文件覆盖率才 20%,那确实有问题,需要补。但如果整体覆盖率 70%,而剩下没有测的大多是纯展示组件,那这就非常合理。
3.2 四问判断法:怎么判断每一个模块该写到什么程度
我自己用的是一套非常朴素的四问判断法,每次开发新模块时都会对照着过一遍。每回答一个“是”,测试级别就往上升一级:
第一问:这个模块出错,影响面有多大?影响所有用户、影响核心主流程、还是只影响一个冷门功能?“所有用户”那必须重点测,“冷门功能”可以先放。
第二问:这个逻辑里,状态分支多不多?如果只有正常路径和异常路径两条,写两条测试就够了。如果有退款、部分退款、重复退款、超额退款……每多一个分支,测试的重要性就上升一档。
第三问:出错之后,我多久能发现,发现后多久能修好?如果错误会导致数据写坏,几小时后才被运营发现,那测试的价值就极高。如果错误顶多让页面显示不对,用户刷新就恢复,直接跳过也没事。
第四问:修这个 bug 的时候,我会不会动到其他功能?如果答案是不会,那测试优先级可以低一些。如果会,比如你要改的是一个被五个模块引用的工具函数,那必须先有测试垫底。
这个四问法还有个好处:它逼着你在动手写代码之前,先想清楚“这个功能最重要、最容易弄坏的是什么”。想清楚了再写,你会发现自己的代码设计都跟着变好了,因为你会自然地把“容易出错的核心逻辑”抽成纯函数,方便测试的同时也方便复用。
3.3 实际走一遍:给一个会员模块做测试级别判断
空谈方法不好懂,拿我在 1.3 节里提到的会员模块来实操一遍。
第一问影响面:会员到期时间影响所有付费用户,出错了用户直接骂街。这一问就直接把优先级拉满了。
第二问状态分支:到期时间的处理同时涉及本地时区、UTC、夏令时、前端展示格式、后台统计口径。分支至少四个,复杂度中等偏上。
第三问发现时长:如果用户不说,我根本不知道时间错了。没有监控、没有告警,等到用户发现已经是几天后。这一问又拉高了优先级。
第四问修复牵动面:我把时间处理工具函数用在了至少五个地方——用户列表排序、会员状态判断、看板统计、续费邮件、定时任务。这一问几乎就是“必须测”的最后一块拼图。
走完四问,结论就非常清晰了:这个模块至少要有“单元测试 + 关键路径集成测试”。单元测试覆盖时间格式化和时区转换的各个分支,集成测试覆盖“接口收到 ISO 时间字符串后数据库存储、前端展示各环节都一致”。而另一个冷门功能,比如“会员等级徽章颜色配置”,走完四问之后就三个字:不测,放。
3.4 测试级别速查表:你怎么知道自己该做到哪一层
为了让你不用每次都在脑子里过一遍四问,我把判断结果整理成一个速查表,可以贴在手边直接用。
| 模块特征 | 建议测试级别 | 精力占比 |
|---|---|---|
| 核心业务状态 + 影响所有用户 | 单元测试(覆盖所有分支)+ 集成测试(关键接口) | 60% |
| 关键工具函数 + 被多处引用 | 单元测试(正常 + 边界 + 异常) | 15% |
| 普通 CRUD + 单一职责 | 冒烟测试(接口能跑通正常路径即可) | 15% |
| 纯展示、一次性脚本、原型 | 不写自动化测试,人工过一眼 | 10% |
这个表格就是我对“写到什么程度”最直接的答案:独立开发者的测试预算应该极度向“核心业务逻辑”倾斜,其他部分能省则省。不要平均用力,也不要把覆盖率当目标。你花在测试上的每一分钟,都要能回答一个问题:这笔时间帮我躲过了哪一类风险?
4. 轻量测试工具链:跑得越快,你才越愿意写
测试策略定了,工具也得选对。独立开发者的工具链原则和团队项目不太一样:跑得慢的测试等于没写测试。你想想,如果每次跑测试要 30 秒,你在开发的时候就不会想跑;不想跑,测试就是摆设。好的测试工具链,应该让“写完代码顺手跑一下测试”这个动作成本无限趋近于零。
4.1 选型逻辑:本地秒级跑通比什么都重要
很多人在工具选型时会陷入一个误区——选“功能最全”的框架,甚至把团队里用过的那套原封不动搬过来。但对独立开发者来说,真正的选型标准只有三条:
- 测试启动时间短:最好一秒内能跑完核心测试,最多不能超过五秒。我见过有人在自己的前端项目里跑一个测试要十几秒,后来他再也没跑过。
- 断言语法自然:读测试代码像读业务规则,而不是像读框架文档。这一点直接决定了你写测试的意愿。
- 报错信息能直接定位:测试红了,你得一眼看出是哪个断言、哪个值不对。如果报错只会告诉你“expect failed”,那排查的时间成本会让你讨厌测试。
基于这三条标准,我常用的搭配是:Python 后端用pytest+pytest-cov,前端用Vitest(Vite 项目天然快),Node 后端的简单模块直接上内置的node:test。这几个框架的共同优点是很轻、文档好、报错信息友好。
4.2 最小可用的测试骨架:一个示例
工具再多,不如直接给你看一个能抄的骨架。假设我在写一个 Node.js 后端项目的核心计算模块,我会顺手建一个测试文件:
// commission.test.js import { describe, it, expect } from 'vitest'; import { calcCommission } from './commission.js'; describe('佣金计算规则', () => { it('正常订单:按订单金额的 10% 计算', () => { expect(calcCommission({ amount: 100, status: 'paid' })).toBe(10); }); it('部分退款:按实际收款金额重新计算', () => { expect(calcCommission({ amount: 100, refunded: 40 })).toBe(6); }); it('已全额退款:佣金为 0', () => { expect(calcCommission({ amount: 100, refunded: 100 })).toBe(0); }); });配合package.json里的脚本:
{ "scripts": { "test": "vitest run", "test:watch": "vitest" } }然后你在终端里跑npm test,几十毫秒出结果。随着项目变大,再在vitest.config.js里按模块拆分,只跑变更相关的文件。
实测下来,这套组合在本地开发时的体验近乎无感。它不会打断你写代码的节奏,反而会给你一种“写完就拿住”的正反馈。
4.3 不用等“写完再测”,改成“写完一个函数顺手补断言”
独立开发者最常见的心理障碍是:测试太占时间,我要等全部写完了再补。但真等你写完再补,大概率是永远不会补了。
我的习惯是把写测试拆成“顺手动作”:每写完一个工具函数,顺手写三条断言(正常路径+边界路径+异常路径),加起来不过两分钟。这不是“先测试后开发”的 TDD,也不是“写完系统再补测试”的大扫除,而是在模块级别立刻锁定行为。等整个功能做完,核心模块其实已经有了一小撮测试垫底,这时候你反而愿意继续补充。
还有一个很实用的小技巧:每条测试都要有明确的名字,读起来像一句人话。比如上面例子里“已全额退款:佣金为 0”就比“test 3 should pass”强百倍。测试不仅是验证工具,更是系统里最好用的一份行为文档。
4.4 在 CI 里的真实配置思路:不是全量跑,而是关键路径必跑
很多独立开发者觉得,项目都上了 CI,那测试就得全量跑,不做不行。我的建议反而相反:别把全量测试变成 CI 的负担,而是把关键路径测试变成 CI 的守门员。
具体做法很朴素:写两个命令,一个是“快速回归”,只跑核心模块的测试,要求一分钟内跑完;另一个是“完整测试”,留到发布前手动跑。CI 的钩子里只挂快速回归,这样每次 push 它都能在二十秒内出结果。完整测试留给你自己,在准备打 tag 发布的时候跑一次。
# 快速回归:核心模块 npm run test:core # 完整回归:发布前手动执行 npm run test:all这套方案的逻辑很简单:CI 的职责不是替你抓所有 bug,而是给你一个“每次改动后心里有底的信号”。如果这个信号来得太重,你就会忍不住绕过它,那就彻底失去意义了。
5. 忍住不写的时刻:测试不是越多越好,这几个场景请果断放弃
聊到这里,前面讲的都是“哪些值得写”。但我想花一整章说反方向的事,因为以我踩坑的经验来看,独立开发者更容易犯的错误往往不是“测试太少”,而是“测试太多”——测试写多了,一样会把你拖垮。我见过太多人(包括我自己)从“不写测试”的极端,直接跳到“什么都测”的另一个极端,最后被测试代码的维护成本压得喘不过气。
5.1 测试本身是负债:读写成本、跑动成本、重构成本
测试不是免费的。每写一条测试,你都欠下了三笔债:写它花的时间(读成本)、每次运行花的时间(跑成本)、以及业务逻辑变更时你不得不跟着更新它的时间(改成本)。
独立开发者最输不起的不是代码行数,是时间窗口。你的竞争优势在于“一个人能快速迭代”,而测试负债太重,会直接拖慢你的迭代速度。特别是当需求频繁调整阶段,一个模块的测试可能在三个月内重写三遍,这时候测试从资产变成了纯负债。
我之前做的一个后台报表模块,就踩过这种坑。最初我把每一个图表的数据聚合查询都写了单元测试,一共四十多条。后来产品把报表的维度从“按天”改成“按周”,又加了“按账号过滤”,我改那四十多条测试花了一整个下午。从那天起,我给自己定了个规矩:离“需求容易变化”越近的代码,越不要急着写详细测试。
5.2 可以直接放弃测试的具体场景清单
给一份可以直接放弃测试的清单,这些场景我都经历过,也不后悔当初没写:
- MVP 阶段的核心流程验证:你还没验证产品方向对不对,这时候写测试就是给草稿排版。
- UI 频繁调整的页面组件:按钮位置、颜色、间距,改一次测试就得重写一次,毫无意义。
- 只跑一次的数据脚本:只要操作前做好备份和预检,脚本本身不需要测试。
- 探索性重构前的旧代码:你打算完全重写的模块,别先补测试再重构,那是给要拆的房子装防震。
- 需求还在大量漂移的模块:连你自己都不确定下个月这个功能还在不在,写什么测试?
我看到很多独立开发者在这些场景里写测试,本质上是“为了心理安慰”或者“为了仪式感”。如果你发现自己在凭惯性写测试,停下来问一句:我真的会在下次改这段代码的时候,依赖这个测试吗?答案犹豫了,就说明不该写。
5.3 测试过度的典型症状:怎么识别自己陷进去了
怎么判断自己陷入了“为了写测试而写测试”的境地?我总结了三个典型症状,你可以对照检查:
第一个症状:为了 mock 而 mock。你花半小时去 mock 数据库、mock 第三方接口、mock 时间,结果最后测的其实是 mock 代码本身,真实逻辑反而没测到。如果发现一条测试的 mock 代码比被测代码还长,这件事大概率跑偏了。
第二个症状:为了凑覆盖率硬造断言。你写了一个断言,但那个断言的内容和业务规则没有任何关系。比如测一个排序函数,断言“数组长度不变”——这种断言除了让覆盖率数字更好看,什么都不值。
第三个症状:测试比实现代码还难读。如果你打开一个测试文件,需要花五分钟才能看懂它在测什么业务场景,那这条测试已经失去了文档价值。好的测试应该像一句话需求描述,而不是被层层 mock 包裹的俄罗斯套娃。
一旦识别出这些症状,我的建议很直接:删掉那些负资产测试,不要觉得可惜。删测试和删代码一样,都是开发者的基本修养。你删掉的不是“保障”,而是“负担”。
5.4 一个合法的不写测试场景:临时兼容层
最后说一个比较特殊但合法的不写测试场景:明确知道下个月要重写的临时兼容层。
独立开发里经常出现这种需求:老版本数据是 JSON 格式,新版本要改成 SQLite;或者第三方服务要迁移,你不得不先写一个适配层过渡。这类代码的生命周期通常很短,长得丑一点没关系,只要走得稳。这种情况下,我的建议是别为它写测试,因为你很快会把它换掉。但有一个前提:迁移后必须确保老数据不会丢。如果涉及存量数据迁移,就按第二章说的,写“预检查脚本”而不是“测试”。
判断标准还是那句:问自己“这段代码活过一年的概率大不大”。活不过一年的代码,别写测试;可能活三年的,才值得为它买单。
6. 独立开发者写测试的真正回报:它让你敢改代码,也让你少陪用户熬夜
说了这么多,其实最想分享的是心态层面的感悟。测试对独立开发者的价值,从来不在于“测试报告多漂亮”,也不在于“覆盖率多高”,而在于它给了你一个特别珍贵的东西:改代码的底气。有了测试垫底,你重构老模块时敢动手,你升级第三方库时敢上线,你半夜收到用户反馈时,能先跑一条测试确认是不是自己的问题,而不是把所有功能都手工点一遍。
6.1 测试即决策底气:重构时它能让你安心上线
我有一句话常对朋友讲:测试不是用来证明“代码没有 bug”的,而是用来证明“我改的这段代码,没有把之前的行为弄坏”。
有一次我给一个老项目加新功能,需要重构用户缓存模块。老代码逻辑混乱,SQL 和缓存交织在一起,我改的时候手心都是汗。但幸运的是,两年前我给这个模块写过一组“关键路径测试”,覆盖了“缓存命中返回正确用户”“缓存失效回源数据库”“写操作后主动清缓存”三个场景。重构完,跑了一遍测试,全绿,我当时长出一口气——这种踏实感,比任何代码 review 都有效。
如果你还没有这种体验,我建议你找个周末,挑一个你最怕改的模块,花一个小时给它的核心路径写几条测试。你不需要覆盖所有分支,只需覆盖“现在它应该怎么做”。下个季度你再回来改这个模块时,你会感谢这一小时。
6.2 测试即文档:三个月后回看,它能告诉你当初为什么这么写
独立开发者还有一个隐形痛点:代码写出来三个月后,连自己都忘了当初为什么这么设计。注释会过期,文档会失真,但测试不会——只要它在运行,它就在描述代码真实的行为。
举一个我自己的例子。某次我改一个订单回调接口,发现原来代码对“重复回调”有特殊处理:如果订单已经成功,直接忽略新回调。我看了一眼,第一反应是“这逻辑多余,删掉算了”。但备份之前我顺手扫了一眼旧测试,发现有一条写着“重复回调不改变订单状态”,旁边备注是“用户曾反馈重复回调导致积分重复发放”。如果没有这条测试,我就把这句话沉默地删了,然后等着三个月后某个用户来告诉我,积分又双叒重复发放了。
这就是我说的“测试即文档”的含义:它不止记录代码行为,还保留了当初写代码时最重要的上下文。对于没有同事可问的独立开发者来说,这份上下文是无价的。
6.3 最后分享一个小技巧:给每个被用户反馈的 bug 补一条回归测试
如果想在“写测试”这件事上做到性价比最高,我强烈推荐一个做法:每修一个 bug,就写一条能复现它的测试。这是整个测试策略里最划算的投资,因为一个 bug 能被用户发现,至少说明两件事:第一,它在真实场景里一定会再发生;第二,你现在能完整地描述它。
步骤很简单:
- 先写一条测试,输入给的是触发 bug 的参数,期望值是你修好后的正确输出。
- 跑一遍,确认它是红的(证明它能复现问题)。
- 去修代码。
- 再跑一遍,确认它变绿(证明修复生效)。
这个“红-绿”循环不花两分钟,但它把一次性的修 bug 变成了一份长期的保护。以后任何一次重构,这条测试都会在那儿帮你守着,防止同一个坑被同一个人(你)再踩一次。我这两年项目里最有价值的测试,几乎全部来自这个习惯,而不是来自那种“先定计划再补测试”的仪式感。
写到这儿,关于独立开发者测试这件事,我能分享的想法基本说完了。说到底,测试方法论和代码一样,也该跟随项目成长,不必一步到位。项目刚起步时,你可能只需要给最核心的计算函数写二十行测试;等项目用户多了、逻辑复杂了,再慢慢把测试网织得更密。独立开发者最该珍视的能力,是“敢改自己代码”的能力,而测试,就是买下这个能力最便宜的那份保险。