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

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 与此同时,我们也缺乏一个有效的手段来验证自己编写的相关代码。如果依靠连接到真实的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这个与数据库交互模块的代码更具可读性(从调用侧可以清楚地了解到要做什么)、健壮性(单元测试)和可扩展性(对后续字段的扩展也很容易支持)。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
10天前
|
SQL 安全 数据库连接
《Go 简易速速上手小册》第6章:错误处理和测试(2024 最新版)(上)
《Go 简易速速上手小册》第6章:错误处理和测试(2024 最新版)
38 1
|
1月前
|
运维 监控 Go
Go语言微服务实战与最佳实践
【2月更文挑战第14天】本文将深入探讨使用Go语言进行微服务实战中的最佳实践,包括服务拆分、API设计、并发处理、错误处理、服务治理与监控等方面。通过实际案例和详细步骤,我们将分享如何在Go语言环境中构建高效、稳定、可扩展的微服务系统。
|
2月前
|
Go API 数据安全/隐私保护
Go语言标准库概览:构建高效、可靠的软件开发基石
【2月更文挑战第8天】Go语言标准库是Go语言生态系统的重要组成部分,它为开发者提供了一系列高效、可靠的工具和函数,帮助开发者构建高质量的软件应用。本文将对Go语言标准库进行概览,介绍其组成特点、设计哲学以及使用场景与优势,帮助读者更好地理解和应用Go语言标准库。
|
2月前
|
测试技术 Go 开发者
go-carbon v2.3.8 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
27 0
|
8天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
2月前
|
Go 索引
Go 1.22 slices 库的更新:高效拼接、零化处理和越界插入优化
本文详细介绍了 Go 1.22 版本中 slices 库的更新内容,总结起来有三个方面:新增了 Concat 函数、对部分函数新增了零化处理的逻辑和对 Insert 函数进行了越界插入优化
95 1
Go 1.22 slices 库的更新:高效拼接、零化处理和越界插入优化
|
2天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
14 1
|
6天前
|
JSON 测试技术 API
Python的Api自动化测试使用HTTP客户端库发送请求
【4月更文挑战第18天】在Python中进行HTTP请求和API自动化测试有多个库可选:1) `requests`是最流行的选择,支持多种请求方法和内置JSON解析;2) `http.client`是标准库的一部分,适合需要低级别控制的用户;3) `urllib`提供URL操作,适用于复杂请求;4) `httpx`拥有类似`requests`的API,提供现代特性和异步支持。根据具体需求选择,如多数情况`requests`已足够。
11 3
|
10天前
|
Kubernetes Cloud Native Go
《Go 简易速速上手小册》第10章:微服务与云原生应用(2024 最新版)(下)
《Go 简易速速上手小册》第10章:微服务与云原生应用(2024 最新版)
50 0
|
10天前
|
Cloud Native 算法 Go
《Go 简易速速上手小册》第10章:微服务与云原生应用(2024 最新版)(上)
《Go 简易速速上手小册》第10章:微服务与云原生应用(2024 最新版)
38 0

热门文章

最新文章