它挡的是「算错了」——过滤、计算、分支这些能拿到输入输出的部分。「接错了」由接口测试挡,两者不是一回事。
把一条改坏的数据放进输入,这个测试会不会变红?
不会,说明用例只走了正常路径,或断言的是替身自己设的常量。补一条会失败的输入,再谈覆盖率的数字——那个数字只有在「改坏会红」的前提下才有意义。
它的判断到函数为止:给一组输入,看输出对不对。装不进「输入—输出」这个形状的事,它一概不管。
| 要判断的事 | 单元测试 | 交给谁 |
|---|---|---|
| 过滤逻辑有没有把别的范围算进来 | 挡得住,改坏输入会红 | — |
| 越权与不存在返回的是不是同一个错误 | 挡得住,断言错误值 | — |
| 会话里的范围值有没有传进处理函数 | 挡不住,函数在测试里是被直接调用的 | 接口测试 |
| 路由、序列化、鉴权中间件 | 不该由它测 | 接口测试 |
| 回答里有没有引用材料之外的内容 | 写不出可判定的字段 | 评测 |
| 页面文案与空状态 | 不在函数这一层 | 界面测试 |
边界要说清:它既便宜又快,所以能覆盖每一个分支;代价是它只保证自己断言过的那些行为,没写进用例的分支,覆盖率再高也是 0。
一条测试就是「准备—执行—断言」三段,缺一段就不算一条。名字里写清输入条件与期望。
TestListByIds_他范围的那条不出现(t *testing.T) ├── 准备 造两组输入:本范围一条 + 他范围一条 ├── 执行 调被测函数一次,拿到返回值 └── 断言 结果里不含他范围那条;右值来自被测逻辑
要认的是这个形状:断言必须比较两个值,且右值是被测逻辑的输出。断言替身设的常量、断言调用次数、只走正常路径,形状上都是「三段里少了一段」。
TestBuildRows(t *testing.T)
├── 准备 造一组输入
├── 执行 rows, err := buildRows(input)
└── 断言 assert.NoError(err)
assert.NotNil(rows)
assert.Len(rows, len(input)) 右值来自输入,不是被测逻辑
认它的办法:改坏被测逻辑,它还绿。三条断言一条都没说「结果里该有什么」——把实现改掉,长度照样等于输入长度。这类测试语法完全正确,还能把覆盖率抬上去,评审里最难看出来。
场景:让助手给「按范围过滤列表」这个纯函数补测试。输入是已经取出的记录与会话里的范围值,不碰数据库。
写完测试按这三问自查,任何一问答不出,这条测试还不算完成。
| 问什么 | 答「否」说明什么 |
|---|---|
| 把数据改坏,它会不会红? | 只走了正常路径,或断言的是替身设的常量 |
| 断言的右值是不是被测逻辑的输出? | 测的是替身自己,被测代码根本没执行 |
| 失败报告能不能直接定位到是哪一组输入? | 用例名没有带输入标识,定位要靠重跑 |
反过来也要认账:写不出测试通常不是没时间,是代码形状的问题——只能写「调用不报错」,说明结果藏在全局状态里;要连库才能构造输入,说明计算与数据访问没分开。出现这两种,先改代码形状,不要加 skip。纯展示的模板与生成代码不该有单元测试,让它们进检查的排除名单。
测试文件与被测文件同名同目录,命令挂进门禁那一条入口命令。覆盖率只回答「新增行有没有被执行过」,不回答断言对不对——所以跳过的用例必须是 0 且不增长,临时跳过要在用例名里写恢复时间;需要真实数据库的用例连不上就跳过,但跳过与通过必须是报告里不同的两行,不能换成内存库把方言差异盖掉。