Go单元测试

3,034 阅读5分钟

Go中写单元测试一直没有找到比较优雅的方式,另一方面在业务开发中一直觉得代码会经常变更就懒于去写单元测试,最近工作上有个服务,作为最基础的用户服务,追求稳定高质量,并且大概率不会经常改动,比较适合开发完整的单元测试,于是计划将单元测试的覆盖率提升到90+。

基本测试操作

懒,就不写了吧。

涉及io操作的单元测试

不可避免的需要mock函数中的io操作,可以使用github.com/bouk/monkey

我的单元测试原则,

1、怎么简单怎么来,避免增加代码的复杂度,也就是直接使用go test原始方式

2、如果涉及到io操作,我会使用monkey包来替换函数,当然也可以使用testify来mock函数。

3、涉及全局变量的话,gomonkey来实现,这个包提供了常见的单元测试patch功能,也能够来替换monkey来替换函数,感兴趣的小伙伴可以试试。

另外,我是通过看下面的这四篇文章学习单元测试的,可能已经过时了,不过在这里,标记一下。

单元测试覆盖率

go test -v -covermode=count -coverprofile=coverage.out -coverpkg ./... ./...
  • covermode 单元测试结果统计说明,covermode可以设置3个值
set: 只包含某一行是否被执行。
count: 某一行被执行过多少次
atomic: 同count,但是用于并发的场景
  • coverprofile 生成单元测试文件

设置覆盖信息的输出文件,覆盖信息包含了哪些行被执行以及执行了几次的信息。

  • coverpkg 指定单元测试包

coverpkg是列举出要统计覆盖率的包,./...代表当前目录下的所有包,使用递归方式。

 ./...特别说明:第二个表示文件当前目录下的所有目录中的单元测试;第一个表示执行目录中的所有包,如果只想测试某个包,需要显示声明,包名为全路径,即其他的文件引用该包的方式。

# 查看某个包的单元测试覆盖率
go test -coverpkg user_info/user_info_server/client  ./...
# 查看某个路径下所有包单元测试覆盖率 
go test -coverpkg ./... user_info/user_info_server/handler/...

根据覆盖信息生成文件htmltxt文件。

go tool cover -html是根据覆盖信息文件来生成html形式的详细的可视化的页面。

go tool cover -func是根据覆盖信息文件来生成基于函数纬度的文本形式的可读的覆盖信息。

@go tool cover -html=coverage.out -o coverage.html
@go tool cover -func=coverage.out -o coverage.txt

有时候生成文件麻烦,可以直接在命令行中查看就行,如go tool cover -html=coverage.outgo tool cover -func=coverage.out

参考文档:

基于interface{}的测试方式,去Mock测试

下面这篇文章讲的不错,强调一下,说三遍。

编写可测试 Go 代码的一种模式

编写可测试 Go 代码的一种模式

编写可测试 Go 代码的一种模式

image.png

补充:可以看看dependency-injectionmocking

再举个例子,

package web

type Client struct{}

func NewClient() Client {
    return Client{}
}

func (c Client) GetData() (string, error) {
    return "data", nil
}
package foo

import (
    "errors"

    "interfaces/web"
)

func Controller() error {
    webClient := web.NewClient()
    fromWebAPI, err := webClient.GetData()
    if err != nil {
        return err
    }
    // do some things based on data from web API
    if fromWebAPI != "data" {
        return errors.New("unexpected data")
    }
    return nil
}
package foo_test

import (
    "testing"

    "interfaces/foo"
)

// 成功例子  
func TestController_Success(t *testing.T) {
    err := foo.Controller()
    if err != nil {
        t.FailNow()
    }
}

// 失败例子
func TestController_Failure(t *testing.T) {
    // 这里我们想返回错误,但似乎比较难。
    err := foo.Controller()
    if err == nil {
        // 这个测试将会fail :(
        t.FailNow()
    }
}

有两种情况,需要说明:

1、如果不mock数据,失败的情况不好模拟。

2、一般情况下,通过monkey函数mock数据。

如果使用interface{}就可以在不mock数据的情况下正常的调用,如下面的方式:

package foo

import (
    "errors"
)

type IWebClient interface {
    GetData() (string, error)
}

func Controller(webClient IWebClient) error {
    fromWebAPI, err := webClient.GetData()
    if err != nil {
        return err
    }
    // do some things based on data from web API
    if fromWebAPI != "data" {
        return errors.New("unexpected data")
    }
    return nil
}
package foo_test

import (
    "errors"
    "testing"

    "interfaces/foo"
)

type MockClient struct {
    GetDataReturn string
}

func (mc MockClient) GetData() (string, error) {
    return mc.GetDataReturn, nil
}

func TestController_Success(t *testing.T) {
    err := foo.Controller(MockClient{"data"})
    if err != nil {
        t.FailNow()
    }
}

type FailingClient struct{}

func (fc FailingClient) GetData() (string, error) {
    return "", errors.New("oh no")
}

func TestController_Failure(t *testing.T) {
    // GetData() 失败分支
    err := foo.Controller(FailingClient{})
    if err == nil {
        t.FailNow()
    }
    // 错误数据分支
    err = foo.Controller(MockClient{"not data"})
    if err == nil {
        t.FailNow()
    }
}

一些问题

1、存在某些情况替换原函数没有效果,可以试着增加-gcflags=-l来执行单元测试。

Golang中虽然没有inline关键字,但仍存在inline函数,一个函数是否是inline函数由编译器决定。inline函数的特点是简单短小,在源代码的层次看有函数的结构,而在编译后却不具备函数的性质。inline函数不是在调用时发生控制转移,而是在编译时将函数体嵌入到每一个调用处,所以inline函数在调用时没有地址。 inline函数没有地址的特性导致了Monkey框架的第一个缺陷:对inline函数打桩无效。

go test . -gcflags=-l --count=1 -v -test.run=TestHttp

2、如何避免已有的mock函数影响新的mock函数,取消所有的patch。

monkey.UnpatchAll()

3、使用monkey去mock gorm中的Select无法起作用,似乎是因为内联的原因。

4、在Goland查看当前单元测试覆盖情况,

image.png

效果类似,

image.png

这种方式实际上执行的命令如下,

GOROOT=/usr/local/go #gosetup
GOPATH=/Users/$username/go #gosetup
/usr/local/go/bin/go test -json -covermode=atomic -coverpkg=./... -coverprofile /Users/$username/Library/Caches/JetBrains/GoLand2020.1/coverage/xx.out ./... #gosetup

如果遇到内联就会报错,没有找到怎么设置-gcflags=-l来去内联。

于是可以通过,在GoLand指定Coverage Suite的方式,来执行Goland显示哪个文件的代码覆盖率。

使用快捷键(GUI没有找到入口):

Macos: command+option+f6 【如果是触摸条:command+option+fn+f6】
Windows: Ctrl+Alt+F6

image.png

添加生成的coverage.out文件的目录,每次文件变更的时候,Goland中的覆盖情况就会发生变化。

image.png

但是呢,下一次重新生成coverage.out的时候,Goland的覆盖样式没有啥变化,只有重新刷新Coverage Suite才行,于是又回到之前使用编译器本生命令。

具体操作方式: 点击菜单【Run】=>选择【Running】=>【Edit Configurations】之后得到下面的操作界面。

Program arguments中配置变量-gcflags=-l --count=1

image.png