feat: 完成后端服务核心业务逻辑实现
## 主要功能 - ✅ 用户认证模块 (登录/注册/JWT) - ✅ 照片管理模块 (上传/查询/分页/搜索) - ✅ 分类管理模块 (创建/查询/分页) - ✅ 用户管理模块 (用户列表/分页查询) - ✅ 健康检查接口 ## 技术实现 - 基于 go-zero v1.8.0 标准架构 - Handler → Logic → Model 三层架构 - SQLite/PostgreSQL 数据库支持 - JWT 认证机制 - bcrypt 密码加密 - 统一响应格式 - 自定义模型方法 (分页/搜索) ## API 接口 - POST /api/v1/auth/login - 用户登录 - POST /api/v1/auth/register - 用户注册 - GET /api/v1/health - 健康检查 - GET /api/v1/photos - 照片列表 - POST /api/v1/photos - 上传照片 - GET /api/v1/categories - 分类列表 - POST /api/v1/categories - 创建分类 - GET /api/v1/users - 用户列表 ## 配置完成 - 开发环境配置 (SQLite) - 生产环境支持 (PostgreSQL) - JWT 认证配置 - 文件上传配置 - Makefile 构建脚本 服务已验证可正常构建和启动。
This commit is contained in:
@ -1,834 +0,0 @@
|
||||
# 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. **事务回滚**: 使用事务确保数据隔离
|
||||
|
||||
本模块确保了代码质量和系统可靠性,是持续集成和交付的重要基础。
|
||||
Reference in New Issue
Block a user