go-test-quality

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Test Quality

Go 测试质量

Tests are production code. They run in CI on every commit, they document behavior, and they're the first thing you read when a function breaks at 3am. Write them with the same care you'd give to code that handles money.
测试代码也是生产级代码。它们会在每次提交时在CI中运行,用于记录代码行为,而且当某个函数在凌晨3点出现故障时,测试代码是你首先要查看的内容。编写测试代码时,要像编写处理资金的代码一样谨慎。

1. Test Design Philosophy

1. 测试设计理念

Test behavior, not implementation

测试行为,而非实现细节

go
// ✅ Good — tests what the function DOES
func TestTransferFunds_InsufficientBalance(t *testing.T) {
    from := NewAccount("alice", 100)
    to := NewAccount("bob", 0)

    err := TransferFunds(from, to, 150)

    require.ErrorIs(t, err, ErrInsufficientFunds)
    assert.Equal(t, 100, from.Balance(), "sender balance should be unchanged")
    assert.Equal(t, 0, to.Balance(), "receiver balance should be unchanged")
}

// ❌ Bad — tests HOW the function does it
func TestTransferFunds_InsufficientBalance(t *testing.T) {
    // asserts that debit() was called before credit()
    // asserts that rollback() was called
    // asserts internal mutex was locked
}
go
// ✅ 良好示例 — 测试函数的实际行为
func TestTransferFunds_InsufficientBalance(t *testing.T) {
    from := NewAccount("alice", 100)
    to := NewAccount("bob", 0)

    err := TransferFunds(from, to, 150)

    require.ErrorIs(t, err, ErrInsufficientFunds)
    assert.Equal(t, 100, from.Balance(), "转账发起方余额应保持不变")
    assert.Equal(t, 0, to.Balance(), "转账接收方余额应保持不变")
}

// ❌ 不良示例 — 测试函数的实现细节
func TestTransferFunds_InsufficientBalance(t *testing.T) {
    // 断言debit()在credit()之前被调用
    // 断言rollback()被调用
    // 断言内部互斥锁已锁定
}

One assertion per logical concept

每个测试验证一个逻辑概念

A test should verify one behavior. Multiple
assert
calls are fine when they verify different facets of the SAME behavior (e.g., both accounts after a transfer). But a test that checks creation AND update AND deletion is three tests pretending to be one.
一个测试应该只验证一种行为。当多个
assert
调用是为了验证同一行为的不同方面时(例如转账后两个账户的状态),是可以接受的。但如果一个测试同时检查创建、更新和删除操作,那其实是三个测试伪装成了一个。

Name tests like bug reports

测试命名要像Bug报告一样清晰

The test name should describe the scenario so clearly that when it fails, you already know what broke without reading the test body:
go
// ✅ Good — reads like a sentence
func TestOrderService_Cancel_RefundsPartiallyShippedItems(t *testing.T) { ... }
func TestParseConfig_ReturnsErrorOnMissingRequiredField(t *testing.T) { ... }
func TestRateLimiter_AllowsBurstAfterCooldown(t *testing.T) { ... }

// ❌ Bad — says nothing useful
func TestCancel(t *testing.T) { ... }
func TestParseConfig2(t *testing.T) { ... }
func TestRateLimiter_Success(t *testing.T) { ... }
测试名称应清晰描述场景,这样当测试失败时,你无需查看测试代码就能知道哪里出了问题:
go
// ✅ 良好示例 — 名称像一句话一样易懂
func TestOrderService_Cancel_RefundsPartiallyShippedItems(t *testing.T) { ... }
func TestParseConfig_ReturnsErrorOnMissingRequiredField(t *testing.T) { ... }
func TestRateLimiter_AllowsBurstAfterCooldown(t *testing.T) { ... }

// ❌ 不良示例 — 没有任何有用信息
func TestCancel(t *testing.T) { ... }
func TestParseConfig2(t *testing.T) { ... }
func TestRateLimiter_Success(t *testing.T) { ... }

2. Subtests for Organized Scenarios

2. 用子测试组织相关场景

