Dreamer2q Blog
见到,不如不见
Dreamer2q

Code is cheap, talk is expensive

64日志

Go编写测试函数

创建于 2020-11-27 共 2053 字,阅读约 8 分钟 更新于 20-12-10 10:30
浏览 11评论 0

不知从何时起,我就想学习如何编写 Go 测试单元。不为别的,只是想恶补一下自己之前的恶习。


怎么说自己写代码也算是有一些日子了,但是写代码的习惯不是很好。比如说,我就不喜欢写很多的注释。


再者,对于模块的测试,我也很少写测试函数。唯一做的测试就是写好之后在main函数里面调用一下,看一下输出结果,这便是我的测试函数了吧。


但是,测试完成完成后,那些临时写的代码,基本上立马注释掉,最终难逃被优化掉的局面。


而我是什么意识到测试函数的重要性呢?


应该是看萧大的视频意思到的。


比如说,你要实现一个A+B的函数,但是你怎么知道你的实现就是正确无误的呢?


如果是之前,我相比会采取老办法,调用printf测试几个输出,然后就自以为没问题了。


殊不知,你的A+B面对大数还能返回正确的结果吗?


如果别人修改了你的实现,又如何确保修改的正确性呢?


以上种种,我们都可以通过编写测试函数来实现模块的测试。


下面我想聊一聊在 Go 中如何编写测试函数呢?


Go Test


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 方法表示当前测试只会与其他带有 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() { ... }
  • 函数 F 的示例,命名为 func ExampleF() { ... }
  • 类型 T 的示例,命名为 func ExampleT() { ... }
  • 类型 T 上的 方法 M 的示例,命名为 func ExampleT_M() { ... }


测试覆盖率


Go从1.2开机就支持覆盖率的测试(go test ./... -cover)


详细内容见



总结


一般情况下,我们只需要考虑代码的实现是否正确,这个时候使用testing.T类型,通过编写以_test.go结尾的文件,


在文件中编写Test_XXX()函数来进行代码验证。但是如果对测试有更多的要求,例如并发性,时间限制等,Go也提供了相应的测试方法。


但是,这些测试都没有涉及到Go最擅长的处理http请求相关的测试,其实这个也是可以编写测试函数的。


可能是编写测试需要额外的时间开支,往往就忽视这方面的内容,我打算另写一篇笔记来介绍http相关的测试。