Go语言微服务框架 - 5.GORM库的适配sqlmock的单元测试

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS AI 助手,专业版
简介: 与此同时,我们也缺乏一个有效的手段来验证自己编写的相关代码。如果依靠连接到真实的MySQL去验证功能,那成本实在太高。那么,这里我们就引入一个经典的sqlmock框架,并配合对数据库相关代码的修改,来实现相关代码的可测试性。

随着GORM库的引入,我们在数据库持久化上已经有了解决方案。但上一篇我们使用的GORM过于简单,应用到实际的项目中局限性很大。

与此同时,我们也缺乏一个有效的手段来验证自己编写的相关代码。如果依靠连接到真实的MySQL去验证功能,那成本实在太高。那么,这里我们就引入一个经典的sqlmock框架,并配合对数据库相关代码的修改,来实现相关代码的可测试性。

v0.4.1:GORM库的适配sqlmock的单元测试

项目链接 https://github.com/Junedayday/micro_web_service/tree/v0.4.1

由于主要是针对GORM的小改动,所以增加了一个小版本号

目标

利用sqlmock工具,并对数据库相关代码进行修改,实现单元测试。

关键技术点

  1. Order相关代码的改造
  2. 引入sqlmock到测试代码
  3. 注意点讲解

目录构造

--- micro_web_service            项目目录
    |-- gen                            从idl文件夹中生成的文件,不可手动修改
       |-- idl                             对应idl文件夹
          |-- demo                             对应idl/demo服务
             |-- demo.pb.go                        demo.proto的基础结构
             |-- demo.pb.gw.go                     demo.proto的HTTP接口,对应gRPC-Gateway
             |-- demo_grpc.pb.go                   demo.proto的gRPC接口代码
    |-- idl                            原始的idl定义
       |-- demo                            业务package定义
          |-- demo.proto                       protobuffer的原始定义
    |-- internal                       项目的内部代码,不对外暴露
       |-- config                          配置相关的文件夹
          |-- viper.go                         viper的相关加载逻辑
       |-- dao                             Data Access Object层
          |-- order.go                         更新:OrderO对象,订单表
          |-- order_test.go                    新增:Order的单元测试
       |-- mysql                           MySQL连接
          |-- init.go                          初始化连接到MySQL的工作
       |-- server                          服务器的实现
          |-- demo.go                          server中对demo这个服务的接口实现
          |-- server.go                        server的定义,须实现对应服务的方法
     |-- zlog                            封装日志的文件夹
        |-- zap.go                           zap封装的代码实现
    |-- buf.gen.yaml                   buf生成代码的定义
    |-- buf.yaml                       buf工具安装所需的工具
    |-- gen.sh                         buf生成的shell脚本
    |-- go.mod                         Go Module文件
    |-- main.go                        项目启动的main函数

1.Order相关代码的改造

我们要对Order相关的代码进行改造,来满足以下两个点:

  1. 可测试性,可以脱离对真实数据库连接的依赖
  2. 灵活的更新方法,可以支持对指定条件、指定字段的更新
/*
  gorm.io/gorm 指的是gorm V2版本,详细可参考 https://gorm.io/zh_CN/docs/v2_release_note.html
  github.com/jinzhu/gorm 一般指V1版本
*/

type OrderRepo struct {
   
    db *gorm.DB
}

// 将gorm.DB作为一个参数,在初始化时赋值:方便测试时,放一个mock的db
func NewOrderRepo(db *gorm.DB) *OrderRepo {
   
    return &OrderRepo{
   db: db}
}

// Order针对的是 orders 表中的一行数据
type Order struct {
   
    Id    int64
    Name  string
    Price float32
}

// OrderFields 作为一个 数据库Order对象+fields字段的组合
// fields用来指定Order中的哪些字段生效
type OrderFields struct {
   
    order  *Order
    fields []interface{
   }
}

func NewOrderFields(order *Order, fields []interface{
   }) *OrderFields {
   
    return &OrderFields{
   
        order:  order,
        fields: fields,
    }
}