Use
t.Run
to group related scenarios under a parent test. Each subtest gets its own setup, its own failure, and its own name in the output:
go
func TestUserService_Create(t *testing.T) {
    svc := setupUserService(t)

    t.Run("succeeds with valid input", func(t *testing.T) {
        user, err := svc.Create(ctx, CreateUserInput{
            Name:  "Alice",
            Email: "alice@example.com",
        })

        require.NoError(t, err)
        assert.NotEmpty(t, user.ID)
        assert.Equal(t, "Alice", user.Name)
    })

    t.Run("rejects duplicate email", func(t *testing.T) {
        _, _ = svc.Create(ctx, CreateUserInput{
            Name: "Alice", Email: "taken@example.com",
        })

        _, err := svc.Create(ctx, CreateUserInput{
            Name: "Bob", Email: "taken@example.com",
        })

        require.ErrorIs(t, err, ErrDuplicateEmail)
    })

    t.Run("rejects empty name", func(t *testing.T) {
        _, err := svc.Create(ctx, CreateUserInput{
            Name: "", Email: "valid@example.com",
        })

        var valErr *ValidationError
        require.ErrorAs(t, err, &valErr)
        assert.Equal(t, "name", valErr.Field)
    })
}
Each subtest is independent, readable, and debuggable. When
rejects duplicate email
fails, you see exactly that name in CI output — not
TestUserService_Create/case_3
.
使用
t.Run
将相关场景分组到父测试下。每个子测试都有自己的初始化逻辑、失败提示和输出名称:
go
func TestUserService_Create(t *testing.T) {
    svc := setupUserService(t)

    t.Run("合法输入创建成功", func(t *testing.T) {
        user, err := svc.Create(ctx, CreateUserInput{
            Name:  "Alice",
            Email: "alice@example.com",
        })

        require.NoError(t, err)
        assert.NotEmpty(t, user.ID)
        assert.Equal(t, "Alice", user.Name)
    })

    t.Run("拒绝重复邮箱", func(t *testing.T) {
        _, _ = svc.Create(ctx, CreateUserInput{
            Name: "Alice", Email: "taken@example.com",
        })

        _, err := svc.Create(ctx, CreateUserInput{
            Name: "Bob", Email: "taken@example.com",
        })

        require.ErrorIs(t, err, ErrDuplicateEmail)
    })

    t.Run("拒绝空名称", func(t *testing.T) {
        _, err := svc.Create(ctx, CreateUserInput{
            Name: "", Email: "valid@example.com",
        })

        var valErr *ValidationError
        require.ErrorAs(t, err, &valErr)
        assert.Equal(t, "name", valErr.Field)
    })
}
每个子测试都是独立、易读且易于调试的。当"拒绝重复邮箱"测试失败时,你会在CI输出中看到确切的名称,而不是
TestUserService_Create/case_3

3. Test Helpers Done Right

3. 正确编写测试辅助函数

Always call
t.Helper()

务必调用
t.Helper()

This makes failure messages point to the caller, not the helper:
go
func createTestUser(t *testing.T, svc *UserService, name string) *User {
    t.Helper()
    user, err := svc.Create(context.Background(), CreateUserInput{
        Name:  name,
        Email: name + "@test.com",
    })
    require.NoError(t, err)
    return user
}
这会让错误信息指向调用辅助函数的位置,而非辅助函数内部:
go
func createTestUser(t *testing.T, svc *UserService, name string) *User {
    t.Helper()
    user, err := svc.Create(context.Background(), CreateUserInput{
        Name:  name,
        Email: name + "@test.com",
    })
    require.NoError(t, err)
    return user
}

Factory functions with functional options

带函数式选项的工厂函数

For complex test objects, avoid a constructor with 15 parameters. Use defaults with overrides:
go
func newTestOrder(t *testing.T, opts ...func(*Order)) *Order {
    t.Helper()
    o := &Order{
        ID:        uuid.New(),
        UserID:    uuid.New(),
        Status:    OrderStatusPending,
        Total:     9999, // $99.99
        CreatedAt: time.Now(),
    }
    for _, opt := range opts {
        opt(o)
    }
    return o
}

