Skip to content

Go 测试

字数
1471 字
阅读时间
7 分钟

测试一般和被测代码在同一包中,测试文件命名以 _test.go 结尾,测试函数命名以 Test 开头。

测试结构

表驱动测试(Table-driven tests)

  • 结构:用切片定义测试用例,包含输入、期望输出、错误场景。
    go
    func TestDivide(t *testing.T) {
        tests := []struct {
            a, b int
            want int
            wantErr bool
        }{
            {4, 2, 2, false},
            {1, 0, 0, true},
        }
        for _, tt := range tests {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide(%d, %d) error = %v, want %v", tt.a, tt.b, err, tt.wantErr)
            }
        }
    }

子测试(Subtests)

结构

使用 t.Run() 为每个测试用例创建子测试,便于统一执行启动和清理的代码。

go
func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

避免使用 idx 来指示错误位置,而是使用 subtest 名称。

go
// BAD:
for i, d := range tests {
    if strings.ToUpper(d.input) != d.want {
        t.Errorf("Failed on case #%d", i)
    }
}

碎碎念

躺枪了,我就这样写过。但是有时候序号比名字更好找,有位置信息。

命名

清晰描述测试场景,避免特殊字符(如斜杠),支持命令行过滤。

go
func TestTranslate(t *testing.T) {
    data := []struct {
        name, desc, srcLang, dstLang, srcText, wantDstText string
    }{
        {
            name:        "hu=en_bug-1234",
            desc:        "regression test following bug 1234. contact: cleese",
            srcLang:     "hu",
            srcText:     "cigarettát és egy öngyújtót kérek",
            dstLang:     "en",
            wantDstText: "cigarettes and a lighter please",
        }, // ...
    }
    for _, d := range data {
        t.Run(d.name, func(t *testing.T) {
            got := Translate(d.srcLang, d.dstLang, d.srcText)
            if got != d.wantDstText {
                t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q",
                    d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText)
            }
        })
    }
}

执行

bash
go test -run ''          # Run 所有测试
go test -run Foo       # Run Foo 开头的测试
go test -run Foo/A=    # Run Foo 开头里面名字是 A= 开头的子测试
go test -run /A=1      # Run 所有测试里 A=1 的子测试

测试的并发

默认的 TestXXX 函数是串行执行,可以通过t.Parallel 函数让它们并发。而子测试默认是并发执行的,无需额外设置。

不同测试报告内容

性能测试(Benchmarks)

  • 结构:使用 testing.B 运行基准测试,避免使用 time.Sleep() 等阻塞操作。

  • 命名:清晰描述测试场景,避免特殊字符(如斜杠),支持命令行过滤。

    go
    func BenchmarkFoo(b *testing.B) {
        for i:= 0; i< b.N; i++ {
            Foo()  
        }
    }
  • 执行:可以用 b.N 来调整循环次数,Go 会自动调整。

    bash
    go test -bench .

覆盖率(Coverage)

  • 要求:测试覆盖率达到 80% 以上。
  • 命令go test -cover 查看覆盖率报告。

测试的最佳实践

使用 t.Errorf 而非 t.Fatalf

  • 区别t.Fatalf 会导致测试立即终止,而 t.Errorf 仅记录错误信息,测试继续执行。
  • 使用场景t.Fatalf 用于严重错误,如测试环境配置错误,t.Errorf 用于普通错误,如预期结果不匹配。

使用 t.Log

  • 只有当测试失败时,t.Log 才会输出。

使用 t.Helper

  • 场景:在测试函数中使用 t.Helper() 标记当前函数为辅助函数,辅助函数不会被测试框架视为测试函数,避免在测试函数中出现冗余的错误信息。
  • 示例
    go
    func TestFoo(t *testing.T) {
        if err:= Foo(); err!= nil {
            t.Helper()
            t.Errorf("Foo() failed: %v", err)  
        }
    }

使用 t.TempDir

  • 场景:在测试函数中使用 t.TempDir() 创建临时目录,测试完成后自动删除。
  • 示例
    go
    func TestFoo(t *testing.T) {
        dir:= t.TempDir()
        // ...
    }

使用 t.Cleanup

  • 场景:在测试函数中使用 t.Cleanup() 注册清理函数,测试完成后自动执行。 会比 defer 更可靠。
  • 示例
    go
    func TestFoo(t *testing.T) {
        t.Cleanup(func() {
            //...
        })
      }

使用 %q 代替 "%s"

  • 格式:使用 %q 格式化字符串,避免因字符串包含特殊字符(如换行符)导致的错误。
  • 示例
    go
    t.Errorf("want %q, got %q", want, got)

断言与比较

Testify

  • Testify 测试框架提供了断言和模拟。

    go
    assert.Equal(t, want, got, "msg")
  • requireassert 区别:require 会在测试失败时立即终止测试,而 assert 会继续执行测试。

  • 争议:规范建议不要使用断言库,而是使用 cmp 库进行比较,t.Errorf 输出错误信息。

    go
    if !cmp.Equal(got, want) {
        t.Errorf("result mismatch: got %v, want %v", got, want)
    }

    TIP

    尽管规范不鼓励,但我个人仍然觉得 assert 用起来显得代码简洁。

diffs、比较

  • 第三方库增强比较,输出 diffs: 对于指针字段,直接使用 == 比较会比较指针地址,而不是字段值。 虽然可以使用 reflect.DeepEqual 进行比较,但不支持定制化。 一些推荐的库:

    • go-cmp: 其中 cmp.Diff用来输出 -want +got 格式的 diff;cmp.Equal 用来比较两个值是否相等,即使指针指向不同,对比的是字段的值,支持许多定制化。
    • pretty: pretty.Compare用来输出 -want +got 格式的 diff; 可以比较美观地打印出来。
  • 第三方库增强打印: 变量包含指针字段时,直接打印会打印指针地址,而不是字段值。可以用以下库:

    • go-spew: 可以直观打印结构体、map、slice 等数据结构。
    go
    spew.Dump(got)
  • 错误的比较:使用 errors.Is 比较错误,避免字符串匹配。

mock

通过 mock 可以模拟依赖,避免依赖外部服务。

贡献者

页面历史


总访问量 次, 访客数 人次