func (repo *OrderRepo) AddOrder(order *Order) (err error) {
   
    err = repo.db.Create(order).Error
    return
}

func (repo *OrderRepo) QueryOrders(pageNumber, pageSize int, condition *OrderFields) (orders []Order, err error) {
   
    db := repo.db
    // condition非nil的话,追加条件
    if condition != nil {
   
        // 这里的field指定了order中生效的字段,这些字段会被放在SQL的where条件中
        db = db.Where(condition.order, condition.fields...)
    }
    err = db.
        Limit(pageSize).
        Offset((pageNumber - 1) * pageSize).
        Find(&orders).Error
    return
}

func (repo *OrderRepo) UpdateOrder(updated, condition *OrderFields) (err error) {
   
    if updated == nil || len(updated.fields) == 0 {
   
        return errors.New("update must choose certain fields")
    } else if condition == nil {
   
        return errors.New("update must include where condition")
    }

    err = repo.db.
        Model(&Order{
   }).
        // 这里的field指定了order中被更新的字段
        Select(updated.fields[0], updated.fields[1:]...).
        // 这里的field指定了被更新的where条件中的字段
        Where(condition.order, condition.fields...).
        Updates(updated.order).
        Error
    return
}

2.引入sqlmock到测试代码

sqlmock是检查数据库最常用的工具,我们先不管它使用起来的复杂性,先来看看怎么实现对应的测试代码:

// 注意,我们使用的是gorm 2.0,网上很多例子其实是针对1.0的
var (
    DB   *gorm.DB
    mock sqlmock.Sqlmock
)

// TestMain是在当前package下,最先运行的一个函数,常用于初始化
func TestMain(m *testing.M) {
   
    var (
        db  *sql.DB
        err error
    )

    db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
    if err != nil {
   
        panic(err)
    }

    DB, err = gorm.Open(mysql.New(mysql.Config{
   
        Conn:                      db,
        SkipInitializeWithVersion: true,
    }), &gorm.Config{
   })
    if err != nil {
   
        panic(err)
    }

    // m.Run 是真正调用下面各个Test函数的入口
    os.Exit(m.Run())
}

/*
  sqlmock 对语法限制比较大,下面的sql语句必须精确匹配(包括符号和空格)
*/

func TestOrderRepo_AddOrder(t *testing.T) {
   
    var order = &Order{
   Name: "order1", Price: 1.1}
    orderRepo := NewOrderRepo(DB)

    mock.ExpectBegin()
    mock.ExpectExec("INSERT INTO `orders` (`name`,`price`) VALUES (?,?)").
        WithArgs(order.Name, order.Price).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()
    err := orderRepo.AddOrder(order)
    assert.Nil(t, err)
}

func TestOrderRepo_QueryOrders(t *testing.T) {
   
    var orders = []Order{
   
        {
   1, "name1", 1.0},
        {
   2, "name2", 1.0},
    }
    page, size := 2, 10
    orderRepo := NewOrderRepo(DB)
    condition := NewOrderFields(&Order{
   Price: 1.0}, []interface{
   }{
   "price"})

    mock.ExpectQuery(
        "SELECT * FROM `orders` WHERE `orders`.`price` = ? LIMIT 10 OFFSET 10").
        WithArgs(condition.order.Price).
        WillReturnRows(
            sqlmock.NewRows([]string{
   "id", "name", "price"}).
                AddRow(orders[0].Id, orders[0].Name, orders[0].Price).
                AddRow(orders[1].Id, orders[1].Name, orders[1].Price))

    ret, err := orderRepo.QueryOrders(page, size, condition)
    assert.Nil(t, err)
    assert.Equal(t, orders, ret)
}

