Golang 代码测试:一点到面用测试驱动开发
摘要:TDD(Test Driven Development),测试驱动开发。期望局部最优到全局最优,这个是一种非常不错的好习惯。
了解 Golang 的测试之前,我们先了解一下 go 语言自带的测试工具。
go test 工具
Go 语言中的测试依赖 go test 命令。编写测试代码和编写普通的 Go 代码过程是类似的,并不需要学习新的语法、规则或工具。
go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go 为后缀名的源代码文件都是 go test 测试的一部分,不会被 go build 编译到最终的可执行文件中。
在*_test.go 文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
运行流程
go test 命令会遍历所有的*_test.go 文件中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
单元测试
以下是来自 wiki 对于单元测试的定义
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是必须的,但也不坏,这牵涉到项目管理的政策决定。
每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用 stubs、mock[1]或 fake 等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。它的实施方式可以是非常手动的(透过纸笔),或者是做成构建自动化的一部分。
简单来说,单元测试就是程序员自己对于自己的代码进行测试,而一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
更有一种开发手法,那就是 TDD(Test Driven Development),测试驱动开发。期望局部最优到全局最优,这个是一种非常不错的好习惯。
请注意这里的局部最优的,局部,并不是函数内的详细。而是整个函数。甚至是一个类,等等。
因为有些函数内部的最优,并非这个函数的最优。这点我们需要格外的注意。若有兴趣,可了解一下有点关系的贪心算法。
测试函数格式
其中参数 t 用于报告测试失败和附加的日志信息。
testing.T 的拥有的方法如下:
说了这么多,那么我们来实现一个简单的 string 中的 Split 函数,并对他进行单元测试,然后我们在剖析代码。了解单元测试的相关规范。
运行结果如下
说明测试成功,本次通过。当然你也可以在 Terminal 里面直接运行 go test,命令,如下所示
温馨提示:关于可能造成运行 test 不成功原因
直接在 split_test.go,运行。
我们或许知道,go 是以文件夹的方法来区分项目。所以当前文件,并不能跑到旁边文件中去找到 Split,以至于测试失败。或未达到预期效果。
那么正确的打开方式应该是?
在 goland 中,鼠标右键点击 run 测试文件所在的文件夹,选择后面第二个 go test projectFileName。
在 Terminal 中,应在测试文件所在的文件夹的路径中,进行 go test [arge...]。
示例看完了,那么我们进行简单的剖析。我们先从函数文件说起,(也就是这里的 splits.go)。
1. 不在是 package main,而是 packge projectFileName
2. 函数名大写,大写意味着公有函数,可支持外部调用
测试文件
1. 文件名为'*_test.go'
2. 不在是 package main,而是 packge projectFileName
3. 函数名为 TestFuncName
基准测试
基准测试函数格式
基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:
基准测试以 Benchmark 为前缀,需要一个*testing.B 类型的参数 b,基准测试必须要执行 b.N 次,这样的测试才有对照性,b.N 的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B 拥有的方法如下:
基准测试示例
我们为我们自己写的 Split 函数编写基准测试如下:
其中 BenchmarkSplit:表示对 Split 函数进行基准测试
BenchmarkSplit-8:数字 8 表示 GOMAXPROCS 的值,这个对于并发基准测试很重要
5188407 和 206ns/op:表示每次调用 Split 函数耗时 203ns
我们还可以为基准测试添加-benchmem 参数,来获得内存分配的统计数据。
112 B/op:表示每次操作内存分配了 112 字节
3 allocs/op:则表示每次操作进行了 3 次内存分配!!!
优化后代码如下:
优化后代码如下
这个使用 make 函数提前分配内存的改动,减少了 2/3 的内存分配次数,并且减少了一半的内存分配。
仅仅小小的一处改动,就引起如此大的性能改变。so good 量变产生质变
性能比较函数
上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理 1000 个元素的耗时与处理 1 万甚至 100 万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
性能比较函数通常是一个带有参数的函数,被多个不同的 Benchmark 函数传入不同的值来调用。举个例子如下:
例如我们编写了一个计算斐波那契数列的函数如下:
我们编写的性能比较函数如下:
运行基准测试:
这里需要注意的是,默认情况下,每个基准测试至少运行 1 秒。如果在 Benchmark 函数返回时没有到 1 秒,则 b.N 的值会按 1,2,5,10,20,50,…增加,并且函数再次运行。
最终的 BenchmarkFib40 只运行了两次,每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用-benchtime 标志增加最小基准时间,以产生更准确的结果。例如:
这一次 BenchmarkFib40 函数运行了 50 次,结果就会更准确一些了。
使用性能比较函数做测试的时候一个容易犯的错误就是把 b.N 作为输入的大小,例如以下两个例子都是错误的示范:
本文分享自华为云社区《Golang代码测试(code review)》,原文作者:PayneWu 。
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/1a3f77327f2769f6aeb4e6938】。文章转载请联系作者。
评论