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