Go 单元测试高效实践

2023-05-16

敏捷开发中有一个广为人知的开发方法就是 XP(极限编程),XP 提倡测试先行,为了将以后出现 bug 的几率降到最低,这一点与近些年流行的 TDD(测试驱动开发)有异曲同工之处。

在最开始做编程时,我总是忽略单元测试在代码中的作用,觉得编写单元测试的功夫都赶上甚至超越业务程序了。到后来,业务量越来越复杂,慢慢地,浮现一个问题,就是系统对于测试人员是一个黑盒,简单的测试无法保证系统所设计的东西都可以测试到⬇️
举两个最简单的例子:
系统设计的数据打点,是无法从功能业务上测试出来的,而对于测试人员,可能由于版本差异,用例未覆盖。
如果一个表中有两个字段,新用户过来更新一个字段之后,测另一个字段的功能时就不再以一个新用户的身份操作了。
在这样的情况下,如果开发人员没有对系统做完全的检查,就很可能出现问题。
就以上情况看,需要从开发人员的维度,对功能做一个“预期”测试,一个功能走过,应该输入什么,输出什么,哪些数据变动了,变动是否符合预期等等。

最近,公司业务基本都转入了 Go 做开发,在 Go 的整个业务处理上也日渐完善,而 Go 的单元测试用起来也十分顺手,所以做个小的总结。

一. Mock DB

在单元测试中,很重要的一项就是数据库的 Mock,数据库要在每次单元测试时作为一个干净的初始状态,并且每次运行速度不能太慢。

1. Mysql 的 Mock

这里使用到的是 github.com/dolthub/go-mysql-server 借鉴了这位大哥的方法 如何针对 MySQL 进行 Fake 测试

  • DB 的初始化
    在 db 目录下
type Config struct {
   DSN             string // write data source name.
   MaxOpenConn     int    // open pool
   MaxIdleConn     int    // idle pool
   ConnMaxLifeTime int
}

var DB *gorm.DB

// InitDbConfig 初始化Db
func InitDbConfig(c *conf.Data) {
   log.Info("Initializing Mysql")
   var err error
   dsn := c.Database.Dsn
   maxIdleConns := c.Database.MaxIdleConn
   maxOpenConns := c.Database.MaxOpenConn
   connMaxLifetime := c.Database.ConnMaxLifeTime
   if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
      QueryFields: true,
      NamingStrategy: schema.NamingStrategy{
         //TablePrefix:   "",   // 表名前缀
         SingularTable: true, // 使用单数表名
      },
   }); err != nil {
      panic(fmt.Errorf("初始化数据库失败: %s \n", err))
   }
   sqlDB, err := DB.DB()
   if sqlDB != nil {
      sqlDB.SetMaxIdleConns(int(maxIdleConns))                               // 空闲连接数
      sqlDB.SetMaxOpenConns(int(maxOpenConns))                               // 最大连接数
      sqlDB.SetConnMaxLifetime(time.Second * time.Duration(connMaxLifetime)) // 单位:秒
   }
   log.Info("Mysql: initialization completed")
}
  • fake-mysql 的初始化和注入
    在 fake_mysql 目录下
var (
   dbName    = "mydb"
   tableName = "mytable"
   address   = "localhost"
   port      = 3380
)

func InitFakeDb() {
   go func() {
      Start()
   }()
   db.InitDbConfig(&conf.Data{
      Database: &conf.Data_Database{
         Dsn:             "no_user:@tcp(localhost:3380)/mydb?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4",
         ShowLog:         true,
         MaxIdleConn:     10,
         MaxOpenConn:     60,
         ConnMaxLifeTime: 4000,
      },
   })
   migrateTable()
}

func Start() {
   engine := sqle.NewDefault(
      memory.NewMemoryDBProvider(
         createTestDatabase(),
         information_schema.NewInformationSchemaDatabase(),
      ))

   config := server.Config{
      Protocol: "tcp",
      Address:  fmt.Sprintf("%s:%d", address, port),
   }

   s, err := server.NewDefaultServer(config, engine)
   if err != nil {
      panic(err)
   }

   if err = s.Start(); err != nil {
      panic(err)
   }

}

func createTestDatabase() *memory.Database {
   db := memory.NewDatabase(dbName)
   db.EnablePrimaryKeyIndexes()
   return db
}

func migrateTable() {
// 生成一个user表到fake mysql中
   err := db.DB.AutoMigrate(&model.User{})
   if err != nil {
      panic(err)
   }
}

在单元测试开始,调用 InitFakeDb()即可

func setup() {
   fake_mysql.InitFakeDb()
}

2. Redis 的 Mock

这里用到的是 miniredis , 与之配套的Redis Client 是 go-redis/redis/v8,在这里调用 InitTestRedis() 注入即可

// RedisClient redis 客户端  
var RedisClient *redis.Client  
  