func TestOrderRepo_UpdateOrder(t *testing.T) {
   
    orderRepo := NewOrderRepo(DB)
    // 表示要更新的字段为Order对象中的id,name两个字段
    updated := NewOrderFields(&Order{
   Id: 1, Name: "test_name"}, []interface{
   }{
   "id", "name"})
    // 表示更新的条件为Order对象中的price字段
    condition := NewOrderFields(&Order{
   Price: 1.0}, []interface{
   }{
   "price"})

    mock.ExpectBegin()
    mock.ExpectExec(
        "UPDATE `orders` SET `id`=?,`name`=? WHERE `orders`.`price` = ?").
        WithArgs(updated.order.Id, updated.order.Name, condition.order.Price).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    err := orderRepo.UpdateOrder(updated, condition)
    assert.Nil(t, err)
}

3.注意点讲解

虽然添加了注释,我这边依旧讲一下修改的重点:

  1. gorm.DB作为一个初始化的参数,将其转变成一个依赖注入,使这块代码更具可测试性
  2. 查询和更新采用了一个新的结构体OrderFields,是用里面的fields声明了order中哪个字段生效

GORM框架的进一步扩展

通过这一次对GORM数据库相关代码的迭代,还是可以发现有些不足:

  1. 对复杂SQL的支持不足:如group by、子查询等语句
  2. 对field这块限制不好,id, nameprice,容易发生误填字段的问题
  3. 没有串联日志模块

接下来的模块,我会逐渐对2、3两点进行补充,而第1点需要有选择性地实现,我也会结合具体的场景进行分享。

总结

通过这一个小版本,我们让DAO这个与数据库交互模块的代码更具可读性(从调用侧可以清楚地了解到要做什么)、健壮性(单元测试)和可扩展性(对后续字段的扩展也很容易支持)。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
7月前
|
Web App开发 人工智能 JavaScript
主流自动化测试框架的技术解析与实战指南
本内容深入解析主流测试框架Playwright、Selenium与Cypress的核心架构与适用场景,对比其在SPA测试、CI/CD、跨浏览器兼容性等方面的表现。同时探讨Playwright在AI增强测试、录制回放、企业部署等领域的实战优势,以及Selenium在老旧系统和IE兼容性中的坚守场景。结合六大典型场景,提供技术选型决策指南,并展望AI赋能下的未来测试体系。
|
5月前
|
SQL 安全 Linux
Metasploit Pro 4.22.8-20251014 (Linux, Windows) - 专业渗透测试框架
Metasploit Pro 4.22.8-20251014 (Linux, Windows) - 专业渗透测试框架
299 1
Metasploit Pro 4.22.8-20251014 (Linux, Windows) - 专业渗透测试框架
|
5月前
|
Linux 网络安全 iOS开发
Metasploit Framework 6.4.95 (macOS, Linux, Windows) - 开源渗透测试框架
Metasploit Framework 6.4.95 (macOS, Linux, Windows) - 开源渗透测试框架
511 1
Metasploit Framework 6.4.95 (macOS, Linux, Windows) - 开源渗透测试框架
|
6月前
|
安全 Linux 网络安全
Metasploit Pro 4.22.8-2025091701 (Linux, Windows) - 专业渗透测试框架
Metasploit Pro 4.22.8-2025091701 (Linux, Windows) - 专业渗透测试框架
431 2
Metasploit Pro 4.22.8-2025091701 (Linux, Windows) - 专业渗透测试框架
|
6月前
|
Linux 网络安全 iOS开发
Metasploit Framework 6.4.90 (macOS, Linux, Windows) - 开源渗透测试框架
Metasploit Framework 6.4.90 (macOS, Linux, Windows) - 开源渗透测试框架
485 1
Metasploit Framework 6.4.90 (macOS, Linux, Windows) - 开源渗透测试框架
|
5月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
319 2
|
6月前
|
安全 Linux 网络安全
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
623 0
|
6月前
|
缓存 安全 Linux
Metasploit Pro 4.22.8-2025082101 (Linux, Windows) - 专业渗透测试框架
Metasploit Pro 4.22.8-2025082101 (Linux, Windows) - 专业渗透测试框架
263 0
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
785 6
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
402 1