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
|
||
```
|