// Usage — only override what matters for THIS test
func TestOrder_Cancel_RejectsShippedOrders(t *testing.T) {
    order := newTestOrder(t, func(o *Order) {
        o.Status = OrderStatusShipped
    })

    err := order.Cancel()
    require.ErrorIs(t, err, ErrCannotCancelShipped)
}
对于复杂的测试对象,避免使用有15个参数的构造函数。使用默认值加覆盖选项的方式:
go
func newTestOrder(t *testing.T, opts ...func(*Order)) *Order {
    t.Helper()
    o := &Order{
        ID:        uuid.New(),
        UserID:    uuid.New(),
        Status:    OrderStatusPending,
        Total:     9999, // $99.99
        CreatedAt: time.Now(),
    }
    for _, opt := range opts {
        opt(o)
    }
    return o
}

// 使用方式 — 仅覆盖当前测试需要的参数
func TestOrder_Cancel_RejectsShippedOrders(t *testing.T) {
    order := newTestOrder(t, func(o *Order) {
        o.Status = OrderStatusShipped
    })

    err := order.Cancel()
    require.ErrorIs(t, err, ErrCannotCancelShipped)
}

Cleanup with
t.Cleanup

t.Cleanup
做资源清理

Prefer
t.Cleanup
over
defer
— it runs even if the test calls
t.FailNow()
, and it's scoped to the test, not the function:
go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("postgres", testDSN)
    require.NoError(t, err)

    t.Cleanup(func() {
        db.Close()
    })
    return db
}
优先使用
t.Cleanup
而非
defer
——即使测试调用
t.FailNow()
,它也会执行,而且它的作用域是测试而非函数:
go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("postgres", testDSN)
    require.NoError(t, err)

    t.Cleanup(func() {
        db.Close()
    })
    return db
}

4. Golden File Testing

4. 黄金文件测试

For complex outputs (JSON responses, HTML, SQL queries, protobuf), comparing against golden files is more maintainable than inline assertions:
go
var update = flag.Bool("update", false, "update golden files")

func TestRenderInvoice(t *testing.T) {
    invoice := buildTestInvoice()

    got, err := RenderInvoice(invoice)
    require.NoError(t, err)

    golden := filepath.Join("testdata", t.Name()+".golden")

    if *update {
        // Run: go test -update  to regenerate golden files
        require.NoError(t, os.WriteFile(golden, got, 0644))
    }

    want, err := os.ReadFile(golden)
    require.NoError(t, err)
    assert.Equal(t, string(want), string(got))
}
Golden files live in
testdata/
directories (which
go build
ignores). Commit them to git — they ARE the expected output. Review diffs in PRs.
对于复杂输出(JSON响应、HTML、SQL查询、protobuf),与黄金文件对比比内联断言更易于维护:
go
var update = flag.Bool("update", false, "更新黄金文件")

func TestRenderInvoice(t *testing.T) {
    invoice := buildTestInvoice()

    got, err := RenderInvoice(invoice)
    require.NoError(t, err)

    golden := filepath.Join("testdata", t.Name()+".golden")

    if *update {
        // 运行:go test -update 以重新生成黄金文件
        require.NoError(t, os.WriteFile(golden, got, 0644))
    }

    want, err := os.ReadFile(golden)
    require.NoError(t, err)
    assert.Equal(t, string(want), string(got))
}
黄金文件存放在
testdata/
目录中(
go build
会忽略该目录)。将它们提交到git——它们就是预期输出。在PR中要审查黄金文件的差异。

5. HTTP Handler Testing with
httptest

5. 用
httptest
测试HTTP处理器

