如何写好 GO 语言单元测试

2019-08-09   出处:腾讯移动品质中心TMQ  作/译者:彭文飞  
 通过基本的单元测试框架介绍(http://km.oa.com/group/viptest/articles/show/374474)和mock框架介绍(http://km.oa.com/group/viptest/articles/show/377938),能指引我们会写自己的单元测试了,最 近在给开发同学宣讲go单测时,交流过程发现开发同学特别关注如何写出好的单元测试,最近也在看业界大牛们的分享,结合实践过程理解,大致整理了下几个要点。


用断言来代替原生的报错函数让我们看这样一个例子: 

GO 语言提供的 Error 太不友好了,判断的 if 需要写在前头。对于这些写 UT 行数还要超过功能代码的 GO 语言编写者来说,增加的代码量是非常恐怖的。使用断言可以让我们省略这个判断的 if 语句,增强代码的可读性。GO 语言本身没有提供assert 包,不过有很多开源的选择。比如使用https://github.com/stretchr/testify,上面的例子可以简化为:

除了 True 和 Equal 之外当然还有很多其它断言,这就需要我们自己看代码或文档去发现了。


避免随机结果 让我们看这样一个例子:

UT 的结果应当是决定性(decisive)的,当我们使用了随机的输入值来进行 UT 时,我们让自己的测试用例变得不可控。当一切正常时,我们还不会意识到这样的坏处,然而当糟糕的事情发生时,随机的结果让我们难以 debug。
比如,上例在大多数时候都能正常运行,唯有当 b 随机到 0 时会 crash。在上例,比较正确的做法是:

避免无意义重复让我们看这样一个例子

在设计 UT 时,我们要问问自己,重复执行 doSomeThing 多次会带来不同的结果吗,如果总是同样的结果,那么 doSomeThing 只做一次就足够了。如果确实会出现不同的结果,那简单重复 10000 次不仅浪费了有限的 CPU 等资源,也比不上精心设计的不同断言能给我们带来的更多好处。 在上例,比较正确的做法是:

尽量避免断言时间的结果

让我们看这样一个例子:即便我们很笃定 doSomeThing() 一定确定以及肯定能在 1 秒内完成,这个测试用例依然有很大可能在某个性能很差的容器上跑失败。除非我们就是在测试 Sleep 之类跟时间有关的函数,否则对时间的断言通常总是能被转化为跟时间无关的断言。一定要断言时间的话,断言超时比断言及时更不容易出错。比如上面的例子,我们没办法断言它一定在 1 秒内完成,但是大概能断言它在 10 微秒内完不成。
尽量避免依赖外部服务即使我们十分确信某个公有云服务是在线的,在 UT 中依赖它也不是一个好主意。毕竟我们的UT 不仅会跑在自己的开发机上,也会跑在一些沙盒容器里,我们可无法知道这些沙盒容器一定能访问到这个公有云服务。如果访问受限,那么测试用例就会失败。要让我们的测试用例在任何情况下都能成功运行,写一个 mock 服务会是更好的选择。不过有些外部服务是必须依赖且无法 mock 的,比如测试数据库驱动时必须依赖具体的数据库服务,对于这样的情况,我们需要在开始 UT 之前设置好相应的环境。此时也有一些需要注意的地方,见下节:


优雅地实行前置和后置任务 为了设置环境或者为了避免测试数据污染,有时候有必要进行一定的前置和后置任务,比如在所有的测试开始的前后清空某个测试数据库中的内容等。 这样的任务如果在每个测试用例中都重复执行,那不仅是的代码冗余,也是资源的浪费。我们可以让 TestMain 来帮我们执行这些前置和后置任务:

TestMain 函数是 GO 测试框架的入口点,运行 m.Run 会执行测试。TestMain 函数不是必须的,除非确实有必要在 m.Run 的前后执行一些任务,我们完全可以不实现这个函数。


测试用例之间相互隔离 TestA,TestB 这样的命名规则已经帮我们在一定程度上隔离了测试用例,但这样还不够。如果我们的测试会访问到外部的文件系统或数据库,那么最好确保不同的测试用例之间用到的文件名,数据库名,数据表名等资源的隔离。 用测试函数的名字来做前缀或后缀会是一个不错的方案,比如:

这样隔离的原因是所有的测试用例会并发执行,我们不希望我们的用例由于试图在同一时间访问同一个文件而互相影响 。


面向接口编程 这是典型的测试倒逼功能代码。 功能代码本身也许完全不需要面向接口编程,一个具体的结构体就足够完成任务。可是当我们去实现相应的单元测试时,有时候会发现构造这样一个具体的结构体会十分复杂。 这种情况下,我们会考虑在实际代码中使用接口(interface),并在单元测试中用一个 mock组件来实现这个接口。考虑如下代码:

我们要为这个 someStruct 写 UT,就不得不先构造出一个 ComplexInnerStruct。而这个ComplexInnerStruct 可能依赖了几十个外部服务,构造这样一个结构体会是一件十分麻烦的事情。此时我们可以这样做,首先我们修改实际的代码,让 someStruct 依赖某个接口而不是某个具体的结构体:

接下来我们的 UT 就可以用一个 mock 结构体来代替那个 ComplexInnerStruct: 

这样,我们就帮自己省去了在 UT 中创建一个 ComplexInnerStruct 的繁杂工作。
最后是一些是code Smell(更多的是对代码的可测性要求),用例设计遵循原则:1、注释要清晰明朗,谜一样的注释、无注释都会影响阅读代码的理解2、重复代码需要抽取与分离3、声明与使用距离太远,不容易读懂4、箭头式的代码,提升了圈复杂度,也降低了可测性5、将内部逻辑与外部请求分开测试(small测试)6、函数太复杂,没有拆分到位,让单个函数功能行为单一且简单7、大量使用成员方法和函数,不利于传参进行测试8、函数不要太长(建议小于40行)9、文件不要太长(建议小于400行)10、原子性,所有的测试只有两种结果:成功或失败11、避免测试中的逻辑,即不该包含if、switch、for、while等12、每个用例只测试一个关注点13、少用sleep,延缓测试时长的行为都是不健康的


欢迎给测试窝投稿或参与内容翻译工作,请邮件至editors@testwo.com。也欢迎大家通过新浪微博(@测试窝)或微信公众号(测试窝)关注我们,并与我们的编辑和其他窝友交流。
158°|1585 人阅读|0 条评论

登录 后发表评论