测试框架
详细测试用法参见 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_*.go | Mock 文件 | 由 mockgen 生成 |