Use
httptest.NewRecorder
for unit-style handler tests:
go
func TestUserHandler_GetByID(t *testing.T) {
    store := &mockUserStore{
        getByIDFunc: func(ctx context.Context, id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }
    handler := NewUserHandler(store, zap.NewNop())

    t.Run("returns user as JSON", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
        req.SetPathValue("id", "123")

        rec := httptest.NewRecorder()
        handler.HandleGet(rec, req)

        assert.Equal(t, http.StatusOK, rec.Code)
        assert.Contains(t, rec.Header().Get("Content-Type"), "application/json")

        var body map[string]string
        require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
        assert.Equal(t, "Alice", body["name"])
    })

    t.Run("returns 404 for unknown user", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users/unknown", nil)
        req.SetPathValue("id", "unknown")

        rec := httptest.NewRecorder()
        handler.HandleGet(rec, req)

        assert.Equal(t, http.StatusNotFound, rec.Code)
    })
}
使用
httptest.NewRecorder
进行单元级的处理器测试:
go
func TestUserHandler_GetByID(t *testing.T) {
    store := &mockUserStore{
        getByIDFunc: func(ctx context.Context, id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }
    handler := NewUserHandler(store, zap.NewNop())

    t.Run("返回用户JSON数据", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
        req.SetPathValue("id", "123")

        rec := httptest.NewRecorder()
        handler.HandleGet(rec, req)

        assert.Equal(t, http.StatusOK, rec.Code)
        assert.Contains(t, rec.Header().Get("Content-Type"), "application/json")

        var body map[string]string
        require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
        assert.Equal(t, "Alice", body["name"])
    })

    t.Run("未知用户返回404", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users/unknown", nil)
        req.SetPathValue("id", "unknown")

        rec := httptest.NewRecorder()
        handler.HandleGet(rec, req)

        assert.Equal(t, http.StatusNotFound, rec.Code)
    })
}

Full server test with
httptest.NewServer
:

httptest.NewServer
进行完整服务器测试:

go
func TestAPI_CreateUser_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    app := setupApp(t)
    srv := httptest.NewServer(app.Router())
    t.Cleanup(srv.Close)

    resp, err := http.Post(srv.URL+"/api/v1/users",
        "application/json",
        strings.NewReader(`{"name":"Alice","email":"alice@test.com"}`))
    require.NoError(t, err)
    defer resp.Body.Close()

    assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
go
func TestAPI_CreateUser_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("跳过集成测试")
    }

    app := setupApp(t)
    srv := httptest.NewServer(app.Router())
    t.Cleanup(srv.Close)

    resp, err := http.Post(srv.URL+"/api/v1/users",
        "application/json",
        strings.NewReader(`{"name":"Alice","email":"alice@test.com"}`))
    require.NoError(t, err)
    defer resp.Body.Close()

    assert.Equal(t, http.StatusCreated, resp.StatusCode)
}

6. Mocking Strategies

6. 模拟策略

Interface-based mocks (preferred for ≤3 methods):

基于接口的模拟(方法数≤3时首选):

go
type mockNotifier struct {
    sendFunc func(ctx context.Context, to, msg string) error
    sent     []string
}

func (m *mockNotifier) Send(ctx context.Context, to, msg string) error {
    m.sent = append(m.sent, to)
    if m.sendFunc != nil {
        return m.sendFunc(ctx, to, msg)
    }
    return nil
}
go
type mockNotifier struct {
    sendFunc func(ctx context.Context, to, msg string) error
    sent     []string
}

func (m *mockNotifier) Send(ctx context.Context, to, msg string) error {
    m.sent = append(m.sent, to)
    if m.sendFunc != nil {
        return m.sendFunc(ctx, to, msg)
    }
    return nil
}

Function injection for simple seams:

针对简单接缝的函数注入:

go
type Service struct {
    now    func() time.Time
    randID func() string
}

// Production: svc := &Service{now: time.Now, randID: uuid.NewString}
// Test:       svc := &Service{now: fixedTime, randID: func() string { return "abc" }}
go
type Service struct {
    now    func() time.Time
    randID func() string
}

// 生产环境:svc := &Service{now: time.Now, randID: uuid.NewString}
// 测试环境:svc := &Service{now: fixedTime, randID: func() string { return "abc" }}

What NOT to mock:

