712063071c
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>
209 lines
4.9 KiB
Markdown
209 lines
4.9 KiB
Markdown
# 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
|
|
```
|