Go 框架学习之:Fx 简单测试示例:fx.Options() 和 fx.Replace()

📋 示例场景

假设我们有一个用户服务,在生产环境中使用真实的数据库,但在测试时需要替换为内存数据库。

🏗️ 生产环境代码结构

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() 测试专用 自动管理测试生命周期

💡 最佳实践

  1. 接口优先设计 - 依赖接口而不是具体实现
  2. 模块化替换 - 为测试创建专门的mock模块
  3. 条件替换 - 根据测试需求动态选择实现
  4. 生命周期管理 - 使用fxtest自动处理启动/停止

🚀 优势

  • 隔离性 - 测试不影响生产代码
  • 可维护性 - 清晰的替换逻辑
  • 灵活性 - 支持多种测试场景
  • 类型安全 - 编译时检查替换类型

通过这种方式,我们可以轻松地为任何组件创建测试版本,而无需修改生产代码!

相关推荐
程序员小假5 小时前
我们来说说 Cookie、Session、Token、JWT
java·后端
短剑重铸之日6 小时前
《SpringBoot4.0初识》第一篇:前瞻与思想
java·开发语言·后端·spring·springboot4.0
it_czz6 小时前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者6 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下7 小时前
apifox登录后设置token到环境变量
java·后端
hashiqimiya8 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev8 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG8 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范
用户8356290780518 小时前
用Python轻松管理Word页脚:批量处理与多节文档技巧
后端·python
想用offer打牌9 小时前
一站式了解Spring AI Alibaba的流式输出
java·人工智能·后端