Go-kratos 框架商城微服务实战之用户服务 (二) 单元测试

Go-kratos 框架商城微服务实战之用户服务 (二)

这篇主要编写单元测试。文章写的不清晰的地方可通过 GitHub 源码 查看, 也感谢您指出不足之处。

注:横排 … 为代码省略,为了保持文章的篇幅简洁,我会将一些不必要的代码使用横排的 . 来代替,如果你在复制本文代码块的时候,切记不要将 . 也一同复制进去。

准备工作

本文主要使用 GinkgogomockGomega 工具来实现单元测试,之前不了解的同学,可以先熟悉一下相关文档。

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 官方介绍

安装 Dockertest

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

编写 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)
        }
    }
}

使用 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()
})

测试模拟数据库连接,还是此目录下运行 go test 命令,得到如下结果:

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

编写单元测试

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

编写 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")) // 手机号应该为创建的时候写入的手机号
    })

})

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

还是此目录下运行 go test 命令,得到如下结果:

此时可以看到共有 1 个 Passed 通过。
提示: 类似 packets.go:37 : unexpected EOF 错误,是因为 docker 模拟数据库链接导致的。

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

具体的使用方法可以参考这篇 gomock 的使用文章

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

编写生成 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 文件

编写 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()
})

还是 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"))
    })

})

验证单元测试

还是 biz 层目录下运行 go test 命令,得到如下结果:

结束语

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

感谢您的耐心阅读,动动手指点个赞吧。

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
本帖由系统于 2年前 自动加精
讨论数量: 12

kratos 的单测要怎么做呢?

1年前 评论
Aliliin (楼主) 1年前
kphcdr (作者) 1年前
Aliliin (楼主) 1年前

避坑

[root@localhost biz]# mockgen -source=./user.go  -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
bash: mockgen: 未找到命令...

执行go get github.com/github/hub 安装go相关组件时报错

[root@localhost biz]# go get github.com/onsi/ginkgo/v2/ginkgo
go get: installing executables with 'go get' in module mode is deprecated.
    To adjust and download dependencies of the current module, use 'go get -d'.
    To install using requirements of the current module, use 'go install'.
    To install ignoring the current module, use 'go install' with a version,
    like 'go install example.com/cmd@latest'.
    For more information, see https://golang.org/doc/go-get-install-deprecation
    or run 'go help get' or 'go help install'.

Go1.17版使用go install安装依赖

go get 用于下载并安装 Go 包、命令等,而 go install 在 module 时代几乎很少使用,在 GOPATH 年代,go install 用来编译安装本地项目。

自 1.16 起,官方说,不应该 go get 下载安装命令(即可执行程序),不过只是这么说,却依然可以使用。

但 Go1.17 开始,如果使用 go get 安装命令会告警。

解决方案

# Go1.16及以前
go get -u -v github.com/github/hub

# Go1.17版本
go install github.com/bingoohuang/hub@latest
1年前 评论

感觉这种测试方式对新手有点复杂,我用github.com/stretchr/testify

1年前 评论
Aliliin (楼主) 1年前

老哥,问一下哈,在data层做es的单元测试的话,dockertest这个工具可以使用吗?还是说这个工具就是单给mysql使用的?网上找的都是mysql的例子,我自己搞了半天es没有搞出来,有的话,方便给个例子吗?感谢!

1年前 评论
Aliliin (楼主) 1年前
Aliliin (楼主) 1年前
HeroSong (作者) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!