不知从何时起,我就想学习如何编写 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相关的测试。

Last modification:October 12th, 2020 at 04:40 pm
要饭啦~