先看看效果吧
当不小心点开网页文档的时候,是不是有看到过这样的展示示例代码的tab?
或者在IDE里看很多函数的简介时是不是有这样的:
有没心生敬畏,觉得很神奇?
其实这就是Golang的testing提供的一种能力,把单测代码同时作为文档,我们来看看要怎么实现吧。
可测试的Example
Example测试会被展示进package的文档,并可以如单测一样被验证。可以在godoc展示的web网页中特别展示出来。
Example本身也是测试,它会和正常的测试一起编译和执行,和它们一样放在xxx_test.go的文件中,但是它的函数签名不一样。
函数签名
Example的函数签名以Example开头,后面紧跟被示例的函数的名字,并且没有入参:
func ExampleFuncName() {
……
}
比如我现在实现一个函数 AdjacentMixString:
util.go
……
// 交互地混合两个字符串
func AdjacentMixString(str1, str2 string) string {
builder := strings.Builder{}
builder.Grow(len(str1) + len(str2))
i := 0
for ; i < len(str1) && i < len(str2); i++ {
builder.WriteByte(str1[i])
builder.WriteByte(str2[i])
}
if len(str1) > i {
builder.WriteString(str1[i:])
}
if len(str2) > i {
builder.WriteString(str2[i:])
}
return builder.String()
}
那它的示例就应该这么写:
func ExampleAdjacentMixString() {
……
}
执行测试
和普通的测试一样,你可以通过go test指令以及相关的option来运行Example测试。
我们来随便写个Example到util_test.go文件中:
……
func ExampleAdjacentMixString() {
// should output 1a2b3c
AdjacentMixString("123", "abc")
}
已经可以看到效果了:
然后来运行它:
admin@……/util$ go test -v -run=AdjacentMixString
=== RUN ExampleAdjacentMixString
--- PASS: ExampleAdjacentMixString (0.00s)
PASS
ok ……/util 1.229s
执行通过了,但好像啥也没测。
目前为止这个Example还不具备“测试”的作用,仅仅是一个能编译通过的“文档”。
Output注释
Example测试不提供Fail等方法,但是可以通过标准输出来实现简单的断言。
在执行Example测试时,测试框架会截获标准输出,并与"output:"后的注释进行比较。如果相等的话,就能通过测试。
这样,我们改造我们的Example。
func ExampleAdjacentMixString() {
fmt.Println(AdjacentMixString("", "abc"))
fmt.Println(AdjacentMixString("123", ""))
fmt.Println(AdjacentMixString("1", "abc"))
fmt.Println(AdjacentMixString("123", "a"))
fmt.Println(AdjacentMixString("123", "abcd"))
fmt.Println(AdjacentMixString("1234", "abc"))
// output:
// abc
// 123
// 1abc
// 1a23
// 1a2b3cd
// 1a2b3c4
}
再次运行。又通过了。和上面的输出几乎一致就不贴了。
我们故意改错一点,把output后的abc改成a看看是什么样的:
admin@……/util ‹release*›$ go test -v -run=AdjacentMixString
=== RUN ExampleAdjacentMixString
--- FAIL: ExampleAdjacentMixString (0.00s)
got:
abc
123
1abc
1a23
1a2b3cd
1a2b3c4
want:
a
123
1abc
1a23
1a2b3cd
1a2b3c4
FAIL
exit status 1
FAIL ……/util 1.316s
可以看到,这个Example成功发挥了一个“测试”的功能。
而文档也变成了这个样子:
Example的命名约定
Godoc使用特定的命名约定来关联Example与其对应的被示例对象。
func ExampleFoo() // 关联到Foo函数或类型
func ExampleA_Go() // 关联到A类型的Go方法
func Example() // 关联整个package
还可以通过下划线加小写字母的后缀,给同一个对象指定多个Example。比如可以这么搞:
func ExampleAdjacentMixString() {
fmt.Println(AdjacentMixString("123", "abcd"))
// output:
// 1a2b3cd
}
func ExampleAdjacentMixString_thisIsB() {
fmt.Println(AdjacentMixString("123", ""))
// output:
// 123
}
func ExampleAdjacentMixString_thisIsC() {
fmt.Println(AdjacentMixString("", "abc"))
// output:
// abc
}
这样文档中的效果就是:
整个文件作为Example
有的时候一个Example函数不足以表达整个模块的功能,这个时候可以使用整个文件作为Example。比如我给util package做一个Example文件:
util_test.go
package util
import "fmt"
type A struct {
hi string
}
func (a *A) Go() {
fmt.Println("This is a file Example")
}
func Example() {
(&A{}).Go()
// Output:
// This is a file Example
}
这样,我就可以在godoc上看到整个文件了:
同样的,你也可以建多个文件,每个文件中的Example函数用不同的下划线后缀,来为同一个package建多个示例。
unordered output
最后,如果你的输出是无法确定顺序的,可以使用unordered output来代替output:
func ExampleUnorderedOuput() {
m := map[string]string{
"a": "b",
"c": "d",
"e": "f",
}
for k, v := range m {
fmt.Println(k, v)
}
// unordered output:
// a b
// c d
// e f
}
结语
今天我们学习了golang的一个很有意思的特性,Example测试。又涨了点姿势对吧哈哈哈。