# Test Module - CLAUDE.md 本文件为 Claude Code 在测试模块中工作时提供指导。 ## 🎯 模块概览 测试模块提供完整的测试策略和工具,确保代码质量和系统可靠性。 ### 主要职责 - 🧪 单元测试和集成测试 - 🔧 测试工具和辅助函数 - 📊 测试覆盖率报告 - 🚀 性能测试和基准测试 - 🎭 Mock 对象和测试数据 ## 📁 模块结构 ``` tests/ ├── CLAUDE.md # 📋 当前文件 - 测试编写和执行指导 ├── unit/ # 🧪 单元测试 │ ├── service/ # 服务层测试 │ │ ├── user_service_test.go │ │ ├── photo_service_test.go │ │ └── auth_service_test.go │ ├── repository/ # 仓储层测试 │ │ ├── user_repository_test.go │ │ └── photo_repository_test.go │ ├── handler/ # 处理器测试 │ │ ├── user_handler_test.go │ │ └── photo_handler_test.go │ └── utils/ # 工具函数测试 │ ├── string_test.go │ └── crypto_test.go ├── integration/ # 🔗 集成测试 │ ├── api/ # API 集成测试 │ │ ├── user_api_test.go │ │ ├── photo_api_test.go │ │ └── auth_api_test.go │ ├── database/ # 数据库集成测试 │ │ ├── migration_test.go │ │ └── transaction_test.go │ └── storage/ # 存储集成测试 │ └── file_storage_test.go ├── e2e/ # 🎯 端到端测试 │ ├── user_journey_test.go # 用户流程测试 │ ├── photo_upload_test.go # 照片上传流程 │ └── auth_flow_test.go # 认证流程测试 ├── benchmark/ # ⚡ 性能测试 │ ├── service_bench_test.go # 服务性能测试 │ ├── repository_bench_test.go # 仓储性能测试 │ └── api_bench_test.go # API 性能测试 ├── fixtures/ # 📄 测试数据 │ ├── users.json # 用户测试数据 │ ├── photos.json # 照片测试数据 │ └── categories.json # 分类测试数据 ├── mocks/ # 🎭 Mock 对象 │ ├── service_mocks.go # 服务层 Mock │ ├── repository_mocks.go # 仓储层 Mock │ └── external_mocks.go # 外部服务 Mock ├── helpers/ # 🛠️ 测试辅助 │ ├── database.go # 数据库测试助手 │ ├── server.go # 测试服务器 │ ├── assertions.go # 自定义断言 │ └── fixtures.go # 测试数据加载 └── config/ # ⚙️ 测试配置 ├── test_config.yaml # 测试环境配置 └── docker-compose.test.yml # 测试容器配置 ``` ## 🧪 单元测试 ### 服务层测试示例 ```go // unit/service/user_service_test.go - 用户服务测试 package service_test import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "photography-backend/internal/model/dto" "photography-backend/internal/model/entity" "photography-backend/internal/service/user" "photography-backend/tests/mocks" "photography-backend/pkg/logger" ) // UserServiceTestSuite 用户服务测试套件 type UserServiceTestSuite struct { suite.Suite userService *user.UserService mockRepo *mocks.MockUserRepository mockLogger logger.Logger } // SetupTest 测试前置设置 func (suite *UserServiceTestSuite) SetupTest() { suite.mockRepo = new(mocks.MockUserRepository) suite.mockLogger = logger.NewNoop() suite.userService = user.NewUserService(suite.mockRepo, suite.mockLogger) } // TearDownTest 测试后置清理 func (suite *UserServiceTestSuite) TearDownTest() { suite.mockRepo.AssertExpectations(suite.T()) } // TestCreateUser_Success 测试创建用户成功 func (suite *UserServiceTestSuite) TestCreateUser_Success() { // Arrange ctx := context.Background() req := &dto.CreateUserRequest{ Username: "testuser", Email: "test@example.com", Password: "password123", } expectedUser := &entity.User{ ID: 1, Username: req.Username, Email: req.Email, Role: entity.UserRoleUser, Status: entity.UserStatusActive, } suite.mockRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound) suite.mockRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")).Return(expectedUser, nil) // Act user, err := suite.userService.CreateUser(ctx, req) // Assert assert.NoError(suite.T(), err) assert.NotNil(suite.T(), user) assert.Equal(suite.T(), expectedUser.ID, user.ID) assert.Equal(suite.T(), expectedUser.Username, user.Username) assert.Equal(suite.T(), expectedUser.Email, user.Email) } // TestCreateUser_UserExists 测试用户已存在 func (suite *UserServiceTestSuite) TestCreateUser_UserExists() { // Arrange ctx := context.Background() req := &dto.CreateUserRequest{ Username: "testuser", Email: "test@example.com", Password: "password123", } existingUser := &entity.User{ ID: 1, Email: req.Email, } suite.mockRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil) // Act user, err := suite.userService.CreateUser(ctx, req) // Assert assert.Error(suite.T(), err) assert.Nil(suite.T(), user) assert.Contains(suite.T(), err.Error(), "user already exists") } // TestCreateUser_InvalidInput 测试无效输入 func (suite *UserServiceTestSuite) TestCreateUser_InvalidInput() { ctx := context.Background() testCases := []struct { name string req *dto.CreateUserRequest }{ { name: "empty email", req: &dto.CreateUserRequest{ Username: "testuser", Email: "", Password: "password123", }, }, { name: "empty username", req: &dto.CreateUserRequest{ Username: "", Email: "test@example.com", Password: "password123", }, }, { name: "weak password", req: &dto.CreateUserRequest{ Username: "testuser", Email: "test@example.com", Password: "123", }, }, } for _, tc := range testCases { suite.T().Run(tc.name, func(t *testing.T) { // Act user, err := suite.userService.CreateUser(ctx, tc.req) // Assert assert.Error(t, err) assert.Nil(t, user) }) } } // 运行测试套件 func TestUserServiceTestSuite(t *testing.T) { suite.Run(t, new(UserServiceTestSuite)) } ``` ### 仓储层测试示例 ```go // unit/repository/user_repository_test.go - 用户仓储测试 package repository_test import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "photography-backend/internal/model/entity" "photography-backend/internal/repository/postgres" "photography-backend/pkg/logger" "photography-backend/tests/helpers" ) // UserRepositoryTestSuite 用户仓储测试套件 type UserRepositoryTestSuite struct { suite.Suite db *gorm.DB repo *postgres.UserRepository } // SetupSuite 套件前置设置 func (suite *UserRepositoryTestSuite) SetupSuite() { // 使用内存数据库 db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) suite.Require().NoError(err) // 自动迁移 err = db.AutoMigrate(&entity.User{}) suite.Require().NoError(err) suite.db = db suite.repo = postgres.NewUserRepository(db, logger.NewNoop()).(*postgres.UserRepository) } // SetupTest 每个测试前清理数据 func (suite *UserRepositoryTestSuite) SetupTest() { helpers.CleanupDatabase(suite.db) } // TearDownSuite 套件后置清理 func (suite *UserRepositoryTestSuite) TearDownSuite() { sqlDB, _ := suite.db.DB() sqlDB.Close() } // TestCreateUser 测试创建用户 func (suite *UserRepositoryTestSuite) TestCreateUser() { ctx := context.Background() user := &entity.User{ Username: "testuser", Email: "test@example.com", Password: "hashedpassword", Role: entity.UserRoleUser, Status: entity.UserStatusActive, } createdUser, err := suite.repo.Create(ctx, user) assert.NoError(suite.T(), err) assert.NotZero(suite.T(), createdUser.ID) assert.Equal(suite.T(), user.Username, createdUser.Username) assert.Equal(suite.T(), user.Email, createdUser.Email) assert.NotZero(suite.T(), createdUser.CreatedAt) assert.NotZero(suite.T(), createdUser.UpdatedAt) } // TestGetUserByEmail 测试根据邮箱获取用户 func (suite *UserRepositoryTestSuite) TestGetUserByEmail() { ctx := context.Background() // 创建测试用户 user := helpers.CreateTestUser(suite.db, &entity.User{ Username: "testuser", Email: "test@example.com", Password: "hashedpassword", }) // 根据邮箱查找用户 foundUser, err := suite.repo.GetByEmail(ctx, user.Email) assert.NoError(suite.T(), err) assert.Equal(suite.T(), user.ID, foundUser.ID) assert.Equal(suite.T(), user.Email, foundUser.Email) } // TestGetUserByEmail_NotFound 测试用户不存在 func (suite *UserRepositoryTestSuite) TestGetUserByEmail_NotFound() { ctx := context.Background() user, err := suite.repo.GetByEmail(ctx, "nonexistent@example.com") assert.Error(suite.T(), err) assert.Nil(suite.T(), user) assert.Equal(suite.T(), repository.ErrNotFound, err) } // 运行测试套件 func TestUserRepositoryTestSuite(t *testing.T) { suite.Run(t, new(UserRepositoryTestSuite)) } ``` ## 🔗 集成测试 ### API 集成测试示例 ```go // integration/api/user_api_test.go - 用户API集成测试 package api_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "photography-backend/internal/model/dto" "photography-backend/tests/helpers" ) // UserAPITestSuite 用户API测试套件 type UserAPITestSuite struct { suite.Suite server *helpers.TestServer router *gin.Engine } // SetupSuite 套件前置设置 func (suite *UserAPITestSuite) SetupSuite() { suite.server = helpers.NewTestServer() suite.router = suite.server.Router() } // SetupTest 每个测试前重置数据 func (suite *UserAPITestSuite) SetupTest() { suite.server.ResetDatabase() } // TearDownSuite 套件后置清理 func (suite *UserAPITestSuite) TearDownSuite() { suite.server.Close() } // TestCreateUser_Success 测试创建用户成功 func (suite *UserAPITestSuite) TestCreateUser_Success() { // Arrange req := &dto.CreateUserRequest{ Username: "testuser", Email: "test@example.com", Password: "password123", } jsonData, _ := json.Marshal(req) // Act w := httptest.NewRecorder() httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) httpReq.Header.Set("Content-Type", "application/json") suite.router.ServeHTTP(w, httpReq) // Assert assert.Equal(suite.T(), http.StatusCreated, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(suite.T(), err) assert.True(suite.T(), response["success"].(bool)) assert.NotNil(suite.T(), response["data"]) userData := response["data"].(map[string]interface{}) assert.Equal(suite.T(), req.Username, userData["username"]) assert.Equal(suite.T(), req.Email, userData["email"]) assert.NotZero(suite.T(), userData["id"]) } // TestCreateUser_ValidationError 测试验证错误 func (suite *UserAPITestSuite) TestCreateUser_ValidationError() { // Arrange req := &dto.CreateUserRequest{ Username: "", // 空用户名应该失败 Email: "invalid-email", // 无效邮箱 Password: "123", // 密码过短 } jsonData, _ := json.Marshal(req) // Act w := httptest.NewRecorder() httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) httpReq.Header.Set("Content-Type", "application/json") suite.router.ServeHTTP(w, httpReq) // Assert assert.Equal(suite.T(), http.StatusBadRequest, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(suite.T(), err) assert.False(suite.T(), response["success"].(bool)) assert.NotNil(suite.T(), response["error"]) } // TestGetUser_Success 测试获取用户成功 func (suite *UserAPITestSuite) TestGetUser_Success() { // Arrange - 创建测试用户 user := suite.server.CreateTestUser() // Act w := httptest.NewRecorder() httpReq, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/users/%d", user.ID), nil) suite.router.ServeHTTP(w, httpReq) // Assert assert.Equal(suite.T(), http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(suite.T(), err) assert.True(suite.T(), response["success"].(bool)) userData := response["data"].(map[string]interface{}) assert.Equal(suite.T(), float64(user.ID), userData["id"]) assert.Equal(suite.T(), user.Username, userData["username"]) assert.Equal(suite.T(), user.Email, userData["email"]) } // 运行测试套件 func TestUserAPITestSuite(t *testing.T) { suite.Run(t, new(UserAPITestSuite)) } ``` ## ⚡ 性能测试 ### 基准测试示例 ```go // benchmark/service_bench_test.go - 服务性能测试 package benchmark_test import ( "context" "testing" "photography-backend/internal/model/dto" "photography-backend/tests/helpers" ) // BenchmarkUserService_CreateUser 用户创建性能测试 func BenchmarkUserService_CreateUser(b *testing.B) { server := helpers.NewTestServer() defer server.Close() userService := server.UserService() ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { req := &dto.CreateUserRequest{ Username: fmt.Sprintf("user%d", i), Email: fmt.Sprintf("user%d@example.com", i), Password: "password123", } _, err := userService.CreateUser(ctx, req) if err != nil { b.Fatal(err) } } } // BenchmarkUserService_GetUser 用户查询性能测试 func BenchmarkUserService_GetUser(b *testing.B) { server := helpers.NewTestServer() defer server.Close() // 预创建测试用户 users := make([]*entity.User, 1000) for i := 0; i < 1000; i++ { users[i] = server.CreateTestUser() } userService := server.UserService() ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { userID := users[i%1000].ID _, err := userService.GetUser(ctx, userID) if err != nil { b.Fatal(err) } } } // BenchmarkUserAPI_CreateUser API性能测试 func BenchmarkUserAPI_CreateUser(b *testing.B) { server := helpers.NewTestServer() defer server.Close() router := server.Router() b.ResetTimer() for i := 0; i < b.N; i++ { req := &dto.CreateUserRequest{ Username: fmt.Sprintf("user%d", i), Email: fmt.Sprintf("user%d@example.com", i), Password: "password123", } jsonData, _ := json.Marshal(req) w := httptest.NewRecorder() httpReq, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) httpReq.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, httpReq) if w.Code != http.StatusCreated { b.Fatalf("Expected status %d, got %d", http.StatusCreated, w.Code) } } } ``` ## 🛠️ 测试辅助工具 ### 测试服务器 ```go // helpers/server.go - 测试服务器 package helpers import ( "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gin-gonic/gin" "photography-backend/internal/model/entity" "photography-backend/internal/service" "photography-backend/internal/repository" "photography-backend/internal/api/handlers" "photography-backend/pkg/logger" ) // TestServer 测试服务器 type TestServer struct { db *gorm.DB router *gin.Engine services *Services } // Services 服务集合 type Services struct { UserService service.UserServicer PhotoService service.PhotoServicer AuthService service.AuthServicer } // NewTestServer 创建测试服务器 func NewTestServer() *TestServer { // 创建内存数据库 db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { panic(err) } // 自动迁移 err = db.AutoMigrate( &entity.User{}, &entity.Photo{}, &entity.Category{}, &entity.Tag{}, &entity.Album{}, ) if err != nil { panic(err) } // 创建logger logger := logger.NewNoop() // 创建仓储 userRepo := repository.NewUserRepository(db, logger) photoRepo := repository.NewPhotoRepository(db, logger) // 创建服务 userService := service.NewUserService(userRepo, logger) photoService := service.NewPhotoService(photoRepo, userRepo, nil, logger) authService := service.NewAuthService(userRepo, nil, logger) services := &Services{ UserService: userService, PhotoService: photoService, AuthService: authService, } // 创建路由 gin.SetMode(gin.TestMode) router := gin.New() // 注册路由 v1 := router.Group("/api/v1") { userHandler := handlers.NewUserHandler(userService, logger) photoHandler := handlers.NewPhotoHandler(photoService, logger) authHandler := handlers.NewAuthHandler(authService, logger) v1.POST("/users", userHandler.Create) v1.GET("/users/:id", userHandler.GetByID) v1.PUT("/users/:id", userHandler.Update) v1.DELETE("/users/:id", userHandler.Delete) v1.POST("/auth/login", authHandler.Login) v1.POST("/auth/register", authHandler.Register) v1.POST("/photos", photoHandler.Create) v1.GET("/photos/:id", photoHandler.GetByID) } return &TestServer{ db: db, router: router, services: services, } } // Router 获取路由器 func (ts *TestServer) Router() *gin.Engine { return ts.router } // UserService 获取用户服务 func (ts *TestServer) UserService() service.UserServicer { return ts.services.UserService } // CreateTestUser 创建测试用户 func (ts *TestServer) CreateTestUser() *entity.User { user := &entity.User{ Username: "testuser", Email: "test@example.com", Password: "hashedpassword", Role: entity.UserRoleUser, Status: entity.UserStatusActive, } result := ts.db.Create(user) if result.Error != nil { panic(result.Error) } return user } // ResetDatabase 重置数据库 func (ts *TestServer) ResetDatabase() { CleanupDatabase(ts.db) } // Close 关闭测试服务器 func (ts *TestServer) Close() { sqlDB, _ := ts.db.DB() sqlDB.Close() } ``` ### 数据库辅助工具 ```go // helpers/database.go - 数据库测试助手 package helpers import ( "gorm.io/gorm" "photography-backend/internal/model/entity" ) // CleanupDatabase 清理数据库 func CleanupDatabase(db *gorm.DB) { // 按外键依赖顺序删除 db.Exec("DELETE FROM album_photos") db.Exec("DELETE FROM photo_tags") db.Exec("DELETE FROM photo_categories") db.Exec("DELETE FROM photos") db.Exec("DELETE FROM albums") db.Exec("DELETE FROM tags") db.Exec("DELETE FROM categories") db.Exec("DELETE FROM users") // 重置自增ID db.Exec("DELETE FROM sqlite_sequence") } // CreateTestUser 创建测试用户 func CreateTestUser(db *gorm.DB, user *entity.User) *entity.User { if user.Role == "" { user.Role = entity.UserRoleUser } if user.Status == "" { user.Status = entity.UserStatusActive } result := db.Create(user) if result.Error != nil { panic(result.Error) } return user } // CreateTestPhoto 创建测试照片 func CreateTestPhoto(db *gorm.DB, photo *entity.Photo) *entity.Photo { if photo.Status == "" { photo.Status = entity.PhotoStatusActive } result := db.Create(photo) if result.Error != nil { panic(result.Error) } return photo } ``` ## 🎯 测试策略 ### 测试金字塔 1. **单元测试 (70%)** - 测试单个函数或方法 - 使用 Mock 对象隔离依赖 - 快速执行,高覆盖率 2. **集成测试 (20%)** - 测试模块间交互 - 使用真实数据库 - 验证接口契约 3. **端到端测试 (10%)** - 测试完整用户流程 - 使用真实环境 - 验证业务逻辑 ### 测试执行命令 ```bash # 运行所有测试 make test # 运行单元测试 make test-unit # 运行集成测试 make test-integration # 运行性能测试 make test-benchmark # 生成测试覆盖率报告 make test-coverage # 运行特定包的测试 go test ./tests/unit/service/... # 运行性能测试 go test -bench=. ./tests/benchmark/... # 生成详细的测试报告 go test -v -race -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html ``` ## 💡 测试最佳实践 ### 测试编写原则 1. **AAA 模式**: Arrange, Act, Assert 2. **独立性**: 测试间不相互依赖 3. **可重复**: 测试结果一致 4. **快速执行**: 单元测试应快速完成 5. **描述性**: 测试名称清晰描述意图 ### Mock 使用建议 1. **接口隔离**: 对外部依赖使用 Mock 2. **行为验证**: 验证方法调用和参数 3. **状态验证**: 验证返回值和状态变化 4. **Mock 生成**: 使用工具自动生成 Mock ### 测试数据管理 1. **固定数据**: 使用 fixtures 文件 2. **随机数据**: 使用测试数据生成器 3. **数据清理**: 每个测试后清理数据 4. **事务回滚**: 使用事务确保数据隔离 本模块确保了代码质量和系统可靠性,是持续集成和交付的重要基础。