一个人的技术债:用守卫测试代替记性
Four months, 1783 commits, one person. Memory is the least reliable asset in a solo project — so every mistake we made became a guard test whose comment explains how it originally broke.
一个人的项目里,最不可靠的资产是记性 ✗ 所以我们的做法是:每踩一个坑,就把它变成一条守卫测试 ✓ —— 而且注释里必须写清"当初是怎么错的"。
问题
一个人 + 四个月 + 1783 次提交 ⇒ 会反复犯同一类错误 ✗ 因为:
- 没有 code review ✗;
- 改 A 的时候看不到 B 依赖 A ✗;
- 三个月前那句"这里以后要注意"没有任何强制力 ✗。
"以后要注意" = 不存在的约定 ✓ 它只存在于写它的那个下午。
做法:把不变量写成测试,而不是写成文档
文档会过期 ✗(我们的 docs/ 有 96 篇,其中不少在写完之后就与世界脱节了 ✓)。
测试不会 —— 它只有两个状态:绿,或红 ✓
于是我们维护了一张"现在有测试守着的不变量"表。下面挑几条真实例子, 它们的共同点是:每一条都对应一次真实事故 ✓
| 不变量 | 它曾经怎么错的 |
|---|---|
agent_loop 的每次 LLM 调用都必须显式带 api_key= |
漏一处就会静默落到服务器公用 key ⇒ 出账事故(别人聊天,我们付钱) |
| 人工接管必须是绝对超时、不被用户活动刷新 | 忘了关 = 用户永久收不到回复且不报错(最难查的那种 bug) |
线程池提交必须带 copy_context() |
否则上下文串号 ⇒ 用户 A 的数据落进 B 的目录 |
列用户不许裸 iterdir();降级名只有一个 |
目录里混进伪用户/临时目录 ⇒ 后台统计凭空多出账号 |
| 同一个干员在两个用户下必须拿到不同的关系边 | 全局缓存漏了用户维度 ⇒ 张三的关系显示给李四 |
| 后台自治任务不许为"没配自己 key"的账号跑 | 否则用公用 key 替没配 key 的用户生成内容 |
| 金/橙语义色在各自主题底色上 ≥4.5:1(真算 WCAG) | 之前只检查"有没有列出来",不检查对比度 ⇒ 浅底主题上文字看不清 |
| 浅底主题必须覆盖 9 个明暗极性 token | 漏一个就出现"深色面板压在浅色页面上" |
| 两套主题的生效值至少差 8 个 token | 否则"换主题"看起来没生效 |
每个含 .scroll-list 的视图都要为底部 Tab Bar 让位 |
手写视图名单必然漏 ⇒ 最后一条消息被 Tab Bar 挡住 |
测试文件不许动 sys.modules、不许有开发机绝对路径 |
否则测试只在开发机绿 |
干员 ID 大小写必须与 .char 文件名逐字一致 |
大小写不一致 ⇒ 她的设定静默加载不到 |
十几条不变量 = 十几次事故 ✓ 每一条都曾经让某个用户看到过坏掉的东西 ✓
守卫测试的写法(我最看重的一点)
一条好的守卫测试,注释比断言重要 ✓ 我们的约定是:
def test_takeover_timeout_is_absolute():
"""⚠️ 人工接管必须是**绝对超时**,不被用户活动刷新。
事故:接管开着、管理员忘了关 ⇒ 用户**永久收不到回复,而且不报错** ✗
(没有 500、没有日志、界面照常转圈。)这一条是那之后补的。
"""
为什么:半年后的自己只会看到一条失败的测试 ✗ 而注释告诉他"这是哪个事故换来的" ✓ 于是判断"能不能改"就有了依据 —— 而不是"这条测试好烦,删掉试试" ✗
结果
| 不变量 | 十几条核心的 + 各模块的,合计 90+ 条测试在基线之外盯着 |
| 写法 | 注释写清当初怎么错的(不是复述断言) |
| 覆盖 | 用户隔离、缓存维度、主题对比度、前端布局、API key 归属、定时任务… |
| 收益 | 改坏了会红,而不是"上线三天后用户来报" ✓ |
⚠️ 顺带一条诚实的:本地跑测试时有 90 条基线失败(本地缺依赖、用桩跑造成的噪声 ✗)。 所以我们的规矩是:只把"新增失败"当信号 ✓ —— 否则红海一片,谁都不看了 ✗ 测试的价值不在全绿,而在于"红的那条是新红" ✓
踩坑与教训
- "以后要注意" 没有任何强制力 ✓ 要么变成测试,要么变成删除的代码 ✗;
- 一条不变量配一条测试,而不是一个文件配一堆测试 ✓ 后者会变成"红了一片但不知道哪条算数" ✗;
- 注释里写事故,不写断言 ✓ 断言自己能读,事故只有当时在场的人知道 ✓;
- 允许有基线噪声,但必须能区分新红 ✓ 否则团队(哪怕只有一个人)会开始无视测试 ✗;
- 技术债不是"以后要还的钱",是"以后一定会忘的事" ✓ 用测试把它钉住,就是还债 ✓。
这一篇的不变量都能在 tests/ 里找到对应文件;每条注释里都有它换来的那次事故。