Go:微服务架构下的单元测试(基于 Ginkgo、gomock 、Gomega)
创始人
2024-02-09 05:31:34

文章目录

    • 简介
    • 一、Ginkgo包的引入和简单介绍
    • 二、Dockertest 使用
    • 三、编写单元测试
      • 1. 编写 data 层的测试代码
    • 四、引入 gomock 包,mock 对象模拟依赖项
      • 1. 编写生成 mock 文件方法
      • 2. 编写 biz 层的测试方法
      • 3. 验证单元测试
    • 小结


简介

本文主要使用 Ginkgo[2] 、gomock[3] 、Gomega[4] 工具来实现单元测试,之前不了解的同学,可以先熟悉一下相关文档。

一、Ginkgo包的引入和简单介绍

$ go get github.com/onsi/ginkgo/v2/ginkgo
$ go get github.com/onsi/gomega

第一条命令是获取 ginkgo 并且安装 ginkgo 可执行文件到 $GOPATH/bin
–- 你需要在你电脑中把 $GOPATH 配置上,并配置上它。第二条命令安装了全部 gomega 库。这样可以导入 gomega 包到你的测试代码中:

import "github.com/onsi/gomega"
import "github.com/onsi/ginkgo"

Ginkgo 与 Go 现有的测试基础设施挂钩,可以使用 go test 运行 Ginkgo 套件。这同时意味着 Ginkgo 测试可以和传统 Go testing 测试一起使用。go test 和 ginkgo 都会运行你套件内的所有测试。

二、Dockertest 使用

使用 Dockertest 来完成咱们服务的 Golang 链接 DB 的集成测试。Dockertest 库提供了简单易用的命令,用于启动 Docker 容器并将其用于测试。简单理解 Dockertest 工具就是 使用 docker 创建一个容器并在测试运行结束后停止并删除。具体信息请查看 Dockertest 官方介绍[5]

安装 Dockertest

go get -u github.com/ory/dockertest/v3

编写 Dockertest 配置代码并将其用于测试,进入 service/user/internal/data/, 目录新建 docker_mysql.go 文件,编写代码如下:

package dataimport ("database/sql""fmt""github.com/ory/dockertest/v3" // 注意这个包的引入"log""time"
)func DockerMysql(img, version string) (string, func()) {return innerDockerMysql(img, version)
}// 初始化 Docker mysql 容器
func innerDockerMysql(img, version string) (string, func()) {// uses a sensible default on windows (tcp/http) and linux/osx (socket)pool, err := dockertest.NewPool("")pool.MaxWait = time.Minute * 2if err != nil {log.Fatalf("Could not connect to docker: %s", err)}// pulls an image, creates a container based on it and runs itresource, err := pool.Run(img, version, []string{"MYSQL_ROOT_PASSWORD=secret", "MYSQL_ROOT_HOST=%"})if err != nil {log.Fatalf("Could not start resource: %s", err)}conStr := fmt.Sprintf("root:secret@(localhost:%s)/mysql?parseTime=true", resource.GetPort("3306/tcp"))if err := pool.Retry(func() error {var err errordb, err := sql.Open("mysql", conStr)if err != nil {return err}return db.Ping()}); err != nil {log.Fatalf("Could not connect to docker: %s", err)}// 回调函数关闭容器return conStr, func() {if err = pool.Purge(resource); err != nil {log.Fatalf("Could not purge resource: %s", err)}}
}

使用 Ginkgo 编写链接 Dockertest 的测试代码,还是此目录下,新建 data_suite_test.go 文件,编写代码如下:代码中有详细的注释,这里就不过多解释了。

package data_testimport ("context""github.com/pkg/errors""gorm.io/gorm""testing""user/internal/conf""user/internal/data". "github.com/onsi/ginkgo". "github.com/onsi/gomega"
)// 测试 data 方法
func TestData(t *testing.T) {//  Ginkgo 测试通过调用 Fail(description string) 功能来表示失败// 使用 RegisterFailHandler 将此函数传递给 Gomega 。这是 Ginkgo 和 Gomega 之间的唯一连接点RegisterFailHandler(Fail)// 通知 Ginkgo 启动测试套件。如果您的任何 specs 失败,Ginkgo 将自动使 testing.T 失败。RunSpecs(t, "test biz data ")
}var cleaner func()      // 定义删除 mysql 容器的回调函数
var Db *data.Data       // 用于测试的 data
var ctx context.Context // 上下文// initialize  AutoMigrate gorm 自动建表的方法
func initialize(db *gorm.DB) error {err := db.AutoMigrate(&data.User{},)return errors.WithStack(err)
}// ginkgo 使用 BeforeEach 为您的 Specs 设置状态
var _ = BeforeSuite(func() {// 执行测试数据库操作之前,链接之前 docker 容器创建的 mysql//con, f := data.DockerMysql("mysql", "latest")con, f := data.DockerMysql("mariadb", "latest")cleaner = f // 测试完成,关闭容器的回调方法config := &conf.Data{Database: &conf.Data_Database{Driver: "mysql", Source: con}}db := data.NewDB(config)mySQLDb, _, err := data.NewData(config, nil, db, nil)if err != nil {return}if err != nil {return}Db = mySQLDberr = initialize(db)if err != nil {return}Expect(err).NotTo(HaveOccurred())
})// 测试结束后 通过回调函数,关闭并删除 docker 创建的容器
var _ = AfterSuite(func() {cleaner()
})

