CtrlK
BlogDocsLog inGet started
Tessl Logo

golang-testing

保证测试驱动开发与 Go 代码高质量的全面测试策略。

65

Quality

80%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./docs/ja-JP/skills/golang-testing/SKILL.md
SKILL.md
Quality
Evals
Security

Go 测试 (Go Testing)

保证测试驱动开发(TDD)与 Go 代码高质量的全面测试策略。

何时激活

  • 编写新的 Go 代码时
  • 审查(Review)Go 代码时
  • 改进现有测试时
  • 提高测试覆盖率(Test Coverage)时
  • 调试与修复 Bug 时

核心原则

1. 测试驱动开发(TDD)工作流

遵循编写失败测试、实现代码、重构(Refactor)的循环。

// 1. 编写测试(失败)
func TestCalculateTotal(t *testing.T) {
    total := CalculateTotal([]float64{10.0, 20.0, 30.0})
    want := 60.0
    if total != want {
        t.Errorf("got %f, want %f", total, want)
    }
}

// 2. 实现代码(使测试通过)
func CalculateTotal(prices []float64) float64 {
    var total float64
    for _, price := range prices {
        total += price
    }
    return total
}

// 3. 重构
// 在不破坏测试的前提下改进代码

2. 表格驱动测试 (Table-Driven Tests)

体系化地测试多个用例。

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"mixed signs", -2, 3, 1},
        {"zeros", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

3. 子测试 (Subtests)

使用子测试组织逻辑测试。

func TestUser(t *testing.T) {
    t.Run("validation", func(t *testing.T) {
        t.Run("empty email", func(t *testing.T) {
            user := User{Email: ""}
            if err := user.Validate(); err == nil {
                t.Error("expected validation error")
            }
        })

        t.Run("valid email", func(t *testing.T) {
            user := User{Email: "test@example.com"}
            if err := user.Validate(); err != nil {
                t.Errorf("unexpected error: %v", err)
            }
        })
    })

    t.Run("serialization", func(t *testing.T) {
        // 另一个测试组
    })
}

测试组织

文件结构 (File Structure)

mypackage/
├── user.go
├── user_test.go          # 单元测试
├── integration_test.go   # 集成测试
├── testdata/             # 测试固件 (Test Fixtures)
│   ├── valid_user.json
│   └── invalid_user.json
└── export_test.go        # 为内部测试提供的私有导出

测试包 (Test Packages)

// user_test.go - 同一包内(白盒测试)
package user

func TestInternalFunction(t *testing.T) {
    // 可以测试内部逻辑
}

// user_external_test.go - 外部包(黑盒测试)
package user_test

import "myapp/user"

func TestPublicAPI(t *testing.T) {
    // 仅测试公开 API
}

断言(Assertions)与辅助函数(Helpers)

基本断言

func TestBasicAssertions(t *testing.T) {
    // 相等性
    got := Calculate()
    want := 42
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }

    // 错误检查
    _, err := Process()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // nil 检查
    result := GetResult()
    if result == nil {
        t.Fatal("expected non-nil result")
    }
}

自定义辅助函数

// 标记为 Helper(不会在堆栈跟踪中显示)
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

// 使用示例
func TestWithHelpers(t *testing.T) {
    result, err := Process()
    assertNoError(t, err)
    assertEqual(t, result.Status, "success")
}

深度一致性检查(Deep Equality Check)

import "reflect"

func assertDeepEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %+v, want %+v", got, want)
    }
}

func TestStructEquality(t *testing.T) {
    got := User{Name: "Alice", Age: 30}
    want := User{Name: "Alice", Age: 30}
    assertDeepEqual(t, got, want)
}

模拟(Mocking)与桩(Stubs)

基于接口的模拟

// 生产代码
type UserStore interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

type UserService struct {
    store UserStore
}

// 测试代码
type MockUserStore struct {
    users map[string]*User
    err   error
}

func (m *MockUserStore) GetUser(id string) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.users[id], nil
}

func (m *MockUserStore) SaveUser(user *User) error {
    if m.err != nil {
        return m.err
    }
    m.users[user.ID] = user
    return nil
}

// 测试
func TestUserService(t *testing.T) {
    mock := &MockUserStore{
        users: make(map[string]*User),
    }
    service := &UserService{store: mock}

    // 测试服务...
}

时间模拟

