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
目录
相关文章
|
5天前
|
Java
PTA帅到没朋友(Java语言)+测试点
PTA帅到没朋友(Java语言)+测试点
11 1
|
2天前
|
Ubuntu Unix Linux
【GO基础】1. Go语言环境搭建
【GO基础】1. Go语言环境搭建
|
3天前
|
JSON 前端开发 Go
lucky - go 语言实现的快速开发平台
go 语言实现的快速开发平台,自动生成crud代码,前端页面通过json配置,无需编写前端代码。
10 0
|
4天前
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
14 2
|
4天前
|
负载均衡 Go 调度
使用Go语言构建高性能的Web服务器:协程与Channel的深度解析
在追求高性能Web服务的今天,Go语言以其强大的并发性能和简洁的语法赢得了开发者的青睐。本文将深入探讨Go语言在构建高性能Web服务器方面的应用,特别是协程(goroutine)和通道(channel)这两个核心概念。我们将通过示例代码,展示如何利用协程处理并发请求,并通过通道实现协程间的通信和同步,从而构建出高效、稳定的Web服务器。
|
4天前
|
算法 Go 分布式数据库
构建高可用的分布式数据库集群:使用Go语言与Raft共识算法
随着数据量的爆炸式增长,单一数据库服务器已难以满足高可用性和可扩展性的需求。在本文中,我们将探讨如何使用Go语言结合Raft共识算法来构建一个高可用的分布式数据库集群。我们不仅会介绍Raft算法的基本原理,还会详细阐述如何利用Go语言的并发特性和网络编程能力来实现这一目标。此外,我们还将分析构建过程中可能遇到的挑战和解决方案,为读者提供一个完整的实践指南。
|
4天前
|
消息中间件 Go API
基于Go语言的微服务架构实践
随着云计算和容器化技术的兴起,微服务架构成为了现代软件开发的主流趋势。Go语言,以其高效的性能、简洁的语法和强大的并发处理能力,成为了构建微服务应用的理想选择。本文将探讨基于Go语言的微服务架构实践,包括微服务的设计原则、服务间的通信机制、以及Go语言在微服务架构中的优势和应用案例。
|
5天前
|
安全 测试技术 数据库连接
使用Go语言进行并发编程
【5月更文挑战第15天】Go语言以其简洁语法和强大的并发原语(goroutines、channels)成为并发编程的理想选择。Goroutines是轻量级线程,由Go运行时管理。Channels作为goroutine间的通信机制,确保安全的数据交换。在编写并发程序时,应遵循如通过通信共享内存、使用`sync`包同步、避免全局变量等最佳实践。理解并发与并行的区别,有效管理goroutine生命周期,并编写测试用例以确保代码的正确性,都是成功进行Go语言并发编程的关键。
|
5天前
|
数据采集 监控 Java
Go语言并发编程:Goroutines和Channels的详细指南
Go语言并发编程:Goroutines和Channels的详细指南
12 3
|
5天前
|
数据采集 人工智能 搜索推荐
快速入门:利用Go语言下载Amazon商品信息的步骤详解
本文探讨了使用Go语言和代理IP技术构建高效Amazon商品信息爬虫的方法。Go语言因其简洁语法、快速编译、并发支持和丰富标准库成为理想的爬虫开发语言。文章介绍了电商网站的发展趋势,如个性化推荐、移动端优化和跨境电商。步骤包括设置代理IP、编写爬虫代码和实现多线程采集。提供的Go代码示例展示了如何配置代理、发送请求及使用goroutine进行多线程采集。注意需根据实际情况调整代理服务和商品URL。
快速入门:利用Go语言下载Amazon商品信息的步骤详解

热门文章

最新文章