测试模拟数据库连接,还是此目录下运行 go test 命令,得到如下结果:
在这里插入图片描述

注:这里可以看到虽然 0 个Passed,但同时也是 0 个 Failed,这是因为咱们这里还没有进行测试,只是验证一下数据库是否连接成功,并未执行 CURD 之类的操作。这里运行可能比较慢,因为它会从docker hub 拉取 mysql 的镜像,本文使用的是 mariadb 的镜像,且我本机已经提前下载好了 mariadb:latest 镜像,如果你的电脑是苹果的M1处理器推荐你用 mariadb。

三、编写单元测试

漫长的准备工作终于完成了,接下来来正式编写单元测试的代码吧

1. 编写 data 层的测试代码

还是data目录下新建 user_test.go 文件,编写内容如下:

package data_testimport (. "github.com/onsi/ginkgo". "github.com/onsi/gomega""user/internal/biz""user/internal/data"
)var _ = Describe("User", func() {var ro biz.UserRepovar uD *biz.UserBeforeEach(func() {// 这里的 Db 是 data_suite_test.go 文件里面定义的ro = data.NewUserRepo(Db, nil)// 这里你可以引入外部组装好的数据uD = &biz.User{ID:       1,Mobile:   "13803881388",Password: "admin123456",NickName: "aliliin",Role:     1,Birthday: 693629981,}})// 设置 It 块来添加单个规格It("CreateUser", func() {u, err := ro.CreateUser(ctx, uD)Ω(err).ShouldNot(HaveOccurred())// 组装的数据 mobile 为 13803881388Ω(u.Mobile).Should(Equal("13803881388")) // 手机号应该为创建的时候写入的手机号})})

Ω 就是 gomega 包的语法,It 是 ginkgo 包的用法。

还是此目录下运行 go test 命令,得到如下结果:
在这里插入图片描述

四、引入 gomock 包,mock 对象模拟依赖项

// gomock 主要包含两个部分:gomock 库和辅助代码生成工具 mockgen
go get github.com/golang/mock  
go get github.com/golang/mock/gomock

1. 编写生成 mock 文件方法

修改 user/internal/biz/user.go 文件

package biz// 注意这一行新增的 mock 数据的命令
//go:generate mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
type UserRepo interface {CreateUser(context.Context, *User) (*User, error)
}

进入 biz 目录执行命令

mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo

这里是用 gomock 提供的 mockgen 工具生成要 mock 的接口的实现,在生成 mock 代码的时候,我们用到了 mockgen 工具,这个工具是 gomock 提供的用来为要mock的接口生成实现的。它可以根据给定的接口,来自动生成代码。

执行完之后,你会看到多出来 service/user/internal/mocks/mrepo/user.go 文件

2. 编写 biz 层的测试方法

biz层目录下,新增 biz_suite_test.go 文件,添加内容如下:

package biz_testimport ("context""github.com/golang/mock/gomock". "github.com/onsi/ginkgo". "github.com/onsi/gomega""testing"
)func TestBiz(t *testing.T) {RegisterFailHandler(Fail)RunSpecs(t, "biz user test")
}var ctl *gomock.Controller
var cleaner func()
var ctx context.Contextvar _ = BeforeEach(func() {ctl = gomock.NewController(GinkgoT())cleaner = ctl.Finishctx = context.Background()
})
var _ = AfterEach(func() {// remove any mockscleaner()
})

还是biz层目录下,新增 user_test.go 文件,添加内容如下:

package biz_testimport ("github.com/golang/mock/gomock". "github.com/onsi/ginkgo". "github.com/onsi/gomega""user/internal/biz""user/internal/mocks/mrepo"
)var _ = Describe("UserUsecase", func() {var userCase *biz.UserUsecasevar mUserRepo *mrepo.MockUserRepoBeforeEach(func() {mUserRepo = mrepo.NewMockUserRepo(ctl)userCase = biz.NewUserUsecase(mUserRepo, nil)})It("Create", func() {info := &biz.User{ID:       1,Mobile:   "13803881388",Password: "admin123456",NickName: "aliliin",Role:     1,Birthday: 693629981,}mUserRepo.EXPECT().CreateUser(ctx, gomock.Any()).Return(info, nil)l, err := userCase.Create(ctx, info)Ω(err).ShouldNot(HaveOccurred())Ω(err).ToNot(HaveOccurred())Ω(l.ID).To(Equal(int64(1)))Ω(l.Mobile).To(Equal("13803881388"))})})

3. 验证单元测试

还是 biz 层目录下运行 go test 命令,得到如下结果:
在这里插入图片描述

小结

到这一步 data 层测试 sql 语句的方法,biz 测试基本逻辑的方法已经编写完成并通过了测试了,service 层的单元测试大同小异,这里就不写了。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...