// 生产代码 - 使时间可注入
type TimeProvider interface {
    Now() time.Time
}

type RealTime struct{}

func (RealTime) Now() time.Time {
    return time.Now()
}

type Service struct {
    time TimeProvider
}

// 测试代码
type MockTime struct {
    current time.Time
}

func (m MockTime) Now() time.Time {
    return m.current
}

func TestTimeDependent(t *testing.T) {
    mockTime := MockTime{
        current: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
    }
    service := &Service{time: mockTime}

    // 在固定时间内测试...
}

HTTP 客户端模拟

type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
}

type MockHTTPClient struct {
    response *http.Response
    err      error
}

func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
    return m.response, m.err
}

func TestAPICall(t *testing.T) {
    mockClient := &MockHTTPClient{
        response: &http.Response{
            StatusCode: 200,
            Body:       io.NopCloser(strings.NewReader(`{"status":"ok"}`)),
        },
    }

    api := &APIClient{client: mockClient}
    // 测试 API 客户端...
}

测试 HTTP 处理程序(Handlers)

使用 httptest

func TestHandler(t *testing.T) {
    handler := http.HandlerFunc(MyHandler)

    req := httptest.NewRequest("GET", "/users/123", nil)
    rec := httptest.NewRecorder()

    handler.ServeHTTP(rec, req)

    // 检查状态码
    if rec.Code != http.StatusOK {
        t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
    }

    // 检查响应体
    var response map[string]interface{}
    if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
        t.Fatalf("failed to decode response: %v", err)
    }

    if response["id"] != "123" {
        t.Errorf("got id %v, want 123", response["id"])
    }
}

测试中间件(Middleware)

func TestAuthMiddleware(t *testing.T) {
    // 虚拟处理程序
    nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    // 用中间件包装
    handler := AuthMiddleware(nextHandler)

    tests := []struct {
        name       string
        token      string
        wantStatus int
    }{
        {"valid token", "valid-token", http.StatusOK},
        {"invalid token", "invalid", http.StatusUnauthorized},
        {"no token", "", http.StatusUnauthorized},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", "/", nil)
            if tt.token != "" {
                req.Header.Set("Authorization", "Bearer "+tt.token)
            }
            rec := httptest.NewRecorder()

            handler.ServeHTTP(rec, req)

            if rec.Code != tt.wantStatus {
                t.Errorf("got status %d, want %d", rec.Code, tt.wantStatus)
            }
        })
    }
}

测试服务器

func TestAPIIntegration(t *testing.T) {
    // 创建测试服务器
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{
            "message": "hello",
        })
    }))
    defer server.Close()

    // 执行真实的 HTTP 请求
    resp, err := http.Get(server.URL)
    if err != nil {
        t.Fatalf("request failed: %v", err)
    }
    defer resp.Body.Close()

    // 验证响应
    var result map[string]string
    json.NewDecoder(resp.Body).Decode(&result)

    if result["message"] != "hello" {
        t.Errorf("got %s, want hello", result["message"])
    }
}

数据库测试

使用事务实现测试隔离

func TestUserRepository(t *testing.T) {
    db := setupTestDB(t)
    defer db.Close()

    tests := []struct {
        name string
        fn   func(*testing.T, *sql.DB)
    }{
        {"create user", testCreateUser},
        {"find user", testFindUser},
        {"update user", testUpdateUser},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            tx, err := db.Begin()
            if err != nil {
                t.Fatal(err)
            }
            defer tx.Rollback() // 测试后回滚

            tt.fn(t, tx)
        })
    }
}

测试固件 (Test Fixtures)

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()

    db, err := sql.Open("postgres", "postgres://localhost/test")
    if err != nil {
        t.Fatalf("failed to connect: %v", err)
    }

    // 迁移架构
    if err := runMigrations(db); err != nil {
        t.Fatalf("migrations failed: %v", err)
    }

    return db
}

func seedTestData(t *testing.T, db *sql.DB) {
    t.Helper()

    fixtures := []string{
        `INSERT INTO users (id, email) VALUES ('1', 'test@example.com')`,
        `INSERT INTO posts (id, user_id, title) VALUES ('1', '1', 'Test Post')`,
    }

    for _, query := range fixtures {
        if _, err := db.Exec(query); err != nil {
            t.Fatalf("failed to seed data: %v", err)
        }
    }
}

基准测试(Benchmarks)

基本基准测试

