📋 示例场景
假设我们有一个用户服务,在生产环境中使用真实的数据库,但在测试时需要替换为内存数据库。
🏗️ 生产环境代码结构
1. 数据库接口和实现
go:/internal/database/database.go
package database
type Database interface {
GetUser(id int) (*User, error)
CreateUser(user *User) error
}
type MySQLDatabase struct {
// 真实的MySQL连接
}
func NewMySQLDatabase() (Database, error) {
// 创建真实的MySQL连接
return &MySQLDatabase{}, nil
}
2. 用户服务
go:/internal/user/service.go
package user
import "your-app/internal/database"
type Service struct {
db database.Database
}
func NewService(db database.Database) *Service {
return &Service{db: db}
}
func (s *Service) GetUser(id int) (*User, error) {
return s.db.GetUser(id)
}
3. 主应用模块
go:/internal/app/module.go
package app
import (
"go.uber.org/fx"
"your-app/internal/database"
"your-app/internal/user"
)
var Module = fx.Module("app",
fx.Provide(
database.NewMySQLDatabase, // 生产环境使用MySQL
user.NewService,
),
)
🧪 测试环境配置
1. 创建内存数据库模拟
go:/test/mocks/memory_db.go
package mocks
import "your-app/internal/database"
type MemoryDatabase struct {
users map[int]*database.User
}
func NewMemoryDatabase() database.Database {
return &MemoryDatabase{
users: make(map[int]*database.User),
}
}
func (m *MemoryDatabase) GetUser(id int) (*database.User, error) {
user, exists := m.users[id]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (m *MemoryDatabase) CreateUser(user *database.User) error {
m.users[user.ID] = user
return nil
}
2. 使用 fx.Replace() 替换依赖
go:/test/user_service_test.go
package test
import (
"testing"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"your-app/internal/app"
"your-app/internal/database"
"your-app/test/mocks"
)
func TestUserService(t *testing.T) {
// 创建测试应用,替换真实数据库为内存数据库
testApp := fxtest.New(t,
app.Module, // 包含所有生产环境模块
// 使用 fx.Replace() 替换数据库实现
fx.Replace(
fx.Annotate(
mocks.NewMemoryDatabase,
fx.As(new(database.Database)), // 绑定到 Database 接口
),
),
)
// 启动测试应用
testApp.RequireStart()
defer testApp.RequireStop()
// 从容器中获取用户服务
var userService *user.Service
err := testApp.Find(&userService)
if err != nil {
t.Fatalf("Failed to find user service: %v", err)
}
// 执行测试
_, err = userService.GetUser(1)
if err == nil {
t.Error("Expected error for non-existent user")
}
}
3. 使用 fx.Options() 组合多个替换
go:/test/complex_test.go
package test
import (
"testing"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"your-app/internal/app"
"your-app/internal/database"
"your-app/internal/logger"
"your-app/test/mocks"
)
func TestComplexScenario(t *testing.T) {
// 创建多个替换选项
testReplacements := fx.Options(
// 替换数据库
fx.Replace(
fx.Annotate(
mocks.NewMemoryDatabase,
fx.As(new(database.Database)),
),
),
// 替换日志器为测试日志器
fx.Replace(
fx.Annotate(
func() logger.Logger {
return logger.NewTestLogger(t)
},
fx.As(new(logger.Logger)),
),
),
// 替换配置
fx.Replace(
fx.Annotate(
func() *config.Config {
return &config.Config{
TestMode: true,
}
},
),
),
)
// 创建测试应用,组合生产模块和测试替换
testApp := fxtest.New(t,
app.Module, // 生产环境模块
testReplacements, // 测试环境替换
)
testApp.RequireStart()
defer testApp.RequireStop()
// 测试逻辑...
}
🎯 高级用法:条件替换
根据环境变量动态替换
go:/test/env_based_test.go
package test
import (
"os"
"testing"
"go.uber.org/fx"
"your-app/internal/database"
"your-app/test/mocks"
)
func getTestReplacements() fx.Option {
if os.Getenv("TEST_MODE") == "memory" {
return fx.Replace(
fx.Annotate(
mocks.NewMemoryDatabase,
fx.As(new(database.Database)),
),
)
}
// 默认不替换,使用生产环境实现
return fx.Options()
}
func TestWithEnvironment(t *testing.T) {
testApp := fxtest.New(t,
app.Module,
getTestReplacements(), // 根据环境动态决定是否替换
)
// ...
}
📊 替换策略对比
方法 | 用途 | 示例 |
---|---|---|
fx.Replace() |
替换单个依赖 | 替换数据库实现 |
fx.Options() |
组合多个替换 | 同时替换数据库和日志器 |
fx.Annotate() |
类型注解 | 指定接口绑定 |
fxtest.New() |
测试专用 | 自动管理测试生命周期 |
💡 最佳实践
- 接口优先设计 - 依赖接口而不是具体实现
- 模块化替换 - 为测试创建专门的mock模块
- 条件替换 - 根据测试需求动态选择实现
- 生命周期管理 - 使用fxtest自动处理启动/停止
🚀 优势
- ✅ 隔离性 - 测试不影响生产代码
- ✅ 可维护性 - 清晰的替换逻辑
- ✅ 灵活性 - 支持多种测试场景
- ✅ 类型安全 - 编译时检查替换类型
通过这种方式,我们可以轻松地为任何组件创建测试版本,而无需修改生产代码!