不要模拟这些内容:

  • Value objects and pure functions — just call them
  • The standard library — test the real
    json.Marshal
    , not a mock
  • Your own code in the same package — test the real thing
  • 值对象和纯函数——直接调用它们即可
  • 标准库——测试真实的
    json.Marshal
    ,而非模拟版本
  • 同一包内的自有代码——测试真实实现

7. Integration Tests with Testcontainers

7. 用Testcontainers做集成测试

go
func TestPostgresUserStore(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    ctx := context.Background()
    pg, err := postgres.Run(ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second),
        ),
    )
    require.NoError(t, err)
    t.Cleanup(func() { pg.Terminate(ctx) })

    connStr, err := pg.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    store, err := NewPostgresStore(connStr)
    require.NoError(t, err)

    t.Run("create and retrieve user", func(t *testing.T) {
        created, err := store.Create(ctx, &User{Name: "Alice"})
        require.NoError(t, err)

        fetched, err := store.GetByID(ctx, created.ID)
        require.NoError(t, err)
        assert.Equal(t, "Alice", fetched.Name)
    })
}
Separate with build tags:
//go:build integration
Run with:
go test -tags=integration -count=1 ./...
go
func TestPostgresUserStore(t *testing.T) {
    if testing.Short() {
        t.Skip("跳过集成测试")
    }

    ctx := context.Background()
    pg, err := postgres.Run(ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second),
        ),
    )
    require.NoError(t, err)
    t.Cleanup(func() { pg.Terminate(ctx) })

    connStr, err := pg.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    store, err := NewPostgresStore(connStr)
    require.NoError(t, err)

    t.Run("创建并查询用户", func(t *testing.T) {
        created, err := store.Create(ctx, &User{Name: "Alice"})
        require.NoError(t, err)

        fetched, err := store.GetByID(ctx, created.ID)
        require.NoError(t, err)
        assert.Equal(t, "Alice", fetched.Name)
    })
}
用构建标签区分:
//go:build integration
运行命令:
go test -tags=integration -count=1 ./...

8. Fuzz Testing (Go 1.18+)

8. 模糊测试(Go 1.18+)

Fuzz tests discover edge cases you'd never think of. Use for parsers, validators, serializers — anything that takes arbitrary input:
go
func FuzzParseEmail(f *testing.F) {
    f.Add("alice@example.com")
    f.Add("")
    f.Add("@")

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseEmail(input)
        if err != nil {
            return // invalid input is fine, just don't panic
        }

        // Round-trip: parsing the output should give the same result
        reparsed, err := ParseEmail(result.String())
        require.NoError(t, err)
        assert.Equal(t, result, reparsed)
    })
}
Run with:
go test -fuzz=FuzzParseEmail -fuzztime=30s
If your function can receive untrusted input, fuzz it.
模糊测试能发现你永远想不到的边缘情况。适用于解析器、验证器、序列化器——任何接收任意输入的组件:
go
func FuzzParseEmail(f *testing.F) {
    f.Add("alice@example.com")
    f.Add("")
    f.Add("@")

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseEmail(input)
        if err != nil {
            return // 无效输入是可接受的,只要不触发panic即可
        }

        // 往返测试:解析输出结果应得到相同的结果
        reparsed, err := ParseEmail(result.String())
        require.NoError(t, err)
        assert.Equal(t, result, reparsed)
    })
}
运行命令:
go test -fuzz=FuzzParseEmail -fuzztime=30s
如果你的函数会接收不可信输入,一定要进行模糊测试。

9. Parallel Tests

9. 并行测试

go
func TestSlugify(t *testing.T) {
    t.Parallel()

    t.Run("lowercases input", func(t *testing.T) {
        t.Parallel()
        assert.Equal(t, "hello-world", Slugify("Hello World"))
    })

    t.Run("strips special characters", func(t *testing.T) {
        t.Parallel()
        assert.Equal(t, "caf", Slugify("café!"))
    })
}
Do NOT use
t.Parallel()
when tests share mutable state, databases, files, or process-level state (
os.Setenv
).
go
func TestSlugify(t *testing.T) {
    t.Parallel()

    t.Run("将输入转为小写", func(t *testing.T) {
        t.Parallel()
        assert.Equal(t, "hello-world", Slugify("Hello World"))
    })

    t.Run("移除特殊字符", func(t *testing.T) {
        t.Parallel()
        assert.Equal(t, "caf", Slugify("café!"))
    })
}
当测试共享可变状态、数据库、文件或进程级状态(如
os.Setenv
)时,请勿使用
t.Parallel()

