Golang Go语言中有没有什么优雅的办法可以进行单元测试?

Golang Go语言中有没有什么优雅的办法可以进行单元测试?

刚刚入坑 golang 没多久,用 go 写了一个小的项目,逐渐感受到了 go 的特性和优点
为了增加项目的可靠性,想写一点单元测试,但是发现了重重阻碍,主要是在 mock 的时候实在是太复杂了
个人体验上感觉,无法做到无侵入性的 mock 某些 func 或者 struct 。
下面写一下自己的做法,不知道是因为我原本代码结构设计的就不对还是 mock 的姿势不对,希望大家指正

函数 mock 。

在一般的使用中假设是这样的

func BaseFunc() {
	info := getInfo("123")
	fmt.Printf(info)
}

func getInfo(name string) string { return name + “.cn” }

func usage() { BaseFunc() }

如果我需要 mock,就得

type getInfoFunc func(string) string

func BaseWithMock(getInfoV getInfoFunc) { info := getInfoV(“123”) fmt.Printf(info) }

func mockGetInfo(name string) string { return name + “.com” }

func usage() { // 正常调用 BaseWithMock(getInfo) // mock BaseWithMock(mockGetInfo) }

那么就存在问题了,如果我的 baseFunc 当中有很多数据库或者 API 接口,在单元测试的时候我需要 mock 他们的数据,
我就必须要定义很多个 type,然后在 BaseFunc 的参数里传进来么?感觉这么做很不优雅。

如果试图去 mock 一个对象,我感觉就更复杂了..
以下是某种简化过的场景..
假设 ServiceRecord 代表一系列数据库和 API 等数据操作
Service 则代表具体处理的对象,那么如果 Service 需要通过 ServiceRecord 读取某些基础信息的场景。
一般情况下,我是这样写的

type ServiceRecord struct {
	Name   string
	Fields map[string]string
}

func (s *ServiceRecord) LoadFields() { // some database work result := map[string]string{ “name”: “jack”, “address”: “no.1 jack street”, “remark”: “none”, } s.Fields = result }

type Service struct { Name string Owner string }

func (s *Service) ReadMoreInfo() { r := &ServiceRecord{Name: s.Name} r.LoadFields() s.Owner = r.Fields[“name”] }

func usage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfo() fmt.Print(s.Owner) }

如果需要进行单元测试,我们需要 mock 掉数据层,也就是 ServiceRecord 这个对象。 一般是通过 Interface 来实现这件事情。

type ServiceRecordInterface interface {
    LoadFields()
    // 因为 Interface 本身不包含数据,所以原来的所有直接访问属性的地方,都必须使用函数来实现
	GetField(string) string
}

func (s *ServiceRecord) GetField(fieldName string) string { return s.Fields[fieldName] }

// 为了 Mock,需要通过参数把接口传进来 func (s *Service) ReadMoreInfo(serviceRecord ServiceRecordInterface) { serviceRecord.LoadFields() s.Owner = serviceRecord.GetField(“name”) }

// 正常调用时 func usage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfoForMock(&ServiceRecord{Name: serviceName}) fmt.Printf(s.Owner) }

// mock 对象 type ServiceRecordMock struct { ServiceRecord }

// mock 掉具体的函数实现 func (srm *ServiceRecordMock) LoadFields() { result := map[string]string{ “name”: “tony”, “address”: “no.1 tony street”, “remark”: “none”, } srm.Fields = result }

// mock 时 func mockUsage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfoForMock(&ServiceRecordMock{ServiceRecord{Name: serviceName}}) fmt.Printf(s.Owner) }

可以看出这仍然对原来的代码产生了很大的影响,为了满足可 mock,必须要声明一个接口,而且必须把这个接口抽离出来,作为参数注入到调用对象里面去。
让我觉得很尴尬的是,采用接口之后,必须通过函数去 get 或者 set 一个属性,感觉很不优雅,产生了很多没有必要的垃圾代码。


更多关于Golang Go语言中有没有什么优雅的办法可以进行单元测试?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复

关注一下,我也有这个问题,我最近转 go,接触新项目,想写些单元测试,发现只要涉及业务的就关联太多,很难实现,可能项目设计之初就没有考虑单元测试,但是如果考虑的话,项目结构设计应该遵循哪些原则呢?

更多关于Golang Go语言中有没有什么优雅的办法可以进行单元测试?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


单元测试需要 mock 的很少, 一般直接测试函数输入输出, 直接给用例测.

那有数据或者外部依赖的函数怎么办呢?不测了么?…

关于 Go 的单元测试,一下是我的几个思考点:

1. 基于 interface 的 mock,这是面向接口可插拔,已经很成熟,就是实现对应的 mock 接口实现注入,具体方法 mock,具体不说了

2. 面向方法的 mock,使用 context 模型,进行 mock 方法注入

举个例子:

https://play.golang.org/p/z5RDSVcTSWD

以上,可以做到针对函数精确的 mock,按需实现

面向数据库的 mock 的话直接 mock 掉底层的 db 对象, 底层的 db 对象不是 interface 的话我一般本地起一个数据库或者 sqlite3 做 mock 数据源, 这样就只需要构造数据或者 sql 文件, 缺点是对 ci/cd 不友好. 不过你可以检查你 io 部分的函数有没有问题, 逻辑部分的函数直接构造输入输出 Test 就好了

看看 goframe

是的…只不过这么做更像是跑在本地的集成测试了,不那么像单测了

资源性的依赖,我更倾向不要 mock 资源,而是按需构建资源,其实代价很小
我做过一个测试项目,就是将所有资源 Docker 化,并且做好初始,结束动作
Mysql / Redis 都是调用 Docker,“资源即服务”,甚至可以构建一个 pool 来重复利用

原因在于,如果资源 mock,可以不能测试到使用资源的错误,比如一条 SQL 语句其实错了,但是不能发现

java 和 c#的特性才刚刚足够使用。对于 golang 这种丐版的语言,你还期望什么?

#9 没文档而已吧, 你看看例子

跑单元测试说明需要 mock 说明你的方法本身就不是纯函数,总归是需要一个环境(ctx)执行的.

看看楼上的讨论,也有了些结论:
1. 对外部环境、框架有严重依赖的代码,应该从核心业务逻辑中抽离出去,用集成测试覆盖。
2. 核心业务内部使用接口完成逻辑的组织,用单元测试覆盖。

哈哈,偶然发现这个问题,可以试试 https://github.com/xhd2015/xgo

在Go语言中,进行单元测试有多种优雅的方法,以下是一些推荐的做法:

  1. 使用Go内置的testing包

    • Go标准库提供了testing包,支持单元测试、基准测试和示例函数。
    • 测试文件以_test.go结尾,测试函数以Test开头,接受*testing.T类型的参数。
    • 使用go test命令运行测试,-v参数可以查看详细测试结果。
  2. 利用表驱动测试

    • 表驱动测试允许针对一系列输入和预期输出运行相同的测试逻辑,提高测试的效率和覆盖面。
    • 通过定义一个包含输入值和预期输出的结构体切片来组织测试数据。
  3. 使用测试框架

    • 如GoConvey,它兼容Go的testing包,提供丰富的测试断言规则和可读的彩色控制台输出,还支持Web UI界面查看测试结果。
  4. 处理并发测试

    • 使用goroutines启用并发执行,使用channels在goroutines之间进行通信。
    • 使用sync.WaitGroup同步goroutines,确保在断言结果之前所有goroutines完成。
    • 考虑竞争条件、执行顺序和隔离性,以确保测试的健壮性。

综上所述,Go语言提供了多种优雅的方法进行单元测试,可以根据具体需求选择适合的方法。同时,建议结合测试覆盖率分析,确保测试代码覆盖了大部分的逻辑。

回到顶部