834 lines
23 KiB
Markdown
834 lines
23 KiB
Markdown
# 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. **事务回滚**: 使用事务确保数据隔离
|
|
|
|
本模块确保了代码质量和系统可靠性,是持续集成和交付的重要基础。 |