// ErrRedisNotFound not exist in redisconst ErrRedisNotFound = redis.Nil  
  
// Config redis config
type Config struct {  
   Addr         string  
   Password     string  
   DB           int  
   MinIdleConn  int  
   DialTimeout  time.Duration  
   ReadTimeout  time.Duration  
   WriteTimeout time.Duration  
   PoolSize     int  
   PoolTimeout  time.Duration  
   // tracing switch  
   EnableTrace bool  
}  
  
// Init 实例化一个redis client  
func Init(c *conf.Data) *redis.Client {  
   RedisClient = redis.NewClient(&redis.Options{  
      Addr:         c.Redis.Addr,  
      Password:     c.Redis.Password,  
      DB:           int(c.Redis.DB),  
      MinIdleConns: int(c.Redis.MinIdleConn),  
      DialTimeout:  c.Redis.DialTimeout.AsDuration(),  
      ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),  
      WriteTimeout: c.Redis.WriteTimeout.AsDuration(),  
      PoolSize:     int(c.Redis.PoolSize),  
      PoolTimeout:  c.Redis.PoolTimeout.AsDuration(),  
   })  
  
   _, err := RedisClient.Ping(context.Background()).Result()  
   if err != nil {  
      panic(err)  
   }  
  
   // hook tracing (using open telemetry)  
   if c.Redis.IsTrace {  
      RedisClient.AddHook(redisotel.NewTracingHook())  
   }  
  
   return RedisClient  
}  
  
// InitTestRedis 实例化一个可以用于单元测试的redis  
func InitTestRedis() {  
   mr, err := miniredis.Run()  
   if err != nil {  
      panic(err)  
   }  
   // 打开下面命令可以测试链接关闭的情况  
   // defer mr.Close()  
  
   RedisClient = redis.NewClient(&redis.Options{  
      Addr: mr.Addr(),  
   })  
   fmt.Println("mini redis addr:", mr.Addr())  
}

二. 单元测试

经过对比,我选择了 goconvey 这个单元测试框架
它比原生的go testing 好用很多。goconvey还提供了很多好用的功能:

  • 多层级嵌套单测
  • 丰富的断言
  • 清晰的单测结果
  • 支持原生go test

使用

go get github.com/smartystreets/goconvey
func TestLoverUsecase_DailyVisit(t *testing.T) {  
   Convey("Test TestLoverUsecase_DailyVisit", t, func() {  
      // clean  
      uc := NewLoverUsecase(log.DefaultLogger, &UsecaseManager{})  
  
      Convey("ok", func() {  
         // execute  
         res1, err1 := uc.DailyVisit("user1", 3)  
         So(err1, ShouldBeNil)  
         So(res1, ShouldNotBeNil)  
         // 第 n (>=2)次拜访,不应该有奖励,也不应该报错  
         res2, err2 := uc.DailyVisit("user1", 3)  
         So(err2, ShouldBeNil)  
         So(res2, ShouldBeNil)  
      })  
   })  
}
可以看到,函数签名和 go 原生的 test 是一致的
测试中嵌套了两层 Convey,外层new了内层Convey所需的参数 
内层调用了函数,对返回值进行了断言

这里的断言也可以像这样对返回值进行比较 So(x, ShouldEqual, 2)
或者判断长度等等 So(len(resMap),ShouldEqual, 2)

Convey的嵌套也可以灵活多层,可以像一棵多叉树一样扩展,足够满足业务模拟。


三. TestMain

为所有的 case 加上一个 TestMain 作为统一入口

import (  
"os"  
"testing"  
  
. "github.com/smartystreets/goconvey/convey"  
)  
  