func BenchmarkCalculation(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Calculate(100)
    }
}

// 报告内存分配情况
func BenchmarkWithAllocs(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        ProcessData([]byte("test data"))
    }
}

子基准测试

func BenchmarkEncoding(b *testing.B) {
    data := generateTestData()

    b.Run("json", func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            json.Marshal(data)
        }
    })

    b.Run("gob", func(b *testing.B) {
        b.ReportAllocs()
        var buf bytes.Buffer
        enc := gob.NewEncoder(&buf)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            enc.Encode(data)
            buf.Reset()
        }
    })
}

基准测试对比

// 运行: go test -bench=. -benchmem
func BenchmarkStringConcat(b *testing.B) {
    b.Run("operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "hello" + " " + "world"
        }
    })

    b.Run("fmt.Sprintf", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = fmt.Sprintf("%s %s", "hello", "world")
        }
    })

    b.Run("strings.Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            sb.WriteString("hello")
            sb.WriteString(" ")
            sb.WriteString("world")
            _ = sb.String()
        }
    })
}

模糊测试(Fuzzing Tests)

基本模糊测试(Go 1.18+)

func FuzzParseInput(f *testing.F) {
    // 种子语料库
    f.Add("hello")
    f.Add("world")
    f.Add("123")

    f.Fuzz(func(t *testing.T, input string) {
        // 确保解析不会引发 panic
        result, err := ParseInput(input)

        // 确保即使有错误,结果不为 nil 或具有一致性
        if err == nil && result == nil {
            t.Error("got nil result with no error")
        }
    })
}

更复杂的模糊测试

func FuzzJSONParsing(f *testing.F) {
    f.Add([]byte(`{"name":"test","age":30}`))
    f.Add([]byte(`{"name":"","age":0}`))

    f.Fuzz(func(t *testing.T, data []byte) {
        var user User
        err := json.Unmarshal(data, &user)

        // 如果 JSON 解码成功,理应可以再次编码
        if err == nil {
            _, err := json.Marshal(user)
            if err != nil {
                t.Errorf("marshal failed after successful unmarshal: %v", err)
            }
        }
    })
}

测试覆盖率(Test Coverage)

运行并查看覆盖率

# 运行测试并生成 HTML 报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# 显示每个包的覆盖率
go test -cover ./...

# 详细覆盖率
go test -coverprofile=coverage.out -covermode=atomic ./...

覆盖率最佳实践

// Good: 易于测试的代码
func ProcessData(data []byte) (Result, error) {
    if len(data) == 0 {
        return Result{}, ErrEmptyData
    }

    // 每个分支均可测试
    if isValid(data) {
        return parseValid(data)
    }
    return parseInvalid(data)
}

// 对应的测试涵盖所有分支
func TestProcessData(t *testing.T) {
    tests := []struct {
        name    string
        data    []byte
        wantErr bool
    }{
        {"empty data", []byte{}, true},
        {"valid data", []byte("valid"), false},
        {"invalid data", []byte("invalid"), false},
    }
    // ...
}

集成测试(Integration Tests)

使用构建标签(Build Tags)

//go:build integration
// +build integration

package myapp_test

import "testing"

func TestDatabaseIntegration(t *testing.T) {
    // 需要真实数据库的测试
}
# 运行集成测试
go test -tags=integration ./...

# 排除集成测试
go test ./...

使用测试容器(Testcontainers)

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

func setupPostgres(t *testing.T) *sql.DB {
    ctx := context.Background()

    req := testcontainers.ContainerRequest{
        Image:        "postgres:15",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_PASSWORD": "test",
            "POSTGRES_DB":       "testdb",
        },
    }

    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatal(err)
    }

    t.Cleanup(func() {
        container.Terminate(ctx)
    })

    // 连接到容器
    // ...
    return db
}

测试并行化(Parallelizing Tests)

并行测试

func TestParallel(t *testing.T) {
    tests := []struct {
        name string
        fn   func(*testing.T)
    }{
        {"test1", testCase1},
        {"test2", testCase2},
        {"test3", testCase3},
    }

    for _, tt := range tests {
        tt := tt // 捕获循环变量
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // 并行执行此测试
            tt.fn(t)
        })
    }
}

控制并行执行

