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自动处理启动/停止

🚀 优势

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

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

相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川1 天前
深入拆解 Java 内存模型:从原子性、可见性到有序性,彻底搞懂 happen-before 规则
java·后端
元宝骑士1 天前
FIND_IN_SET使用指南:场景、优缺点与MySQL优化策略
后端·mysql
用户31952370347711 天前
记一次 PostgreSQL WAL 日志撑爆磁盘的排查
后端
nghxni1 天前
LightESB PlatformHttp v3.0.0:JSONPath 订单转换 HTTP 路由实战
后端
武子康1 天前
大数据-263 实时数仓-Canal 增量订阅与消费原理:MySQL Binlog 数据同步实践
大数据·hadoop·后端