使用gone v2 的 Provider 机制升级改造 goner/xorm 的过程记录

Gone 是作者开发的基于标签的依赖注入框架,给golang提供了一种简单、灵活、可扩展的依赖注入方式,将长期维护并持续优化。

项目地址:github.com/gone-io/gon...

Goner 是基于Gone框架的一个子项目,提供了基于依赖注入封装的一些列组件,如:配置读取、日志输出、数据库访问、大模型接入等。

项目地址:github.com/gone-io/gon...

  • [使用gone v2 的Provider 机制升级改造 goner/xorm 的过程记录](#使用gone v2 的Provider 机制升级改造 goner/xorm 的过程记录 "#%E4%BD%BF%E7%94%A8gone-v2-%E7%9A%84provider-%E6%9C%BA%E5%88%B6%E5%8D%87%E7%BA%A7%E6%94%B9%E9%80%A0-gonerxorm-%E7%9A%84%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95")
    • 缘起
    • 目标
      • [1. 保证配置兼容改造前的,保证注入方式兼容改造前的](#1. 保证配置兼容改造前的,保证注入方式兼容改造前的 "#1-%E4%BF%9D%E8%AF%81%E9%85%8D%E7%BD%AE%E5%85%BC%E5%AE%B9%E6%94%B9%E9%80%A0%E5%89%8D%E7%9A%84%E4%BF%9D%E8%AF%81%E6%B3%A8%E5%85%A5%E6%96%B9%E5%BC%8F%E5%85%BC%E5%AE%B9%E6%94%B9%E9%80%A0%E5%89%8D%E7%9A%84")
      • [2. 提供的功能不能改变](#2. 提供的功能不能改变 "#2-%E6%8F%90%E4%BE%9B%E7%9A%84%E5%8A%9F%E8%83%BD%E4%B8%8D%E8%83%BD%E6%94%B9%E5%8F%98")
        • [2.1 增强事务支持](#2.1 增强事务支持 "#21-%E5%A2%9E%E5%BC%BA%E4%BA%8B%E5%8A%A1%E6%94%AF%E6%8C%81")
        • [2.2 增强SQL拼接能力](#2.2 增强SQL拼接能力 "#22-%E5%A2%9E%E5%BC%BAsql%E6%8B%BC%E6%8E%A5%E8%83%BD%E5%8A%9B")
      • [3. 采用v2版本的Provider机制来统一实现](#3. 采用v2版本的Provider机制来统一实现 "#3-%E9%87%87%E7%94%A8v2%E7%89%88%E6%9C%AC%E7%9A%84provider%E6%9C%BA%E5%88%B6%E6%9D%A5%E7%BB%9F%E4%B8%80%E5%AE%9E%E7%8E%B0")
      • [4. 优化代码设计,使职责更清晰,边界更分明](#4. 优化代码设计,使职责更清晰,边界更分明 "#4-%E4%BC%98%E5%8C%96%E4%BB%A3%E7%A0%81%E8%AE%BE%E8%AE%A1%E4%BD%BF%E8%81%8C%E8%B4%A3%E6%9B%B4%E6%B8%85%E6%99%B0%E8%BE%B9%E7%95%8C%E6%9B%B4%E5%88%86%E6%98%8E")
    • 设计
    • 实现
      • [1. 实现xormProvider及相关Provider](#1. 实现xormProvider及相关Provider "#1-%E5%AE%9E%E7%8E%B0xormprovider%E5%8F%8A%E7%9B%B8%E5%85%B3provider")
      • [2. 实现事务增强功能](#2. 实现事务增强功能 "#2-%E5%AE%9E%E7%8E%B0%E4%BA%8B%E5%8A%A1%E5%A2%9E%E5%BC%BA%E5%8A%9F%E8%83%BD")
      • [3. 实现Engine封装](#3. 实现Engine封装 "#3-%E5%AE%9E%E7%8E%B0engine%E5%B0%81%E8%A3%85")
      • [4. 实现Engine提供者](#4. 实现Engine提供者 "#4-%E5%AE%9E%E7%8E%B0engine%E6%8F%90%E4%BE%9B%E8%80%85")
    • 测试
    • 总结

缘起

最近在给 goner增加测试代码,提高项目的测试覆盖率。改到goner/xorm时,发现存在两个主要问题:

1.原来的设计比较复杂

goner/xorm是gone框架中较早提供的组件,其演进历程如下:

  • v1.0版本:完全基于Goner机制实现

  • v1.2版本:引入Provider机制,为支持多数据库和集群场景做了增量改造

目前的情况是Goner机制和Provider机制的实现同时存在:

  • 默认配置的数据库:基于Goner机制注入

  • 集群数据库或多数据库:使用Provider机制注入

这种双机制并存的设计增加了代码的复杂性和维护难度。

2.测试不够友好,比较难做覆盖。

代码的设计职责不够清晰,组件边界不够分明,导致测试编写困难,难以实现良好的测试覆盖率。

目标

1. 保证配置兼容改造前的,保证注入方式兼容改造前的

先回顾goner/xorm的配置约定:

yaml 复制代码
# database 默认数据库配置前缀
database:
    driver-name: mysql  # 数据库驱动名称
    dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options  # 数据库连接字符串
    max-idle-count: 10  # 连接池最大空闲连接数
    max-open: 100  # 连接池最大打开连接数
    max-lifetime: 10s  # 连接最大生存时间
    show-sql: true  # 是否显示SQL语句
    cluster:  # 集群配置
        enable: false  # 是否启用集群模式
        master:  # 主库配置
            driver-name: mysql
            dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options
        slaves:  # 从库配置列表
            - driver-name: mysql
              dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options
            - driver-name: mysql
              dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options

# 自定义数据库配置前缀,custom-name 是自定义的数据库配置前缀
custom-name:
    driver-name: mysql
    dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options
    max-idle-count: 10
    max-open: 100
    max-lifetime: 10s
    show-sql: true
    cluster:
        enable: false
        master:
            driver-name: mysql
            dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options
        slaves:
            - driver-name: mysql
              dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options
            - driver-name: mysql
              dsn: user:password@tcp(IP_ADDRESS:3306)/dbname?options

再回顾goner/xorm的注入方式:

go 复制代码
//  默认配置前缀的数据库注入
type defaultDbUser struct {
    engine0 xorm.Engine `gone:"*"`  // 注入默认配置前缀的数据库
    engine1 xorm.Engine `gone:"xorm,master"`  // 注入默认配置数据库集群(如果开启集群模式)的 master 数据库
    engine2 []xorm.Engine `gone:"xorm"`  // 注入默认配置数据库集群(如果开启集群模式)的 slave 数据库集合
    engine3 xorm.Engine `gone:"xorm,slave=0"`  // 注入默认配置数据库集群(如果开启集群模式)的 slave 数据库集合中的第一个数据库
    engine4 xorm.Engine `gone:"xorm,slave=1"`  // 注入默认配置数据库集群(如果开启集群模式)的 slave 数据库集合中的第二个数据库
}

//  自定义配置前缀的数据库注入
type customerDbUser struct {
    engine0 xorm.Engine `gone:"xorm,db=custom-name"`  // 注入 配置前缀等于 custom-name 的数据库
    engine1 xorm.Engine `gone:"xorm,db=custom-name,master"`  // 注入 配置前缀等于 custom-name 集群(如果开启集群模式)的 master 数据库
    engine2 []xorm.Engine `gone:"xorm,db=custom-name"`  // 注入 配置前缀等于 custom-name 集群(如果开启集群模式)的 slave 数据库集合
    engine3 xorm.Engine `gone:"xorm,db=custom-name,slave=0"`  // 注入 配置前缀等于 custom-name 集群(如果开启集群模式)的 slave 数据库集合中的第一个数据库
    engine4 xorm.Engine `gone:"xorm,db=custom-name,slave=1"`  // 注入 配置前缀等于 custom-name 集群(如果开启集群模式)的 slave 数据库集合中的第二个数据库
}

改造后必须保持与上述配置格式和注入方式的兼容性,以确保用户代码无需更改。

2. 提供的功能不能改变

在功能上,goner/xorm中的Engine对xorm.io/xorm做了两个主要的增强:

2.1 增强事务支持

使用github.com/jtolds/gls增强事务,更好地支持事务嵌套,无需手动传递事务上下文:

go 复制代码
var db *xorm.Engine

// 主流程处理函数,开启顶层事务
func ProcessA(){
    db.Transaction(func(session xorm.XInterface) error {
        // 调用其他事务函数,无需传递session
        ProcessB()
        ProcessC()
        return nil
    })
}

// 子流程B,自动使用父事务上下文
func ProcessB(){
    db.Transaction(func(session xorm.XInterface) error {
        ProcessD()
        return nil
    })
}

// 子流程C,自动使用父事务上下文
func ProcessC(){
    db.Transaction(func(session xorm.XInterface) error {
        // 处理具体业务逻辑
        return nil
    })
}

// 子流程D,自动使用最上层事务上下文
func ProcessD(){
    db.Transaction(func(session xorm.XInterface) error {
        // 处理具体业务逻辑
        return nil
    })
}

这种设计可以让开发者无需关心事务的传递,简化嵌套事务的处理。

2.2 增强SQL拼接能力

使用github.com/jmoiron/sqlx增强SQL拼接能力,提供Sqlx函数:

go 复制代码
type dbUser struct {
    gone.Flag
    db xorm.Engine `gone:"*"`
}

func (s* dbUser)Query(){
    // 基本参数化SQL,使用问号占位符
    db.Sqlx("select * from user where id=?", 1).Exec()

    // 支持数组参数,会自动展开为 IN 查询
    db.Sqlx("select * from user where id in (?)", []int{1,2,3}).Exec()
    // 生成的SQL: select * from user where id in (1,2,3)

    // 支持命名参数,更直观清晰
    db.Sqlx("select * from user where id in (:id) and status = :status", map[string]any{
        "id": []int{1,2,3},
        "status": 1,
    }).Exec()
    // 生成的SQL: select * from user where id in (1,2,3) and status = 1
}

这种方式让SQL拼接更加灵活和安全,特别是处理复杂查询和动态条件时。

3. 采用v2版本的Provider机制来统一实现

改造的核心目标是统一使用一种机制(Provider机制)来实现全部功能,避免双机制并存带来的复杂性。

4. 优化代码设计,使职责更清晰,边界更分明

优化代码结构和组件划分,使每个组件职责单一明确,组件之间边界清晰,便于测试和维护。

设计

  1. 编写xormProvider 读取约定配置,为系统提供xorm.io/xormxorm.EngineInterface类型实例;
  2. 使用gone.WrapFunctionProvider包装xormProvider,为系统提供*xorm.Engine*xorm.EngineGroup类型实例;
  3. 使用eng 封装xorm.EngineInterface,实现原来goner/xorm中定义的的Engine接口,完成事务增强和SQL拼接增强;
  4. 使用engProvider为系统提供goner/xorm.Engine类型实例。

设计图

实现

下面是改造的核心实现代码,完整代码可在GitHub仓库查看:完整代码

1. 实现xormProvider及相关Provider

首先实现xormProvider及其封装类xormEngineProvider和xormGroupProvider,用于提供原生的xorm对象:

go 复制代码
// xorm_provide.go
package xorm

import (
    "fmt"
    "github.com/gone-io/gone/v2"
    "xorm.io/xorm"
)

type xormProvider struct {
    gone.Flag
    configure gone.Configure   `gone:"configure"`
    logger    gone.Logger      `gone:"*"`
    policy    xorm.GroupPolicy `gone:"*" option:"allowNil"`

    dbMap map[string]xorm.EngineInterface
}

func (s *xormProvider) Init() {
    s.dbMap = make(map[string]xorm.EngineInterface)
}

// ... 省略部分代码

// Provide 根据标签配置提供xorm.EngineInterface实例
func (s *xormProvider) Provide(tag string) (eng xorm.EngineInterface, err error) {
    dbName := getDbName(tag)

    if eng = s.dbMap[dbName]; eng == nil {
        eng, err = s.configAndInitDb(dbName)
        if err != nil {
            return nil, gone.ToError(err)
        }
        s.dbMap[dbName] = eng
    }
    return eng, nil
}

// configAndInitDb 根据数据库名称配置并初始化数据库连接
func (s *xormProvider) configAndInitDb(dbName string) (eng xorm.EngineInterface, err error) {
    var config Conf
    var enableCluster bool

    _ = s.configure.Get(dbName, &config, "")
    _ = s.configure.Get(dbName+".cluster.enable", &enableCluster, "false")

    if !enableCluster {
        // 创建单一数据库连接
        eng, err = xorm.NewEngine(config.DriverName, config.Dsn)
        if err != nil {
            return nil, gone.ToErrorWithMsg(err, "failed to create engine for db: "+dbName)
        }
    } else {
        // 创建数据库集群连接
        // ... 代码省略
    }

    // 配置数据库连接池和其他选项
    if config.MaxIdleCount > 0 {
        eng.SetMaxIdleConns(config.MaxIdleCount)
    }
    if config.MaxOpen > 0 {
        eng.SetMaxOpenConns(config.MaxOpen)
    }
    if config.MaxLifetime > 0 {
        eng.SetConnMaxLifetime(config.MaxLifetime)
    }
    eng.ShowSQL(config.ShowSql)
    eng.SetLogger(&dbLogger{Logger: s.logger, showSql: config.ShowSql})
    // 根据配置检测连接是否可用
    if config.PingAfterInit {
        if err = eng.Ping(); err != nil {
            return nil, gone.ToErrorWithMsg(err, "failed to ping db: "+dbName)
        }
    }
    return eng, nil
}

// 提供原生Engine实例
func (s *xormProvider) ProvideEngine(tagConf string) (*xorm.Engine, error) {
    // ... 代码省略
}

// 提供原生EngineGroup实例
func (s *xormProvider) ProvideEngineGroup(tagConf string) (*xorm.EngineGroup, error) {
    // ... 代码省略
}

// 封装函数Provider,提供*xorm.Engine
var xormEngineProvider = gone.WrapFunctionProvider(func(tagConf string, param struct {
    xormProvider *xormProvider `gone:"*"`
}) (*xorm.Engine, error) {
    return param.xormProvider.ProvideEngine(tagConf)
})

// 封装函数Provider,提供*xorm.EngineGroup
var xormGroupProvider = gone.WrapFunctionProvider(func(tagConf string, param struct {
    xormProvider *xormProvider `gone:"*"`
}) (*xorm.EngineGroup, error) {
    return param.xormProvider.ProvideEngineGroup(tagConf)
})

这部分代码的主要功能是解析配置文件,根据配置创建数据库连接,并提供原生的xorm引擎对象。关键点包括:

  • 支持单一数据库连接和主从集群连接

  • 实现配置解析和数据库连接池配置

  • 提供原生的xorm.Engine和xorm.EngineGroup实例

2. 实现事务增强功能

接下来实现事务增强部分,提供嵌套事务支持:

go 复制代码
// trans.go
package xorm

import (
    "fmt"
    "github.com/gone-io/gone/v2"
    "github.com/jtolds/gls"
    "sync"
)

func newTrans(logger gone.Logger, newSession func() Session) trans {
    return trans{
        logger:     logger,
        newSession: newSession,
    }
}

type trans struct {
    logger     gone.Logger
    newSession func() Session
}

var sessionMap = sync.Map{}

// 获取事务会话,如果当前goroutine没有事务则创建一个新会话
func (e *trans) getTransaction(id uint) (Session, bool) {
    session, suc := sessionMap.Load(id)
    if suc {
        return session.(Session), false
    } else {
        s := e.newSession()
        sessionMap.Store(id, s)
        return s, true
    }
}

// 删除事务并关闭会话
func (e *trans) delTransaction(id uint, session Session) error {
    defer sessionMap.Delete(id)
    return session.Close()
}

// Transaction 在事务中执行SQL
func (e *trans) Transaction(fn func(session Interface) error) error {
    var err error
    gls.EnsureGoroutineId(func(gid uint) {
        session, isNew := e.getTransaction(gid)

        if isNew {
            // 当前goroutine没有活跃事务,创建新事务
            rollback := func() {
                rollbackErr := session.Rollback()
                if rollbackErr != nil {
                    e.logger.Errorf("rollback err:%v", rollbackErr)
                    err = gone.ToErrorWithMsg(err, fmt.Sprintf("rollback error: %v", rollbackErr))
                }
            }

            isRollback := false
            defer func(e *trans, id uint, session Session) {
                err := e.delTransaction(id, session)
                if err != nil {
                    e.logger.Errorf("del session err:%v", err)
                }
            }(e, gid, session)

            // 处理panic情况,确保事务回滚
            defer func() {
                if info := recover(); info != nil {
                    e.logger.Errorf("session rollback for panic: %s", info)
                    e.logger.Errorf("%s", gone.PanicTrace(2, 1))
                    if !isRollback {
                        rollback()
                        err = gone.NewInnerError(fmt.Sprintf("%s", info), gone.DbRollForPanicError)
                    } else {
                        err = gone.ToErrorWithMsg(info, fmt.Sprintf("rollback for err: %v, but panic for", err))
                    }
                }
            }()

            // 开始事务
            err = session.Begin()
            if err != nil {
                err = gone.ToError(err)
                return
            }
            
            // 执行用户函数
            err = gone.ToError(fn(session))
            if err == nil {
                // 事务提交
                err = gone.ToError(session.Commit())
            } else {
                // 事务回滚
                e.logger.Errorf("session rollback for err: %v", err)
                isRollback = true
                rollback()
            }
        } else {
            // 当前goroutine已有活跃事务,复用现有事务
            err = gone.ToError(fn(session))
        }
    })
    return err
}

事务增强的核心是通过goroutine本地存储(gls)实现,这样可以:

  • 在同一goroutine中自动共享事务上下文

  • 支持嵌套事务调用,内层事务函数自动使用外层事务

  • 提供统一的错误处理和回滚机制

3. 实现Engine封装

然后实现eng结构体,封装原生的xorm接口并增强功能:

go 复制代码
//eng.go
package xorm

import (
    "github.com/gone-io/gone/v2"
    "xorm.io/xorm"
)

func newEng(xEng xorm.EngineInterface, logger gone.Logger) *eng {
    e := eng{EngineInterface: xEng}
    e.trans = newTrans(logger, func() Session {
        return e.NewSession()
    })
    return &e
}

type eng struct {
    xorm.EngineInterface  // 嵌入原生xorm接口
    trans                 // 嵌入事务增强功能
}

// 获取原始引擎实例
func (e *eng) GetOriginEngine() xorm.EngineInterface {
    return e.EngineInterface
}

// 设置主从策略
func (e *eng) SetPolicy(policy xorm.GroupPolicy) {
    if group, ok := e.EngineInterface.(*xorm.EngineGroup); ok {
        group.SetPolicy(policy)
    }
}

// 增强的SQL拼接功能
func (e *eng) Sqlx(sql string, args ...any) *xorm.Session {
    sql, args = sqlDeal(sql, args...)
    return e.SQL(sql, args...)
}

这个封装层实现了:

  • 嵌入原生xorm接口保持API兼容

  • 集成事务增强功能

  • 添加SQL拼接增强功能

4. 实现Engine提供者

最后,实现engProvider为应用提供增强版的Engine实例:

go 复制代码
//eng_provider.go
package xorm

import (
    "github.com/gone-io/gone/v2"
    "github.com/spf13/cast"
    "reflect"
)

type engProvider struct {
    gone.Flag
    logger    gone.Logger   `gone:"*"`
    xProvider *xormProvider `gone:"*"`
}

func (s *engProvider) GonerName() string {
    return "xorm" // 注册为xorm名称
}

// 提供实例,根据类型和配置提供不同的实例
func (s *engProvider) Provide(tagConf string, t reflect.Type) (any, error) {
    switch t {
    case xormInterfaceSlice: // 提供Engine切片
        group, err := s.xProvider.ProvideEngineGroup(tagConf)
        if err != nil {
            return nil, gone.ToError(err)
        }
        slaves := group.Slaves()
        engines := make([]Engine, 0, len(slaves))
        for _, slave := range slaves {
            engines = append(engines, newEng(slave, s.logger))
        }
        return engines, nil
    case xormInterface: // 提供单一Engine
        return s.ProvideEngine(tagConf)
    default:
        return nil, gone.NewInnerErrorWithParams(gone.GonerTypeNotMatch, "Cannot find matched value for %q", gone.GetTypeName(t))
    }
}

// 根据标签配置提供Engine实例
func (s *engProvider) ProvideEngine(tagConf string) (Engine, error) {
    m, _ := gone.TagStringParse(tagConf)
    
    // 处理master标记
    if v, ok := m[masterKey]; ok && (v == "" || cast.ToBool(v)) {
        group, err := s.xProvider.ProvideEngineGroup(tagConf)
        if err != nil {
            return nil, gone.ToError(err)
        }
        return newEng(group.Master(), s.logger), nil
    }
    
    // 处理slave标记
    if index, ok := m[slaveKey]; ok {
        i := cast.ToInt(index)
        group, err := s.xProvider.ProvideEngineGroup(tagConf)
        if err != nil {
            return nil, gone.ToError(err)
        }
        slaves := group.Slaves()
        if i < 0 || i >= len(slaves) {
            return nil, gone.ToError("slave index out of range")
        }
        return newEng(slaves[i], s.logger), nil
    }

    // 提供默认引擎
    provideEngine, err := s.xProvider.Provide(tagConf)
    if err != nil {
        return nil, gone.ToError(err)
    }
    return newEng(provideEngine, s.logger), nil
}

这个Provider实现了:

  • 将自身注册为"xorm"名称,与原来的注入标签保持一致;

  • 能够提供单个Engine实例或Engine切片;

  • 根据标签配置提供主库、从库或默认数据库实例;

  • 使用newEng包装原生引擎实例,增强其功能。

测试

测试覆盖率

测试是验证改造成功的关键环节。通过编写全面的测试用例,我们不仅能验证功能正确性,还能提高代码的测试覆盖率。

测试覆盖率

如图所示,改造后的代码测试覆盖率得到了显著提升。

代码覆盖率

下面是部分测试代码,展示了如何测试xormProvider:

go 复制代码
// xorm_provider_test.go
package xorm

import (
    "database/sql"
    "github.com/DATA-DOG/go-sqlmock"
    "github.com/gone-io/gone/v2"
    "github.com/stretchr/testify/assert"
    "os"
    "testing"
    "xorm.io/xorm"
)

func Test_xormProvider_Init(t *testing.T) {
    // 创建SQL模拟器,用于模拟数据库操作而无需真实数据库
    db, mock, _ := sqlmock.NewWithDSN(
        "root@/blog",
        sqlmock.MonitorPingsOption(true),
    )
    defer db.Close()

    // 注册模拟的数据库驱动
    drivers := sql.Drivers()
    if !contains(drivers, "mysql") {
        sql.Register("mysql", db.Driver())
    }

    // 定义测试用例结构
    type Tests struct {
        name        string
        before      func()         // 测试前准备
        after       func()         // 测试后清理
        exceptPanic bool           // 是否期望发生panic
        injectFn    any            // 依赖注入测试函数
    }

    tests := []Tests{
        {
            name: "inject default db failed",
            before: func() {
                // 设置错误的驱动名称,预期会失败
                _ = os.Setenv("GONE_DATABASE", `{
                    "driver-name":"error",
                    "dsn": "root@/blog",
                    "max-idle-count": 5,
                    "max-open": 10,
                    "max-lifetime": 10000,
                    "show-sql": true,
                    "ping-after-init": true
                }`)
            },
            after: func() {
                _ = os.Unsetenv("GONE_DATABASE")
            },
            injectFn: func(in struct {
                db *xorm.Engine `gone:"*"`
            }) {
                assert.NotNil(t, db)
            },
            exceptPanic: true, // 预期会发生panic
        },
        
        // ... 更多测试用例
        
        {
            name: "inject default db",
            before: func() {
                // 设置正确的数据库配置
                _ = os.Setenv("GONE_DATABASE", `{
                    "driver-name":"mysql",
                    "dsn": "root@/blog",
                    "max-idle-count": 5,
                    "max-open": 10,
                    "max-lifetime": 10000,
                    "show-sql": true,
                    "ping-after-init": true
                }`)
                mock.ExpectPing() // 期望执行ping操作
            },
            after: func() {
                _ = os.Unsetenv("GONE_DATABASE")
            },
            injectFn: func(in struct {
                db1      *xorm.Engine         `gone:""`
                db2      *xorm.Engine         `gone:"*"`
                db       xorm.EngineInterface `gone:"*"`
                injector gone.StructInjector  `gone:"*"`
            }) {
                // 验证注入的实例是否符合预期
                assert.NotNil(t, in.db1)
                assert.NotNil(t, in.db2)
                assert.Equal(t, in.db1, in.db2)
                assert.Equal(t, in.db1, in.db)

                // 尝试注入不兼容的类型,预期会失败
                var x struct {
                    db *xorm.EngineGroup `gone:"*"`
                }
                err := in.injector.InjectStruct(&x)
                assert.Error(t, err)
            },
            exceptPanic: false, // 不期望发生panic
        },
        
        // ... 更多测试用例
    }

    // 执行所有测试用例
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            tt.before()
            defer tt.after()

            func() {
                defer func() {
                    err := recover()
                    if tt.exceptPanic {
                        assert.Error(t, err.(error))
                    } else {
                        assert.Nil(t, err)
                    }
                }()

                // 启动应用并执行测试函数
                gone.
                    NewApp().
                    Load(&xormProvider{}).
                    Load(xormEngineProvider).
                    Load(xormGroupProvider).
                    Run(tt.injectFn)
            }()
        })
    }
}

这段测试代码使用了多种测试技巧:

  1. SQL模拟:使用sqlmock库模拟数据库操作,无需真实数据库即可进行测试

  2. 环境变量配置:通过设置环境变量模拟不同的配置场景

  3. 依赖注入测试:利用gone框架的依赖注入系统验证各种注入场景

  4. 异常测试:通过recover捕获并验证预期的panic情况

  5. 组件交互测试:测试不同组件之间的交互行为

通过这些测试用例,我们可以全面验证改造后的功能是否正常工作,同时提高代码的测试覆盖率。

总结

通过这次改造,我们成功地:

  1. 统一了xorm组件的实现机制,全部采用Provider机制,简化了代码结构

  2. 保持了与原有配置和注入方式的兼容性,确保用户代码无需更改

  3. 保留并增强了原有功能,包括事务管理和SQL拼接能力

  4. 重构了代码结构,使各组件职责更加清晰,边界更加分明

  5. 提高了代码的可测试性,显著提升了测试覆盖率

这次改造不仅提高了代码质量,还为未来的功能扩展和维护奠定了良好基础。通过合理采用设计模式和分层架构,我们可以在保持兼容性的同时,不断优化和演进系统。

相关推荐
小杨4044 分钟前
springboot框架项目实践应用十九(nacos配置中心)
spring boot·后端·spring cloud
小马爱打代码15 分钟前
设计模式:迪米特法则 - 最少依赖,实现高内聚低耦合
设计模式·迪米特法则
骊山道童37 分钟前
设计模式-观察者模式
观察者模式·设计模式
终身学习基地2 小时前
第二篇:go包管理
开发语言·后端·golang
图南随笔2 小时前
Spring Boot(二十一):RedisTemplate的String和Hash类型操作
java·spring boot·redis·后端·缓存
吃饭了呀呀呀2 小时前
🐳 《Android》 安卓开发教程 - 三级地区联动
android·java·后端
shengjk12 小时前
SparkSQL Join的源码分析
后端
Linux编程用C2 小时前
Rust编程学习(一): 变量与数据类型
开发语言·后端·rust
uhakadotcom2 小时前
一文读懂DSP(需求方平台):程序化广告投放的核心基础与实战案例
后端·面试·github