小工具的测试策略:只测那几处容易错的地方
给一个几百行的命令行小工具写测试,最容易走两个极端:一是不写,靠手动跑;二是追求覆盖率,给每个函数都套一层。
两种都不划算。小工具的错误高度集中:真正会出问题的就那么几个纯函数,其余的失败大多是网络、权限、配置,测也测不出来。这篇说说我会测什么。
一、先分清三类代码
写之前我会把代码分成三堆:
- 纯函数:输入确定、输出确定、不碰网络和磁盘。比如时间截断、参数解析、字符串拼装。
- 编排逻辑:串起多个步骤,决定先做什么后做什么。
- 外部交互:HTTP 请求、文件读写、子进程。
第三类最难测也最少出逻辑错误,它出错往往是环境问题,靠日志和重试解决,不值得写单元测试。第一类正好相反:逻辑全在里面,错了还很难从日志看出来。
所以测试写在第一类。第二类靠一次端到端手动验证。
二、值得测的四个典型
时间相关的函数。 跨零点、跨时区、闰年,这几个边界人脑模拟起来容易错:
func TestDayOf(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
a := time.Date(2026, 10, 31, 23, 59, 59, 0, loc)
b := time.Date(2026, 11, 1, 0, 0, 1, 0, loc)
if dayOf(a).Equal(dayOf(b)) {
t.Fatal("跨零点被判断成同一天")
}
}
解析和格式化。 配置字段多了就有「没填」和「填了空」的区别,这类分支不测很容易漏。
状态判断。 「今天做过了吗」「这次算成功吗」——业务规则写错的成本最高,改起来也最便宜。
补跑、重试这类计数逻辑。 上限算不算、从 0 开始还是从 1 开始,都是典型的差一错误。
三、时间要能注入,不然测不了
上面那个 dayOf 能测,是因为它接收 time.Time 参数。如果函数内部直接调 time.Now(),就只能测「现在」这一种情况。
改造成本很小:
// 不好测
func shouldRun() bool {
return time.Now().Hour() == 9
}
// 好测
func shouldRunAt(now time.Time) bool {
return now.Hour() == 9
}
外面调的时候传 time.Now() 进去。多一个参数,换来的可测性是值得的。
四、别为外部接口造假实现
给 HTTP 客户端写 mock 是小工具里性价比最低的事。要造的东西太多:状态码、响应体、超时、重试,造完一圈发现测的是 mock 的行为,不是程序的行为。
真需要验证「拿到 500 会不会重试」,用一个 httptest.Server 起个真服务,几行就够:
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
它走的是真的 HTTP 栈,测出来的结论可信。
五、测试要能一次跑完
小工具的测试应该几秒钟跑完,不依赖网络和登录态。一旦某个测试需要真实 Cookie,整个 go test 就变成了手工操作,很快没人跑。
判断标准:go test ./... 在任何一台干净机器上都能通过。做不到的,那部分就别写成测试,写进文档说明怎么手动验证。
六、小结
小工具的测试不是用来证明「代码正确」,是用来防止「改一处错另一处」。所以覆盖范围窄一点没关系,关键是那几处一旦写错就会静默出问题的逻辑。
我的做法:纯函数全测,外部交互不测,端到端手动跑一遍。几百行的工具,测试文件通常也就一百行出头。
- 点赞
- 收藏
- 关注作者
评论(0)