让 Go 文档里的示例也接受测试

原文:Andrew Gerrand,Testable Examples in Go,2015 年 5 月 7 日,Go 官方博客。本文为中文翻译与技术整理;补充的边界说明已明确标注。

文档中的代码示例往往是读者接触一个包的第一步。如果示例能够和包的测试一起编译、运行,就更容易发现 API 变化造成的失效。Go 的 example 函数正是为此设计:它既可作为包文档中的用法示例,也可成为测试套件的一部分。在支持运行示例的文档页面,读者还可以编辑并运行代码。

标准库大量采用这种形式,例如 strings 包。本文依次介绍示例如何进入测试、输出注释的意义、名称与文档的对应关系,以及需要额外类型和方法时如何组织完整示例。

Go 示例从 test 文件进入编译,再根据是否有 Output 注释决定执行比较,并作为包文档展示的流程图
示例同时服务文档与测试。图由未完纪依据本文内容绘制,不是运行截图。

示例也是测试

与普通测试一样,示例写在以 _test.go 结尾的文件中。与以 Test 开头并接收测试参数的函数不同,示例函数以 Example 开头,不接收参数,也没有返回值。

原文使用 Go 示例仓库里的 reverse 包。下面的 ExampleString 演示它的 String 函数,可放在 reverse 目录下的 example_test.go 中:

package reverse_test

import (
    "fmt"
    "golang.org/x/example/hello/reverse"
)

func ExampleString() {
    fmt.Println(reverse.String("hello"))
    // Output: olleh
}

reverse_test 是外部测试包名:示例通过导入包来使用公开接口,接近真正调用者的使用方式。包文档服务器 pkg.go.dev 会把这一示例放在 String 的文档旁边。原文展示的文档页面插图说明了这种关联;这里用自绘流程图说明机制,没有将历史页面伪装成当前界面截图。

原作者在包目录运行 go test -v 时,输出中既有普通的 TestString,也有 ExampleString:

$ go test -v
=== RUN   TestString
--- PASS: TestString (0.00s)
=== RUN   ExampleString
--- PASS: ExampleString (0.00s)
PASS
ok      golang.org/x/example/hello/reverse  0.209s

结果归属:以上是原文记录,用来说明测试工具会发现示例函数;不是本次编辑的执行结果,耗时也不是性能承诺。

Output 注释决定比较什么

示例“通过”的含义很具体。测试框架执行示例,收集写到标准输出的数据,再与 Output: 注释中的内容比较。两者符合预期,示例才通过。这不是对所有副作用、异常分支或说明文字的全面验证。

把正确的输出注释故意改成另一段文本:

func ExampleString() {
    fmt.Println(reverse.String("hello"))
    // Output: golly
}

原文再次运行测试,出现以下失败记录。got 是实际输出,want 是注释声明的期望值:

$ go test
--- FAIL: ExampleString (0.00s)
got:
olleh
want:
golly
FAIL

如果完全去掉输出注释:

func ExampleString() {
    fmt.Println(reverse.String("hello"))
}

这个函数仍随包测试一起编译,但不会作为示例测试执行。原文对应的详细测试输出只列出 TestString,没有 ExampleString。这种写法适合展示不适合在单元测试里直接运行的代码,例如需要访问网络的用法。它能帮助发现编译层面的错误,却不能证明运行结果正确。

编者补充:不带输出注释的示例并不是运行任意仓库测试的安全沙箱。go test 仍可能执行包初始化、普通测试和测试辅助代码;不应在带有个人文件、凭证或生产访问权限的环境中运行未审查的项目。本文未下载依赖或执行任何 Go 命令。

函数名称决定示例归到哪里

Go 文档工具依照命名约定,将示例关联到某个包级标识符:

func ExampleFoo()     // 说明 Foo 函数或类型
func ExampleBar_Qux() // 说明 Bar 类型的 Qux 方法
func Example()        // 说明整个包

因此,ExampleString 会显示在 String 的文档旁。同一个标识符可以有多个示例:在名称后加下划线,再接一个以小写字母开头的后缀。

func ExampleString()
func ExampleString_second()
func ExampleString_third()

这些声明用于说明命名规则,并不是可以独立编译的完整源码。实际文件中的每个函数都需要函数体。后缀用于区分展示场景,不需要把多个用法硬塞进一个越来越长的函数。

需要辅助类型时,使用整文件示例

有些用法仅靠一个函数体说不清楚。例如,演示 sort.Interface 需要定义一个类型,并在类型上实现方法。Go 不能在函数体中声明方法,因此文档还需要展示示例外部的上下文。

“整文件示例”满足三个条件:文件以 _test.go 结尾;文件中恰好有一个示例函数,没有普通测试函数和 benchmark 函数;此外至少包含一个其他包级声明。文档工具便会展示整个文件。原文的排序示例如下,中文注释是译文:

package sort_test

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s: %d", p.Name, p.Age)
}

// ByAge 根据 Age 字段为 []Person 实现 sort.Interface。
type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

func Example() {
    people := []Person{
        {"Bob", 31},
        {"John", 42},
        {"Michael", 17},
        {"Jenny", 26},
    }

    fmt.Println(people)
    sort.Sort(ByAge(people))
    fmt.Println(people)

    // Output:
    // [Bob: 31 John: 42 Michael: 17 Jenny: 26]
    // [Michael: 17 Jenny: 26 Bob: 31 John: 42]
}

Person.String 决定元素的输出格式;ByAge 的三个方法告诉排序函数元素数量、交换方式和“小于”的判定。示例在排序前后各打印一次,让文档读者直接看到变化,而输出注释使这份预期可以被测试框架检查。

同一个包可以有多个整文件示例,只需各放在独立文件中。原文指向 sort 包源码,可以进一步观察实际组织方式。

让可执行文档保持可信

原文的核心建议是把示例作为可维护的文档资产:说明 API 的代码与测试同步演进,读者也可以以它为起点继续修改。对于一篇发表于 2015 年的文章,需要保留历史背景:文档服务器与运行按钮的具体界面会变化,本文核对的是源站当前保留的文章与示例规则。

编者补充:示例的预期输出应尽量确定。系统时间、随机数、网络响应和没有稳定顺序的结果,都可能令注释与实际输出偶然不一致。应在示例设计时控制这些因素,而不是把一次测试通过当作永久正确的保证。可执行示例能帮助发现代码过时,但不会自动核对全部叙述、平台差异和业务边界。

署名与许可:Andrew Gerrand / The Go Authors。原站版权说明规定,除另有注明外,文字采用 CC BY 4.0,代码采用 BSD 许可证。本译稿保留来源并标明翻译、中文注释和编者补充;代码完整许可证保留在本文下方。

不含 Output 的原文测试输出

以下为原文2015年示例记录,不是本次运行。

$ go test -v
=== RUN   TestString
--- PASS: TestString (0.00s)
PASS
ok      golang.org/x/example/hello/reverse  0.110s

Go 代码 BSD 许可全文

Copyright 2009 The Go Authors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.
* Neither the name of Google LLC nor the names of its contributors may be used
  to endorse or promote products derived from this software without specific
  prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

Source: https://go.dev/LICENSE
Article prose: CC BY 4.0, https://go.dev/copyright
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容