go 示例测试

testing 包除了测试,还提供了运行并验证示例的功能。示例,一方面是文档的效果,是关于某个功能的使用例子;另一方面,可以被当做测试运行。

一个示例的例子如下:

1
2
3
4
func ExampleHello() {
fmt.Println("Hello")
// Output: Hello
}

如果 Output: Hello 改为:Output: hello,运行测试会失败,提示:

1
2
3
4
got:
Hello
want:
hello

一个示例函数以 Example 开头,如果示例函数包含以 “Output” 开头的行注释,在运行测试时,go 会将示例函数的输出和 “Output” 注释中的值做比较,就如上面的例子。

有时候,输出顺序可能不确定,比如循环输出 map 的值,那么可以使用 “Unordered output” 开头的注释。

如果示例函数没有 “Output” 注释,该示例函数只会被编译而不会被运行。

命名约定

Go 语言通过大量的命名约定来简化工具的复杂度,规范代码的风格。对示例函数的命名有如下约定:

  • 包级别的示例函数,直接命名为 func Example() { ... }
  • 函数 F 的示例,命名为 func ExampleF() { ... }
  • 类型 T 的示例,命名为 func ExampleT() { ... }
  • 类型 T 上的 方法 M 的示例,命名为 func ExampleT_M() { ... }

有时,我们想要给 包/类型/函数/方法 提供多个示例,可以通过在示例函数名称后附加一个不同的后缀来实现,但这种后缀必须以小写字母开头,如:

1
2
3
4
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

通常,示例代码会放在单独的示例文件中,命名为 example_test.go。可以查看 io 包中的 example_test.go 了解示例的编写。

实现原理

本节开头提到了示例的两个作用,它们分别是由 godocgo test 这两个命令实现的。

在执行 go test 时,会运行示例。具体的实现原理,可以通过阅读 go test 命令源码和 testing 包中 example.go 文件了解。

源代码文件

源代码文件example.go中包含SayHello()SayGoodbye()PrintNames()三个方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package gotest
import "fmt"
// SayHello 打印一行字符串
func SayHello() {
fmt.Println("Hello World")
}
// SayGoodbye 打印两行字符串
func SayGoodbye() {
fmt.Println("Hello,")
fmt.Println("goodbye")
}
// PrintNames 打印学生姓名
func PrintNames() {
students := make(map[int]string, 4)
students[1] = "Jim"
students[2] = "Bob"
students[3] = "Tom"
students[4] = "Sue"
for _, value := range students {
fmt.Println(value)
}
}

这几个方法打印内容略有不同,分别代表一种典型的场景:

  • SayHello():只有一行打印输出
  • SayGoodbye():有两行打印输出
  • PrintNames():有多行打印输出,且由于Map数据结构的原因,多行打印次序是随机的。

测试文件

测试文件example_test.go中包含3个测试方法,于源代码文件中的3个方法一一对应,测试文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package gotest_test
import "gotest"
// 检测单行输出
func ExampleSayHello() {
gotest.SayHello()
// OutPut: Hello World
}
// 检测多行输出
func ExampleSayGoodbye() {
gotest.SayGoodbye()
// OutPut:
// Hello,
// goodbye
}
// 检测乱序输出
func ExamplePrintNames() {
gotest.PrintNames()
// Unordered output:
// Jim
// Bob
// Tom
// Sue
}

例子测试函数命名规则为”Examplexxx”,其中”xxx”为自定义的标识,通常为待测函数名称。

这三个测试函数分别代表三种场景:

  • ExampleSayHello(): 待测试函数只有一行输出,使用”// OutPut: “检测。
  • ExampleSayGoodbye():待测试函数有多行输出,使用”// OutPut: “检测,其中期望值也是多行。
  • ExamplePrintNames():待测试函数有多行输出,但输出次序不确定,使用”// Unordered output:”检测。

注:字符串比较时会忽略前后的空白字符。

执行测试

命令行下,使用go testgo test example_test.go命令即可启动测试,如下所示:

1
2
E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test example_test.go
ok command-line-arguments 0.331s

总结

  1. 例子测试函数名需要以”Example”开头;
  2. 检测单行输出格式为“// Output: <期望字符串>”;
  3. 检测多行输出格式为“// Output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一行;
  4. 检测无序输出格式为”// Unordered output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一行;
  5. 测试字符串时会自动忽略字符串前后的空白字符;
  6. 如果测试函数中没有“Output”标识,则该测试函数不会被执行;
  7. 执行测试可以使用go test,此时该目录下的其他测试文件也会一并执行;
  8. 执行测试可以使用go test <xxx_test.go>,此时仅执行特定文件中的测试函数;