• Go:微服务架构下的单元测试(基于 Ginkgo、gomock 、Gomega)



    简介

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

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

    $ go get github.com/onsi/ginkgo/v2/ginkgo
    $ go get github.com/onsi/gomega
    
    • 1
    • 2

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

    import "github.com/onsi/gomega"
    import "github.com/onsi/ginkgo"
    
    • 1
    • 2

    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
    
    • 1

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

    package data
    
    import (
     "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 * 2
     if err != nil {
      log.Fatalf("Could not connect to docker: %s", err)
     }
     
     // pulls an image, creates a container based on it and runs it
     resource, 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 error
      db, 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)
      }
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

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

    package data_test
    
    import (
     "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 = mySQLDb
     err = initialize(db)
     if err != nil {
      return
     }
     Expect(err).NotTo(HaveOccurred())
    })
    
    // 测试结束后 通过回调函数,关闭并删除 docker 创建的容器
    var _ = AfterSuite(func() {
     cleaner()
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

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

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

    三、编写单元测试

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

    1. 编写 data 层的测试代码

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

    package data_test
    
    import (
     . "github.com/onsi/ginkgo"
     . "github.com/onsi/gomega"
     "user/internal/biz"
     "user/internal/data"
    )
    
    var _ = Describe("User", func() {
     var ro biz.UserRepo
     var uD *biz.User
     BeforeEach(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")) // 手机号应该为创建的时候写入的手机号
     })
    
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

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

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

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

    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)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    进入 biz 目录执行命令

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

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

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

    2. 编写 biz 层的测试方法

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

    package biz_test
    
    import (
     "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.Context
    
    var _ = BeforeEach(func() {
     ctl = gomock.NewController(GinkgoT())
     cleaner = ctl.Finish
     ctx = context.Background()
    })
    var _ = AfterEach(func() {
     // remove any mocks
     cleaner()
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    package biz_test
    
    import (
     "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.UserUsecase
     var mUserRepo *mrepo.MockUserRepo
    
     BeforeEach(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"))
     })
    
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    3. 验证单元测试

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

    小结

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

  • 相关阅读:
    深度剖析Java HashMap:源码分析、线程安全与最佳实践
    2022极端高温!人工智能如何预测森林火灾?| 万物AI
    Git clone Unsupported proxy syntax in ‘proxy:port‘
    Process assessment techniques-3
    HBase数据存储
    如何利用客户旅程打造好的用户体验?
    【计算机网络】传输层协议——TCP(上)
    go语言学习
    初识操作系统
    Deep Dual-resolution Network 原理与代码解析
  • 原文地址:https://blog.csdn.net/zhanggqianglovec/article/details/127996326