Code is cheap, talk is expensive
不知从何时起,我就想学习如何编写 Go 测试单元。不为别的,只是想恶补一下自己之前的恶习。
怎么说自己写代码也算是有一些日子了,但是写代码的习惯不是很好。比如说,我就不喜欢写很多的注释。
再者,对于模块的测试,我也很少写测试函数。唯一做的测试就是写好之后在main
函数里面调用一下,看一下输出结果,这便是我的测试函数了吧。
但是,测试完成完成后,那些临时写的代码,基本上立马注释掉,最终难逃被优化掉的局面。
而我是什么意识到测试函数的重要性呢?
应该是看萧大的视频意思到的。
比如说,你要实现一个A+B
的函数,但是你怎么知道你的实现就是正确无误的呢?
如果是之前,我相比会采取老办法,调用printf
测试几个输出,然后就自以为没问题了。
殊不知,你的A+B
面对大数还能返回正确的结果吗?
如果别人修改了你的实现,又如何确保修改的正确性呢?
以上种种,我们都可以通过编写测试函数来实现模块的测试。
下面我想聊一聊在 Go 中如何编写测试函数呢?
Go 标准库中的 2 个用于测试的包:
testing
方便进行 Go 包的自动化单元测试、基准测试net/http/httptest
提供测试 HTTP 的工具testing
为 Go 语言 package 提供自动化测试的支持。通过 go test
命令,能够自动执行如下形式的任何函数:
func TestXXX(t *testing.T)
其实 Go 的标准库都包含许多的测试代码,感兴趣的可以好好的研究一下。
下面我们实现一个Add
函数
func Add(a,b int) int { return a + b }
测试函数
func TestAdd(t *testing.T) { var ( a = 1 b = 2 result = 3 ) if Add(a,b) != result { t.Errorf("Add(%d,%d) = %d, expected %d",a,b,Add(a,b),result) } }
测试的思想很简单,我们通过输入特定参数来比较结果是否符合预期,来判定函数是否正确无误。
但是要注意,我们的函数需要保证输出的结果于时间没有关系。
另外需要注意的是,我们的测试函数接受的参数是固定的,这个testing.T
类型,提供了测试需要使用到的函数。
但一个函数没有满足测试条件的时候,我们使用t.Errorf
来报告错误,同时错误的提示应该尽量友好,这样更方便快速定位问题所在。
此外,测试文件的文件名需要以_test.go 为结尾,这样使用go test
才可以正确执行你的测试函数。
包中的 Parallel 方法表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。
这种测试主要测试函数的并发性,这一般和共享变量的写有关系,这里就不详细介绍了。
如下形式的函数:
func BenchmarkXxx(*testing.B)
被认为是基准测试,通过 go test 命令,加上 -bench 标志来执行。多个基准测试按照顺序运行。
例如
func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("hello") } }
我们可以看到基准测试的耗时,方便我们进行瓶颈分析。
我们编写一个递归的斐波那契函数,n
变大时,函数费变得非常耗时。
func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }
func BenchmarkFib1(b *testing.B) { benchmarkFib(1, b) } func BenchmarkFib2(b *testing.B) { benchmarkFib(2, b) } func BenchmarkFib3(b *testing.B) { benchmarkFib(3, b) } func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) } func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) } func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) } func benchmarkFib(i int, b *testing.B) { for n := 0; n < b.N; n++ { Fib(i) } }
在基准测试中,我们可能需要进行一下初始化操作,同时希望不对这些操作计时。
这时可以使用testing.B
类似提供的方法,来管理计时器的行为。
StartTimer
:开始对测试进行计时。该方法会在基准测试开始时自动被调用,也可以在调用 StopTimer 之后恢复计时;StopTimer
:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;ResetTimer
:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。除此之外,还可以进行内存统计,这里省略。
testing
包除了测试,还提供了运行并验证示例的功能。
示例,一方面是文档的效果,是关于某个功能的使用例子;另一方面,可以被当做测试运行。
一个示例的例子如下:
func ExampleHello() { fmt.Println("Hello") // Output: Hello }
如果 Output: Hello
改为:Output: hello
,运行测试会失败。
一个示例函数以 Example 开头,如果示例函数包含以 "Output:" 开头的行注释,在运行测试时,go 会将示例函数的输出和 "Output:" 注释中的值做比较,就如上面的例子。
有时候,输出顺序可能不确定,比如循环输出 map 的值,那么可以使用 "Unordered output:" 开头的注释。
如果示例函数没有上述输出注释,该示例函数只会被编译而不会被运行。
Go 语言通过大量的命名约定来简化工具的复杂度,规范代码的风格。
对示例函数的命名有如下约定:
func Example() { ... }
func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }
Go从1.2开机就支持覆盖率的测试(go test ./... -cover
)
详细内容见
一般情况下,我们只需要考虑代码的实现是否正确,这个时候使用testing.T
类型,通过编写以_test.go
结尾的文件,
在文件中编写Test_XXX()
函数来进行代码验证。但是如果对测试有更多的要求,例如并发性,时间限制等,Go也提供了相应的测试方法。
但是,这些测试都没有涉及到Go最擅长的处理http请求相关的测试,其实这个也是可以编写测试函数的。
可能是编写测试需要额外的时间开支,往往就忽视这方面的内容,我打算另写一篇笔记来介绍http相关的测试。