func TestWithResourceLimit(t *testing.T) {
    // 同时仅运行 5 个测试
    sem := make(chan struct{}, 5)

    tests := generateManyTests()

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()

            sem <- struct{}{}        // 获取
            defer func() { <-sem }() // 释放

            tt.fn(t)
        })
    }
}

Go 工具集成

测试命令

# 基本测试
go test ./...
go test -v ./...                    # 详细输出
go test -run TestSpecific ./...     # 运行特定测试

# 覆盖率
go test -cover ./...
go test -coverprofile=coverage.out ./...

# 竞态检测 (Race Condition)
go test -race ./...

# 基准测试
go test -bench=. ./...
go test -bench=. -benchmem ./...
go test -bench=. -cpuprofile=cpu.prof ./...

# 模糊测试
go test -fuzz=FuzzTest

# 集成测试
go test -tags=integration ./...

# JSON 格式(用于 CI 集成)
go test -json ./...

测试配置

# 测试超时
go test -timeout 30s ./...

# 短时间测试(跳过耗时测试)
go test -short ./...

# 清除构建缓存
go clean -testcache
go test ./...

最佳实践

DRY(Don't Repeat Yourself)原则

// Good: 通过表格驱动测试减少重复
func TestValidation(t *testing.T) {
    tests := []struct {
        input string
        valid bool
    }{
        {"valid@email.com", true},
        {"invalid-email", false},
        {"", false},
    }

    for _, tt := range tests {
        t.Run(tt.input, func(t *testing.T) {
            err := Validate(tt.input)
            if (err == nil) != tt.valid {
                t.Errorf("Validate(%q) error = %v, want valid = %v",
                    tt.input, err, tt.valid)
            }
        })
    }
}

测试数据分离

// Good: 将测试数据存放在 testdata/ 目录中
func TestLoadConfig(t *testing.T) {
    data, err := os.ReadFile("testdata/config.json")
    if err != nil {
        t.Fatal(err)
    }

    config, err := ParseConfig(data)
    // ...
}

使用清理函数(Cleanup)

func TestWithCleanup(t *testing.T) {
    // 设置资源
    file, err := os.CreateTemp("", "test")
    if err != nil {
        t.Fatal(err)
    }

    // 注册清理操作(类似于 defer,但在子测试中也有效)
    t.Cleanup(func() {
        os.Remove(file.Name())
    })

    // 继续测试...
}

明确错误信息

// Bad: 错误信息不明确
if result != expected {
    t.Error("wrong result")
}

// Good: 带有上下文的错误信息
if result != expected {
    t.Errorf("Calculate(%d) = %d; want %d", input, result, expected)
}

// Better: 使用辅助函数
assertEqual(t, result, expected, "Calculate(%d)", input)

应避免的反模式(Anti-patterns)

// Bad: 依赖外部状态
func TestBadDependency(t *testing.T) {
    result := GetUserFromDatabase("123") // 使用真实数据库
    // 测试脆弱且缓慢
}

// Good: 注入依赖
func TestGoodDependency(t *testing.T) {
    mockDB := &MockDatabase{
        users: map[string]User{"123": {ID: "123"}},
    }
    result := GetUser(mockDB, "123")
}

// Bad: 在测试间共享状态
var sharedCounter int

func TestShared1(t *testing.T) {
    sharedCounter++
    // 依赖测试顺序
}

// Good: 每个测试保持独立
func TestIndependent(t *testing.T) {
    counter := 0
    counter++
    // 不影响其他测试
}

// Bad: 忽略错误
func TestIgnoreError(t *testing.T) {
    result, _ := Process()
    if result != expected {
        t.Error("wrong result")
    }
}

// Good: 检查错误
func TestCheckError(t *testing.T) {
    result, err := Process()
    if err != nil {
        t.Fatalf("Process() error = %v", err)
    }
    if result != expected {
        t.Errorf("got %v, want %v", result, expected)
    }
}

快速参考

命令/模式目的
go test ./...运行所有测试
go test -v详细输出
go test -cover覆盖率报告
go test -race检测竞态条件
go test -bench=.运行基准测试
t.Run()子测试
t.Helper()测试辅助函数标识
t.Parallel()并行执行测试
t.Cleanup()注册清理操作
testdata/测试固件目录
-short跳过耗时测试
-tags=integration通过构建标签运行测试

请记住:优秀的测试应该是快速、可靠、可维护且清晰的。追求清晰度而非复杂性。

Repository
xu-xiang/everything-claude-code-zh
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.