Files
ai-proj-helper/skills-dev/dev-test-plugin/skills/dev-test/go-testing.md
John Qiu 712063071c refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4),
skills-integration(8), skills-biz(4), skills-workflow(7)

generate-marketplace.py 改为自动扫描所有 skills-* 目录。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:31:58 +10:30

4.9 KiB
Raw Blame History

Go 后端测试

测试框架

  • testify: 断言和套件
  • httptest: HTTP 测试
  • gomock: Mock 生成(仅用于 handler 层)

⚠️ Biz 层测试规则:禁止使用 Mock

Biz/Service 层测试必须使用真实 PostgreSQL test DB不允许使用 mock store。

Mock store 只是在测试你的 mock 实现,无法验证真实的 SQL 行为、事务、FK 约束等。

测试方式 原因
model/store test DB (PostgreSQL) 验证真实 SQL/ORM 行为
biz/service test DB (PostgreSQL) + 真实 store 验证业务逻辑 + 真实数据交互
handler mock biz + httptest 只测 HTTP 路由和参数绑定
// ✅ 正确 — biz 层使用真实 test DB + 真实 store
func setupBiz(t *testing.T) (*SomeBiz, *gorm.DB) {
    db := newTestDB(t)
    s := store.NewSomeStore(db)
    biz := NewSomeBiz(s)
    t.Cleanup(func() {
        db.Exec("DELETE FROM some_table WHERE tenant_id = ?", testTenantID)
    })
    return biz, db
}

// ❌ 错误 — biz 层使用 mock store等于没测
mockStore := store.NewMockIStore(ctrl)
mockStore.EXPECT().Get(gomock.Any(), id).Return(fakeData, nil)
biz := NewSomeBiz(mockStore)

testdb_test.go 模板

package biz

import (
    "os"
    "testing"

    "github.com/stretchr/testify/require"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

const testTenantID int64 = 99

func newTestDB(t *testing.T, models ...interface{}) *gorm.DB {
    t.Helper()
    dsn := os.Getenv("TEST_DATABASE_DSN")
    if dsn == "" {
        dsn = "host=localhost user=coolbuy-dev dbname=coolbuy_paas_test sslmode=disable"
    }
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    require.NoError(t, err)
    require.NoError(t, db.AutoMigrate(models...))
    return db
}

运行测试

# 所有测试
go test ./...
make test

# 带覆盖率
make cover
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# 特定包
go test -v ./internal/twms/biz/...

# 特定函数
go test -v -run TestFunctionName ./...

Biz 层单元测试模板(真实 DB

package biz

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"

    "project/internal/user/model"
    "project/internal/user/store"
)

func setupUserBiz(t *testing.T) (*UserBiz, *gorm.DB) {
    db := newTestDB(t, &model.User{})
    s := store.NewUserStore(db)
    biz := NewUserBiz(s)
    t.Cleanup(func() {
        db.Exec("DELETE FROM users WHERE tenant_id = ?", testTenantID)
    })
    return biz, db
}

func createTestUser(t *testing.T, db *gorm.DB, username string) *model.User {
    t.Helper()
    user := &model.User{TenantID: testTenantID, Username: username, Email: username + "@test.com"}
    require.NoError(t, db.Create(user).Error)
    return user
}

func TestUserBiz_Get(t *testing.T) {
    biz, db := setupUserBiz(t)
    user := createTestUser(t, db, "john")

    result, err := biz.Get(context.Background(), user.ID)

    assert.NoError(t, err)
    assert.Equal(t, "john", result.Username)
}

func TestUserBiz_Get_NotFound(t *testing.T) {
    biz, _ := setupUserBiz(t)

    _, err := biz.Get(context.Background(), 99999)

    assert.Error(t, err)
}

表驱动测试

func TestValidateUsername(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        wantErr  bool
    }{
        {"valid", "john_doe", false},
        {"too_short", "ab", true},
        {"too_long", strings.Repeat("a", 65), true},
        {"special_chars", "user@name", true},
        {"empty", "", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUsername(tt.input)
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

HTTP Handler 测试

func TestUserController_List(t *testing.T) {
    gin.SetMode(gin.TestMode)

    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockBiz := biz.NewMockIBiz(ctrl)
    mockUserBiz := biz.NewMockUserBiz(ctrl)

    mockBiz.EXPECT().Users().Return(mockUserBiz).AnyTimes()
    mockUserBiz.EXPECT().List(gomock.Any(), gomock.Any()).Return(&v1.ListUsersResponse{
        Total: 1,
        Users: []*v1.User{{Id: 1, Username: "test"}},
    }, nil)

    controller := NewUserController(mockBiz)

    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    c.Request = httptest.NewRequest("GET", "/v1/users?page=1&limit=10", nil)

    controller.List(c)

    assert.Equal(t, http.StatusOK, w.Code)
}

Mock 生成

# 生成 Mock
mockgen -source=internal/twms/store/store.go \
        -destination=internal/twms/store/mock_store.go \
        -package=store

# go:generate 方式
//go:generate mockgen -source=store.go -destination=mock_store.go -package=store