文章

测试框架

测试框架

详细测试用法参见 01-工程化/04-测试体系,本篇补充标准库 testing 包的完整 API 参考。

testing 包 API

testing.T 方法

func TestSomething(t *testing.T) {
    // 失败
    t.Fail()           // 标记失败但继续执行
    t.FailNow()        // 标记失败并立即停止

    // 输出 + 失败
    t.Error("msg")         // 相当于 Log + Fail
    t.Errorf("msg: %d", v) // 相当于 Logf + Fail
    t.Fatal("msg")         // 相当于 Log + FailNow
    t.Fatalf("msg: %d", v) // 相当于 Logf + FailNow

    // 跳过
    t.Skip("reason")       // 跳过当前测试
    t.Skipf("skip: %s", r)
    t.SkipNow()            // 立即跳过

    // 日志
    t.Log("message")
    t.Logf("format: %d", v)

    // 子测试
    t.Run("subtest_name", func(t *testing.T) { ... })

    // 并行
    t.Parallel()  // 标记当前测试可并行执行

    // 超时(Go 1.17+)
    t.Deadline()  // 返回测试截止时间

    // 临时目录
    dir := t.TempDir()  // 自动清理的临时目录

    // 清理
    t.Cleanup(func() { /* 测试结束后执行 */ })

    // 辅助函数标记
    t.Helper()  // 标记为辅助函数,错误报告调用者位置

    // 名称
    t.Name()  // 返回测试名称

    // 状态检查
    t.Failed()  // 是否已失败
    t.Skipped() // 是否已跳过
}

testing.B 方法

func BenchmarkSomething(b *testing.B) {
    // 准备(不计入计时)
    data := prepareData()
    b.ResetTimer()

    // 运行 b.N 次
    for i := 0; i < b.N; i++ {
        process(data)
    }

    // 停止/恢复计时
    b.StopTimer()
    // 做不计入基准的工作
    b.StartTimer()

    // 报告自定义指标
    b.ReportMetric(float64(ops), "ops")
    b.SetBytes(1024)  // 报告吞吐量 MB/s

    // 内存报告
    b.ReportAllocs()  // 报告每次操作的内存分配

    // 并行基准
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            process()
        }
    })

    // 子基准
    b.Run("variant1", func(b *testing.B) { ... })
    b.Run("variant2", func(b *testing.B) { ... })
}

testing.F 方法(模糊测试)

func FuzzParse(f *testing.F) {
    // 添加种子语料
    f.Add("hello")
    f.Add("123")
    f.Add("")

    // 模糊测试函数
    f.Fuzz(func(t *testing.T, input string) {
        // 尝试解析
        result, err := Parse(input)
        if err != nil {
            return  // 解析失败是正常的
        }

        // 往返测试
        reformatted := Format(result)
        result2, err := Parse(reformatted)
        if err != nil {
            t.Fatalf("re-parse failed: %v", err)
        }
        if result != result2 {
            t.Errorf("roundtrip mismatch")
        }
    })
}

testing.M(全局测试控制)

func TestMain(m *testing.M) {
    // 全局 setup
    setupTestDB()
    setupTestRedis()

    // 运行测试
    code := m.Run()

    // 全局 teardown
    teardownTestDB()
    teardownTestRedis()

    os.Exit(code)
}

testing.TB 接口

// testing.TB 是 T 和 B 的公共接口
// 可以在通用辅助函数中使用
func assertEqual(t testing.TB, got, want interface{}) {
    t.Helper()
    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v, want %v", got, want)
    }
}

testing/iotest(IO 测试)

import "testing/iotest"

// 测试 Reader 的错误处理
r := iotest.TimeoutReader(strings.NewReader("hello"))
// 读取一次后返回 Error

r := iotest.OneByteReader(strings.NewReader("hello"))
// 每次只读 1 字节

r := iotest.HalfReader(strings.NewReader("hello"))
// 每次只读一半

r := iotest.DataErrReader(strings.NewReader("hello"))
// 读取结束后返回自定义错误

