为了测试覆盖率而写测试,最后反而成了开发者的负担。
在给我的 Candidate Tracker 写 Job Opportunity 创建流程时,我发现了一个很离谱的现象:我的
如果这三个用例挂了,意味着系统可能会接受脏数据或者破坏数据库的关系约束,这种风险是不可接受的。
测试的本质应该是降低我们修改代码时的恐惧感。当你发现为了写一个测试而搭建的环境比业务逻辑本身还复杂时,你应该考虑的是简化测试范围,而不是去简化功能。
下一篇
Demo跑通了不代表产品能成:聊聊AI应用落地的那些深坑 →
application-service 测试代码竟然比业务代码本身还难读。原本的生产环境逻辑极其简单,就三步:
1. 校验并标准化输入数据。
2. 确认所选公司是否存在。
3. 创建职位机会。
结果我的测试代码里充斥着各种 fixture、fakes、spies 和 mocks,甚至还得写一大堆断言来证明某个 repository 方法“绝对没有被调用”。这种复杂度在短期内看着很专业,但实际上它在消耗我的认知带宽。
我花了一下午时间复盘,决定砍掉那些为了测试而测试的代码,只保留真正能给我带来“信心”的用例。
哪些测试是必须留下的?
我最后只保留了三个 application-service 的测试用例,因为它们保护的是核心业务决策:
- 输入合法 → 数据被正确标准化并创建。
- 输入非法 → 在持久化之前就被拦截。
- 公司不存在 → 不会创建孤立的职位记录。
如果这三个用例挂了,意味着系统可能会接受脏数据或者破坏数据库的关系约束,这种风险是不可接受的。
哪些测试被我删掉了?
最让我纠结的是对 repository 返回失败结果的转发测试。理论上,我可以 mock 每一个 repository 方法,然后测试每一个分支,但这样写出来的代码本质上是在验证:
if (!result.success) return result;这段代码是否真的返回了它收到的结果。这太冗余了,TypeScript 的类型系统已经约束了这一点,而真正的持久化行为应该交给 PostgreSQL 的集成测试去验证。
如果我继续追求 100% 的分支覆盖率,我只会得到一堆毫无意义的 mock 代码,而不会增加我对系统的信心。
总结我的测试分层实操指南
这次踩坑后,我给自己定了一套简单的测试原则,建议大家在构建 AI Agent 或复杂工作流时也可以参考,避免陷入“测试地狱”:
- 单元测试 (Unit Test): 仅用于业务规则和关键决策点。
- 集成测试 (Integration Test): 验证实际的持久化行为(比如直接跑在 Docker 数据库上)。
- 端到端验证 (E2E): 跑通完整的用户工作流。
- 回归测试 (Regression Test): 只有在真实 Bug 出现且现有测试没覆盖到时,才补写相关用例。
- 禁忌: 不要为了刷测试数量或代码覆盖率而写测试。
测试的本质应该是降低我们修改代码时的恐惧感。当你发现为了写一个测试而搭建的环境比业务逻辑本身还复杂时,你应该考虑的是简化测试范围,而不是去简化功能。