单测运行失败的唯一原因应该是出现bug,因不稳定而导致的失败会失去单测的警示作用,
“狼来了”的故事是惨痛的。一起来看看我们曾掉进的单测陷阱吧。
陷阱一:异步超时 + mock方法失效
被测代码:
func (c *JunoClient) RecallCampaignWithRetry(junoCtx *dsp_context.PipeJunoContext) {
...
go func() { // 异步, 子协程
out, err := mrpc.GetCfg().GetRouterKey()
call2 <- struct{}{}
}()
...
select {
case <-time.After(time.Millisecond): // 超时退出, 子协程还在
return
case call := <-call2:
return
}
测试代码:
func TestJunoClient_RecallCampaignWithRetry(t *testing.T) {
...
patch := gomonkey.ApplyFunc(mrpc.GetCfg, func() *mrpc.Config {
return &mrpc.Config{}
})
defer patch.Reset() // 还有子协助在执行mrpc.GetCfg().GetRouterKey(),patch无效, 报空指针异常
...
}
陷阱二:忽略map按键输出结果是随机的
被测代码:
type IntSet map[int64]struct{}
func (set IntSet) Add(k int64) {
if set == nil {
return
}
set[k] = struct{}{}
}
// Keys 返回集合中的所有整数作为切片。
func (set IntSet) Keys() []int64 {
if len(set) == 0 {
return nil
}
keys := make([]int64, len(set))
idx := 0
for k, _ := range set {
keys[idx] = k
idx++
}
return keys
}
测试代码:
func TestIntSet(t *testing.T) {
// 测试 Add 和 Exist 方法
set := IntSet{}
set.Add(1)
set.Add(2)
set.Add(3)
...
// 测试 Keys 方法
assert.Equal(t, []int64{1, 2, 3}, set.Keys()) // map按键输出结果随机,case执行结果不稳定
}
陷阱三:忽略测试可移植性,单测直接引入外部依赖存储/数据/服务
被测代码:
func initDicLoader(config loader.DicLoaderConfig) error {
if err := DicLoader.Init(config); err != nil {
return err
}
return nil
}
测试代码:
func TestInitDictLoader(t *testing.T) {
Convey("test initDictLoader", t, func() {
...
conf := loader.DicLoaderConfig{
WatchDir: "../data", // 引入开发本地环境数据,单测在非开发本地环境无法执行通过
}
err := initDicLoader(conf)
So(err, ShouldBeNil)
})
}
总结
以上内容我们找了tb制作成了小幅的宣传海报,张贴在公司洗手间,茶水间,小型会议室,研发工位旁等地儿。
通过简洁而直观的宣传海报,在研发团队中植入两个核心思维:首先,强调编写单元测试;其次,强调编写高质量的单元测试。这两点激发工程师认识到单元测试的重要性以及质量对于整个代码过程的关键性。 这种抛砖引玉的方式,期望能够在团队中引发更多对于代码质量的讨论和关注。
在贴出海报后,我们也收集到了一些研发工程师提出的不稳定单元测试案例。例如,有开发者反馈多个模块的单元测试引用了一些旧版本的 GoMonkey,在 Macbook M1/M2 上运行失败,导致其他测试用例的执行受到了影响。另一位开发者建议使用字节开源的 Mockey 库,提高测试稳定性。
总体来说,这次的宣传活动取得了良好的反馈。工程师们对于单元测试的关注度提高了,同时也积极分享了一些实际案例,为整个团队共同提高代码过程质量贡献了有益的经验。
后续的每一期内容我都会及时发布在掘金的工程师质量文化专栏,欢迎大家关注,共同进步。
最后,给大家分享下我们在公司洗手间,办公室,工位旁张贴的海报照片吧~~