Install
1
Install the plugin$
npx claudepluginhub xu-xiang/everything-claude-code-zhWant just this skill?
Add to a custom plugin, then install with one command.
Description
Go 测试模式,包括表格驱动测试、子测试、基准测试、模糊测试和测试覆盖率。遵循具有惯用 Go 实践的 TDD 方法论。
Tool Access
This skill uses the workspace's default tool permissions.
Skill Content
Go 测试模式 (Testing Patterns)
遵循测试驱动开发(TDD)方法论,编写可靠且易于维护的 Go 测试的全面模式。
何时激活
- 编写新的 Go 函数或方法时
- 为现有代码增加测试覆盖率时
- 为性能关键型代码创建基准测试(Benchmarks)时
- 为输入验证实现模糊测试(Fuzz tests)时
- 在 Go 项目中遵循 TDD 工作流时
Go 的 TDD 工作流
红-绿-重构 (RED-GREEN-REFACTOR) 循环
RED → 先编写一个失败的测试
GREEN → 编写最少的代码使测试通过
REFACTOR → 在保持测试通过的同时改进代码
REPEAT → 继续处理下一个需求
Go 中的分步 TDD
// 步骤 1:定义接口/签名
// calculator.go
package calculator
func Add(a, b int) int {
panic("not implemented") // 占位符
}
// 步骤 2:编写失败的测试 (RED)
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}
// 步骤 3:运行测试 - 验证失败 (FAIL)
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: not implemented
// 步骤 4:实现最少代码 (GREEN)
func Add(a, b int) int {
return a + b
}
// 步骤 5:运行测试 - 验证通过 (PASS)
// $ go test
// PASS
// 步骤 6:如果需要则进行重构,并验证测试仍然通过
表格驱动测试 (Table-Driven Tests)
Go 测试的标准模式。能够以最少的代码实现全面的覆盖。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero values", 0, 0, 0},
{"mixed signs", -1, 1, 0},
{"large numbers", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.expected)
}
})
}
}
带错误情况的表格驱动测试
func TestParseConfig(t *testing.T) {
tests := []struct {
name string
input string
want *Config
wantErr bool
}{
{
name: "valid config",
input: `{"host": "localhost", "port": 8080}`,
want: &Config{Host: "localhost", Port: 8080},
},
{
name: "invalid JSON",
input: `{invalid}`,
wantErr: true,
},
{
name: "empty input",
input: "",
wantErr: true,
},
{
name: "minimal config",
input: `{}`,
want: &Config{}, // 零值配置
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseConfig(tt.input)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %+v; want %+v", got, tt.want)
}
})
}
}
子测试与子基准测试 (Subtests and Sub-benchmarks)
组织相关测试
func TestUser(t *testing.T) {
// 所有子测试共享的设置
db := setupTestDB(t)
t.Run("Create", func(t *testing.T) {
user := &User{Name: "Alice"}
err := db.CreateUser(user)
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
if user.ID == "" {
t.Error("expected user ID to be set")
}
})
t.Run("Get", func(t *testing.T) {
user, err := db.GetUser("alice-id")
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %q; want %q", user.Name, "Alice")
}
})
t.Run("Update", func(t *testing.T) {
// ...
})
t.Run("Delete", func(t *testing.T) {
// ...
})
}
并行子测试
func TestParallel(t *testing.T) {
tests := []struct {
name string
input string
}{
{"case1", "input1"},
{"case2", "input2"},
{"case3", "input3"},
}
for _, tt := range tests {
tt := tt // 捕获循环变量
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 并行运行子测试
result := Process(tt.input)
// 断言...
_ = result
})
}
}
测试助手 (Test Helpers)
助手函数
func setupTestDB(t *testing.T) *sql.DB {
t.Helper() // 将此函数标记为测试助手函数
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
// 测试结束时清理
t.Cleanup(func() {
db.Close()
})
// 运行迁移
if _, err := db.Exec(schema); err != nil {
t.Fatalf("failed to create schema: %v", err)
}
return db
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
临时文件与目录
func TestFileProcessing(t *testing.T) {
// 创建临时目录 - 自动清理
tmpDir := t.TempDir()
// 创建测试文件
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0644)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
// 运行测试
result, err := ProcessFile(testFile)
if err != nil {
t.Fatalf("ProcessFile failed: %v", err)
}
// 断言...
_ = result
}
Golden Files (对比文件测试)
针对存储在 testdata/ 中的预期输出文件进行测试。
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
tests := []struct {
name string
input Template
}{
{"simple", Template{Name: "test"}},
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Render(tt.input)
golden := filepath.Join("testdata", tt.name+".golden")
if *update {
// 更新 golden file: go test -update
err := os.WriteFile(golden, got, 0644)
if err != nil {
t.Fatalf("failed to update golden file: %v", err)
}
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatalf("failed to read golden file: %v", err)
}
if !bytes.Equal(got, want) {
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
}
})
}
}
使用接口进行 Mock (Mocking with Interfaces)
基于接口的 Mock
// 为依赖定义接口
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
// 生产环境实现
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
// 真实的数据库查询
}
// 用于测试的 Mock 实现
type MockUserRepository struct {
GetUserFunc func(id string) (*User, error)
SaveUserFunc func(user *User) error
}
func (m *MockUserRepository) GetUser(id string) (*User, error) {
return m.GetUserFunc(id)
}
func (m *MockUserRepository) SaveUser(user *User) error {
return m.SaveUserFunc(user)
}
// 使用 mock 进行测试
func TestUserService(t *testing.T) {
mock := &MockUserRepository{
GetUserFunc: func(id string) (*User, error) {
if id == "123" {
return &User{ID: "123", Name: "Alice"}, nil
}
return nil, ErrNotFound
},
}
service := NewUserService(mock)
user, err := service.GetUserProfile("123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %q; want %q", user.Name, "Alice")
}
}
基准测试 (Benchmarks)
基础基准测试
func BenchmarkProcess(b *testing.B) {
data := generateTestData(1000)
b.ResetTimer() // 不要计算准备时间
for i := 0; i < b.N; i++ {
Process(data)
}
}
// 运行:go test -bench=BenchmarkProcess -benchmem
// 输出:BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
不同规模的基准测试
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000, 100000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := generateRandomSlice(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 创建副本以避免对已排序的数据进行排序
tmp := make([]int, len(data))
copy(tmp, data)
sort.Ints(tmp)
}
})
}
}
内存分配基准测试
func BenchmarkStringConcat(b *testing.B) {
parts := []string{"hello", "world", "foo", "bar", "baz"}
b.Run("plus", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for _, p := range parts {
s += p
}
_ = s
}
})
b.Run("builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, p := range parts {
sb.WriteString(p)
}
_ = sb.String()
}
})
b.Run("join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(parts, "")
}
})
}
模糊测试 (Fuzzing) (Go 1.18+)
基础模糊测试
func FuzzParseJSON(f *testing.F) {
// 添加种子语料库
f.Add(`{"name": "test"}`)
f.Add(`{"count": 123}`)
f.Add(`[]`)
f.Add(`""`)
f.Fuzz(func(t *testing.T, input string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(input), &result)
if err != nil {
// 对于随机输入,预期的结果是无效的 JSON
return
}
// 如果解析成功,重新编码应该也有效
_, err = json.Marshal(result)
if err != nil {
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
}
})
}
// 运行:go test -fuzz=FuzzParseJSON -fuzztime=30s
多输入模糊测试
func FuzzCompare(f *testing.F) {
f.Add("hello", "world")
f.Add("", "")
f.Add("abc", "abc")
f.Fuzz(func(t *testing.T, a, b string) {
result := Compare(a, b)
// 属性:Compare(a, a) 应该始终等于 0
if a == b && result != 0 {
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
}
// 属性:Compare(a, b) 和 Compare(b, a) 应该符号相反
reverse := Compare(b, a)
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
if result != 0 || reverse != 0 {
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
a, b, result, b, a, reverse)
}
}
})
}
测试覆盖率 (Test Coverage)
运行覆盖率检查
# 基础覆盖率
go test -cover ./...
# 生成覆盖率配置文件
go test -coverprofile=coverage.out ./...
# 在浏览器中查看覆盖率
go tool cover -html=coverage.out
# 按函数查看覆盖率
go tool cover -func=coverage.out
# 带有竞态检测的覆盖率
go test -race -coverprofile=coverage.out ./...
覆盖率目标
| 代码类型 | 目标 |
|---|---|
| 关键业务逻辑 | 100% |
| 公共 API | 90%+ |
| 通用代码 | 80%+ |
| 生成的代码 | 排除 |
从覆盖率中排除生成的代码
//go:generate mockgen -source=interface.go -destination=mock_interface.go
// 在覆盖率配置文件中,使用构建标签排除:
// go test -cover -tags=!generate ./...
HTTP Handler 测试
func TestHealthHandler(t *testing.T) {
// 创建请求
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
// 调用 handler
HealthHandler(w, req)
// 检查响应
resp := w.Result()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != "OK" {
t.Errorf("got body %q; want %q", body, "OK")
}
}
func TestAPIHandler(t *testing.T) {
tests := []struct {
name string
method string
path string
body string
wantStatus int
wantBody string
}{
{
name: "get user",
method: http.MethodGet,
path: "/users/123",
wantStatus: http.StatusOK,
wantBody: `{"id":"123","name":"Alice"}`,
},
{
name: "not found",
method: http.MethodGet,
path: "/users/999",
wantStatus: http.StatusNotFound,
},
{
name: "create user",
method: http.MethodPost,
path: "/users",
body: `{"name":"Bob"}`,
wantStatus: http.StatusCreated,
},
}
handler := NewAPIHandler()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.body != "" {
body = strings.NewReader(tt.body)
}
req := httptest.NewRequest(tt.method, tt.path, body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != tt.wantStatus {
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
}
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
}
})
}
}
测试命令
# 运行所有测试
go test ./...
# 运行测试并输出详细信息
go test -v ./...
# 运行特定测试
go test -run TestAdd ./...
# 运行匹配模式的测试
go test -run "TestUser/Create" ./...
# 运行带有竞态检测器的测试
go test -race ./...
# 运行带有覆盖率检查的测试
go test -cover -coverprofile=coverage.out ./...
# 仅运行短测试
go test -short ./...
# 运行带有超时的测试
go test -timeout 30s ./...
# 运行基准测试
go test -bench=. -benchmem ./...
# 运行模糊测试
go test -fuzz=FuzzParse -fuzztime=30s ./...
# 统计测试运行次数(用于检测不稳定的测试)
go test -count=10 ./...
最佳实践
建议 (DO):
- 先编写测试 (TDD)
- 使用表格驱动测试实现全面覆盖
- 测试行为,而非实现
- 在助手函数中使用
t.Helper() - 为相互独立的测试使用
t.Parallel() - 使用
t.Cleanup()清理资源 - 使用描述场景的有意义的测试名称
避免 (DON'T):
- 直接测试私有函数(应通过公共 API 进行测试)
- 在测试中使用
time.Sleep()(应使用通道或条件) - 忽视不稳定的测试(应修复或移除它们)
- Mock 所有内容(尽可能优先使用集成测试)
- 跳过错误路径测试
CI/CD 集成
# GitHub Actions 示例
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run tests
run: go test -race -coverprofile=coverage.out ./...
- name: Check coverage
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
awk -F'%' '{if ($1 < 80) exit 1}'
记住:测试即文档。它们展示了代码应该如何被使用。请清晰地编写测试并保持更新。
Stats
Stars75
Forks15
Last CommitMar 5, 2026
Actions