func TestMain(m *testing.M) {  
   setup()  
   code := m.Run()  
   teardown()  
   os.Exit(code)
}
// 初始化fake db
func setup() {  
   fake_mysql.InitFakeDb()  
   redis.InitTestRedis()
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go 单元测试高效实践 的相关文章

  • docker服务器的图形显示方案

    问题描述 xff1a 一般docker实操时都是作为服务器 xff0c 以字符方式交互 xff0c 非常不方便 本人尝试各种图形解决方案 xff0c 最终找到完美方案 最初本人尝试过VNC和SSH方式 xff0c 最终被否定了 1 本来do
  • Centos7下使用CMake

    在进行需要提供跨平台服务的项目时 xff0c 最好有相应的跨平台项目构建工具 本文所述的CMake即其中比较好用的跨平台构建工具之一 下文主要以C 43 43 语言为例进行使用演示 安装C 43 43 所需的环境 xff1a yum ins
  • 树莓派+神经计算棒2实时人脸检测

    树莓派配置摄像头 sudo apt get install python opencv sudo apt get install fswebcam 配置摄像头 sudo nano etc modules 查看树莓派CPU型号 cat pro
  • 学习总结-编写自己的CMakeLists.txt

    cmake minimum required span class token punctuation span VERSION span class token number 3 3 span span class token punct
  • 7.4V锂电池USB平衡充电器 串联锂电池充电器

    7 4V锂电池USB平衡充电器 串联锂电池充电器 本文介绍一种简单实用的串联锂电池充电器 大家知道 xff0c 串联电池的充电 xff0c 是一个麻烦的问题 如果直接拿7 4V来充 xff0c 可能会因为两颗电池的参数差异 xff0c 会导
  • 【Echarts】数据可视化完成大屏地图(拓展乡镇地区)的绘制

    绘制地图要素 地图边缘 地理位置 xff08 中心点或者自定义的未知 xff09 echarts绘制 实现在前 成品展示放在最后 代码太长 xff0c 参考代码可见Github Github地址 获取地图 获取精确到乡镇街道的地图JSON数
  • K8s问题【flannel一直重启问题,CrashLoopBackOff】

    kubectl describe 命令查看 Events Type Reason Age From Message Normal Scheduled 13m default scheduler Successfully assigned k
  • Python openpyxl库

    1 读写单元格 span class token keyword from span openpyxl span class token keyword import span load workbook wb span class tok
  • 子网掩码

    子网掩码 subnet mask 是每个网管必须要掌握的基础知识 xff0c 只有掌握它 xff0c 才能够真正理解TCP IP协议的设置 以下我们就来深入浅出地讲解什么是子网掩码 IP地址的结构 要想理解什么是子网掩码 xff0c 就不能
  • AS学习网址大全

    都是我在学习过程中精心收集的 xff0c 大部分为国内网站 xff0c 绝对是您学习AS最好的去处 本贴于2010年3月22日再次更新 xff0c 并新加了很多好的网站 1 同时将网址全都贴出来 xff0c 方便不想下载的朋友使用 2 附件
  • 紧耦合和松耦合有什么区别

  • 我的大一学习生活总结

    今天最后的一科英语考完了 xff0c 但此刻的我并不觉的轻松 xff0c 我知道从现在开始就标志着我的大一已经结束了 xff0c 在大学仅有的四年时光就过去了四分之一 回想起大一这一年 xff0c 自问一下我到底学到了什么 xff1f 我发
  • 阿里云导出raw文件如何还原查看及centos7系统密码破解

    1 Raw格式转换 1 1 格式介绍 目前阿里云ecs镜像文件的导出格式默认为 raw tar gz xff0c 解压后为 raw格式 raw为最原始的虚拟机镜像文件 xff0c vmdk是vmware Virtual Box的虚拟机镜像文
  • 5.33 综合案例2.0 -ESP32拍照上传阿里云OSS

    综合案例2 0 ESP32拍照上传阿里云OSS 案例说明连线功能实现1 阿里云平台连接2 OSS对象存储服务3 ESP32 CAM开发环境4 代码ESP32 CAM开发板代码HaaS506开发板代码 测试数据转图片方法 案例说明 使用ESP
  • 'grep' 不是内部或外部命令,也不是可运行的程序或批处理文件

    使用 grep 来过滤 xff1a adb shell pm list packages grep qq 然后就报了 39 grep 39 不是内部或外部命令 xff0c 也不是可运行的程序或批处理文件 xff0c 后来发现根本不是grep
  • 一个程序员的一生

    一个程序员的一生 作者 佚名 我在程序员的时候 xff0c 我一开始追逐这个API怎么用 xff0c 数据库SQL怎么写更优化 xff0c Dcom技术的细节 xff0c 然后我发现我写出来的产品为了符合客户 需求必须要大量修改 xff0c
  • 搭建Ubuntu Samba服务器(超简单)

    1 xff09 安装samba服务 sudo apt get install samba 2 xff09 配置samba sudo vim etc samba smb conf share comment 61 myshare path 6
  • Nginx-配置HTTPS证书(单向认证)

    目录 一 生成 CA 私钥 1 生成一个 CA 私钥 ca key 二 生成CA 的数字证书 1 生成一个 CA 的数字证书 ca crt 三 生成 server 端数字证书请求 1 生成 nginx 端的私钥 nginx key 2 生成
  • 数据结构—B+树

    1 约束 B 43 树的约束与 B 树类似 xff0c 一棵 m m m 阶 B 43 树具有如下特点 xff1a xff08 1 xff09 根节点要么是一个叶节点 xff0c 要么至少具有两个孩子节点 xff1b xff08 2 xff
  • 服务端三种方式实现单设备登录

    单设备登录 xff0c 顾名思义 xff0c 一个账号在一个app中只能在一个设备上进行登录 使用的场景例如 xff1a 账号多端登录时云存档的一致性问题 单设备登录常用的方法 xff1a 1 web端 xff0c session 43 c

随机推荐