文末有彩蛋。作者:yukkizhang,腾讯CSIG专项技术测试工程师本篇文章站在测试的角度,旨在给行业平台乃至其他团队的开发同学,进行一定程度的单元测试指引,让其能够快速的明确单元测试的方式方法。本文主要从单元测试出发,对Golang的单元测试框架、Stub/Mock框架进行简单的介绍和选型推荐,列举出几种针对于Mock场景的最佳实践,并以具体代码示例进行说明。一、单元测试1.单元测试是什么单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、超类、抽象类等中的方法。单元测试就是软件开发中对最小单位进行正确性检验的测试工作。不同地方对单元测试有的定义可能会有所不同,但有一些基本共识:单元测试是比较底层的,关注代码的局部而不是整体。单元测试是开发人员在写代码时候写的。单元测试需要比其他测试运行得快。2.单元测试的意义提高代码质量。代码测试都是为了帮助开发人员发现问题从而解决问题,提高代码质量。尽早发现问题。问题越早发现,解决的难度和成本就越低。保证重构正确性。随着功能的增加,重构(修改老代码)几乎是无法避免的。很多时候我们不敢重构的原因,就是担心其它模块因为依赖它而不工作。有了单元测试,只要在改完代码后运行一下单测就知道改动对整个系统的影响了,从而可以让我们放心的重构代码。简化调试过程。单元测试让我们可以轻松地知道是哪一部分代码出了问题。简化集成过程。由于各个单元已经被测试,在集成过程中进行的后续测试会更加容易。优化代码设计。编写测试用例会迫使开发人员仔细思考代码的设计和必须完成的工作,有利于开发人员加深对代码功能的理解,从而形成更合理的设计和结构。单元测试是最好的文档。单元测试覆盖了接口的所有使用方法,是最好的示例代码。而真正的文档包括注释很有可能和代码不同步,并且看不懂。3.单元测试用例编写的原则3.1理论原则快。单元测试是回归测试,可以在开发过程的任何时候运行,因此运行速度必须快一致性。代码没有改变的情况下,每次运行得结果应该保持确定且一致原子性。结果只有两种情况:Pass/Fail用例独立。执行顺序不影响;用例间没有状态共享或者依赖关系;用例没有副作用(执行前后环境状态一致)单一职责。一个用例只负责一个场景隔离。功能可能依赖于数据库、web访问、环境变量、系统时间等;一个单元可能依赖于另一部分代码,用例应该解除这些依赖可读性。用例的名称、变量名等应该具有可读性,直接表现出该测试的目标自动化。单元测试需要全自动执行。测试程序不应该有用户输入;测试结果应该能直接被电脑获取,不应该由人来判断。3.2规约原则在实际编写代码过程中,不同的团队会有不同团队的风格,只要团队内部保持有一定的规约即可,比如:单元测试文件名必须以xxx_test.go命名方法必须是TestXxx开头,建议风格保持一致(驼峰或者下划线)方法参数必须t*testing.T测试文件和被测试文件必须在一个包中3.3衡量原则单元测试是要写额外的代码的,这对开发同学的也是一个不小的工作负担,在一些项目中,我们合理的评估单元测试的编写,我认为我们不能走极端,当然理论上来说全写肯定时好的,但是从成本,效率上来说我们必须做出权衡,衡量原则如下:优先编写核心组件和逻辑模块的测试用例逻辑类似的组件如果存在多个,优先编写其中一种逻辑组件的测试用例发现Bug时一定先编写测试用例进行Debug关键util工具类要编写测试用例,这些util工具适用的很频繁,所以这个原则也叫做热点原则,和第1点相呼应。测试用户应该独立,一个文件对应一个,而且不同的测试用例之间不要互相依赖。测试用例的保持更新4.单元测试用例设计方法4.1规范(规格)导出法规范(规格)导出法将需求”翻译“成测试用例。例如,一个函数的设计需求如下:函数:一个计算平方根的函数输入:实数输出:实数要求:当输入一个0或者比0大的实数时,返回其正的平方根;当输入一个小于0的实数时,显示错误信息“平方根非法—输入之小于0”,并返回0;库函数printf()可以用来输出错误信息。在这个规范中有3个陈述,可以用两个测试用例来对应:测试用例1:输入4,输出2。测试用例2:输入-1,输出0。4.2等价类划分法等价类划分法假定某一特定的等价类中的所有值对于测试目的来说是等价的,所以在每个等价类中找一个之作为测试用例。按照[输入条件][有效等价类][无效等价类]建立等价类表,列出所有划分出的等价类为每一个等价类规定一个唯一的编号设计一个新的测试用例,使其尽可能多地覆盖尚未被覆盖地有效等价类。重复这一步,直到所有的有效等价类都被覆盖为止设计一个新的测试用例,使其仅覆盖一个尚未被覆盖的无效等价类。重复这一步,直到所有的无效等价类都被覆盖为止例如,注册邮箱时要求用6~18个字符,可使用字母、数字、下划线,需以字母开头。测试用例:4.3边界值分析法边界值分析法使用与等价类测试方法相同的等价类划分,只是边界值分析假定错误更多地存在于两个划分的边界上。边界值测试在软件变得复杂的时候也会变得不实用。边界值测试对于非向量类型的值(如枚举类型的值)也没有意义。例如,和4.1相同的需求:划分(ii)的边界为0和最大正实数;划分(i)的边界为最小负实数和0。由此得到以下测试用例:输入{最小负实数}输入{绝对值很小的负数}输入0输入{绝对值很小的正数}输入{最大正实数}4.4基本路径测试法基本路径测试法是在程序控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行路径集合,从而设计测试用例的方法。设计出的测试用例要保证在测试中程序的每个可执行语句至少执行一次。基本路径测试法的基本步骤:程序的控制流图:描述程序控制流的一种图示方法。程序圈复杂度:McCabe复杂性度量。从程序的环路复杂性可导出程序基本路径集合中的独立路径条数,这是确定程序中每个可执行语句至少执行一次所必须的测试用例数目的上界。导出测试用例:根据圈复杂度和程序结构设计用例数据输入和预期结果。准备测试用例:确保基本路径集中的每一条路径的执行。二、Golang的测试框架Golang有这几种比较常见的测试框架:从测试用例编写的简易难度上来说:testify比GoConvey简单;GoConvey比Go自带的testing包简单。 但在测试框架的选择上,我们更推荐GoConvey。因为:GoConvey和其他Stub/Mock框架的兼容性相比Testify更好。Testify自带Mock框架,但是用这个框架Mock类需要自己写。像这样重复有规律的部分在GoMock中是一键自动生成的。1.Go自带的testing包testing为Go语言package提供自动化测试的支持。通过gotest命令,能够自动执行如下形式的任何函数:func TestXxx(*testing.T)注意:Xxx可以是任何字母数字字符串,但是第一个字母不能是小写字母。在这些函数中,使用Error、Fail或相关方法来发出失败信号。要编写一个新的测试套件,需要创建一个名称以_test.go结尾的文件,该文件包含TestXxx函数,如上所述。将该文件放在与被测试文件相同的包中。该文件将被排除在正常的程序包之外,但在运行gotest命令时将被包含。有关详细信息,请运行gohelptest和gohelptestflag了解。1.1第一个例子被测代码:func Fib(n int) int { if n 600 and id