Files
ai-proj-helper/plugins/dev-test-plugin/skills/dev-test/go-testing.md

209 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 路由和参数绑定 |
```go
// ✅ 正确 — 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 模板
```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
}
```
## 运行测试
```bash
# 所有测试
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
```go
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)
}
```
## 表驱动测试
```go
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 测试
```go
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 生成
```bash
# 生成 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
```