Files
photography/backend-old/tests/CLAUDE.md
xujiang 010fe2a8c7
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 5m8s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
fix
2025-07-10 18:09:11 +08:00

23 KiB

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  # 测试容器配置

🧪 单元测试

服务层测试示例

// 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))
}

仓储层测试示例

// 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 集成测试示例

// 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))
}

性能测试

基准测试示例

// 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)
        }
    }
}

🛠️ 测试辅助工具

测试服务器

// 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()
}

数据库辅助工具

// 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%)

    • 测试完整用户流程
    • 使用真实环境
    • 验证业务逻辑

测试执行命令

# 运行所有测试
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. 事务回滚: 使用事务确保数据隔离

本模块确保了代码质量和系统可靠性,是持续集成和交付的重要基础。