testing/fstest(文件系统测试)

import "testing/fstest"

// 测试自定义 fs.FS 实现
func TestMyFS(t *testing.T) {
    myFS := NewMyFS()
    err := fstest.TestFS(myFS, "file1.txt", "dir/file2.txt")
    if err != nil {
        t.Fatal(err)
    }
}

testing/slogtest(slog Handler 测试)

import "testing/slogtest"

func TestMyHandler(t *testing.T) {
    var buf bytes.Buffer
    handler := NewMyHandler(&buf)
    err := slogtest.TestHandler(handler, func() []map[string]any {
        return parseLogEntries(buf.String())
    })
    if err != nil {
        t.Fatal(err)
    }
}

第三方测试库速查

testify

import "github.com/stretchr/testify/assert"
import "github.com/stretchr/testify/require"
import "github.com/stretchr/testify/mock"
import "github.com/stretchr/testify/suite"

// assert:断言失败继续执行
assert.Equal(t, 42, result)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Contains(t, "hello world", "world")
assert.ElementsMatch(t, []int{1,2,3}, result)

// require:断言失败立即停止
require.NoError(t, err)  // 失败后不继续

// mock
type MockRepo struct {
    mock.Mock
}
func (m *MockRepo) GetByID(ctx context.Context, id string) (*User, error) {
    args := m.Called(ctx, id)
    return args.Get(0).(*User), args.Error(1)
}

// suite
type UserTestSuite struct {
    suite.Suite
    svc *UserService
}
func (s *UserTestSuite) SetupTest() {
    s.svc = NewUserService(mockRepo)
}
func (s *UserTestSuite) TestGetUser() {
    // ...
}
func TestUserSuite(t *testing.T) {
    suite.Run(t, new(UserTestSuite))
}

assert vs require

失败行为适用场景
assert标记失败,继续执行想看到所有失败
require标记失败,立即停止后续测试依赖前置条件

testcontainers-go

import "github.com/testcontainers/testcontainers-go"

func TestWithPostgres(t *testing.T) {
    ctx := context.Background()
    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    require.NoError(t, err)
    defer pgContainer.Terminate(ctx)

    connStr, _ := pgContainer.ConnectionString(ctx)
    db, _ := gorm.Open(postgres.Open(connStr), &gorm.Config{})
    // 使用真实 PostgreSQL 测试
}

go-sqlmock

import "github.com/DATA-DOG/go-sqlmock"

func TestUserRepo(t *testing.T) {
    db, mock, _ := sqlmock.New()
    defer db.Close()

    // 预期 SQL 查询
    rows := sqlmock.NewRows([]string{"id", "name"}).
        AddRow(1, "小徐").
        AddRow(2, "小王")
    mock.ExpectQuery("SELECT id, name FROM users").
        WillReturnRows(rows)

    // 执行
    repo := NewUserRepo(db)
    users, err := repo.List(context.Background())

    // 断言
    assert.NoError(t, err)
    assert.Len(t, users, 2)
    assert.NoError(t, mock.ExpectationsWereMet())
}

测试目录组织

project/
├── internal/
│   └── service/
│       ├── user_service.go
│       ├── user_service_test.go          # 单元测试(与源码同目录)
│       ├── user_service_bench_test.go    # 基准测试
│       └── mocks/
│           └── mock_user_repo.go         # Mock 文件
├── test/
│   ├── integration/                      # 集成测试
│   │   ├── user_api_test.go
│   │   └── order_flow_test.go
│   ├── e2e/                             # E2E 测试
│   │   └── full_flow_test.go
│   └── testdata/                        # 测试数据
│       ├── fixtures/
│       └── golden/
├── go.mod
└── Makefile

测试文件命名约定

文件名用途命令
*_test.go测试文件go test
*_bench_test.go基准测试go test -bench
*_fuzz_test.go模糊测试go test -fuzz
*_integration_test.go集成测试go test -tags=integration
testdata/测试数据目录自动被 go tool 识别
mock_*.goMock 文件由 mockgen 生成