10. TestMain for Shared Setup

10. 用TestMain做共享初始化

Use when ALL tests in a package need expensive one-time setup:
go
var testDB *sql.DB

func TestMain(m *testing.M) {
    var teardown func()
    testDB, teardown = setupTestDatabase()

    code := m.Run()

    teardown()
    os.Exit(code)
}
Use sparingly — most tests don't need it.
当包内所有测试都需要昂贵的一次性初始化时使用:
go
var testDB *sql.DB

func TestMain(m *testing.M) {
    var teardown func()
    testDB, teardown = setupTestDatabase()

    code := m.Run()

    teardown()
    os.Exit(code)
}
谨慎使用——大多数测试不需要这个。

11. Coverage

11. 测试覆盖率

bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out
Targets: business logic 80%+, critical paths (auth, payments) 95%+, handlers 70%+. Don't chase 100% on generated code and simple getters.
bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out
目标: 业务逻辑覆盖率≥80%,关键路径(认证、支付)≥95%,处理器≥70%。不要为生成的代码和简单的getter追求100%覆盖率。

Anti-Patterns

反模式

  • 🔴 Test with no assertions — always passes, proves nothing
  • 🔴
    time.Sleep
    for synchronization — use channels or polling
  • 🔴 Test depends on execution order — each test must stand alone
  • 🔴 Mocking everything — you end up testing your mocks, not your code
  • 🟡 Test names like
    Test1
    ,
    TestSuccess
    — name the scenario
  • 🟡 Reaching into private fields — test through the public API
  • 🟡 No edge cases: empty, nil, zero, max values, unicode
  • 🟡 Giant shared setup — each test should set up only what it needs
  • 🟢 Fuzz anything that takes untrusted input
  • 🟢 Golden files for complex output comparisons
  • 🔴 没有断言的测试——永远通过,毫无意义
  • 🔴 用
    time.Sleep
    做同步——使用通道或轮询
  • 🔴 测试依赖执行顺序——每个测试必须独立运行
  • 🔴 模拟所有内容——最终你测试的是模拟对象,而非真实代码
  • 🟡 像
    Test1
    TestSuccess
    这样的测试名称——要命名具体场景
  • 🟡 直接访问私有字段——通过公共API进行测试
  • 🟡 未覆盖边缘情况:空值、nil、零值、最大值、Unicode字符
  • 🟡 庞大的共享初始化——每个测试只应初始化自己需要的内容
  • 🟢 对所有接收不可信输入的组件进行模糊测试
  • 🟢 用黄金文件对比复杂输出

Verification Checklist

验证清单

  1. Every test has meaningful assertions (no empty test bodies)
  2. Test names describe the scenario, not the method
  3. t.Helper()
    called in every test utility function
  4. t.Cleanup()
    used for resource teardown
  5. t.Parallel()
    used where safe, avoided where not
  6. Integration tests guarded with
    testing.Short()
    or build tags
  7. Mocks are minimal — only mock external dependencies
  8. Edge cases covered: empty, nil, zero, boundary values
  9. go test -race ./...
    passes
  10. Coverage is meaningful, not just high numbers
  1. 每个测试都有有意义的断言(无空测试体)
  2. 测试名称描述场景,而非方法
  3. 每个测试工具函数都调用了
    t.Helper()
  4. t.Cleanup()
    做资源清理
  5. 在安全的场景使用
    t.Parallel()
    ,不安全的场景避免使用
  6. 集成测试用
    testing.Short()
    或构建标签做防护
  7. 模拟是最小化的——只模拟外部依赖
  8. 覆盖边缘情况:空值、nil、零值、边界值
  9. go test -race ./...
    执行通过
  10. 覆盖率有实际意义,不只是数字高