使用gomock对golang项目进行单元测试

熟悉golang的工程师应该都会利用golang自带的go test工具对自己的代码进行单元测试,go test除了能够自动的进行单元测试、输出格式化结果之外,还可以输出对应的覆盖率统计,借助覆盖率统计信息,我们可以看到单测中覆盖到和没有覆盖到的代码行,从而对单测进行一定的优化。

gomock其实也是一个官方的、用于优化单测的工具。

gomock用在什么地方

以下我们以一个例子说明什么情况下需要用到gomock这个工具

src/fetcher/fetcher.go

package fetcher

import (
   "fmt"
   "net/http"
)

type Fetcher interface {
   Get(string) (*http.Response, error)
}

func (httpFetcher *HttpFetcher) Get(query string) (*http.Response, error) {
	url := fmt.Sprintf("%s://%s:%d/%s", httpFetcher.Protocol, httpFetcher.Host, httpFetcher.Port, query)
	return http.Get(url)
}

type HttpFetcher struct {
   Host string
   Port int
   Protocol string
}

func ServiceCheck(query string, fetcher Fetcher) {
   resp, err := fetcher.Get(query)
   if err != nil {
      // todo: do something record
      fmt.Printf("%v", err)
   }

   if resp.StatusCode == 404 {
      // todo: do something record
      fmt.Println("status code is 404")
      return
   } else if resp.StatusCode == 400 {
      // todo: do something record
      fmt.Println("status code is 400")
      return
   } else if resp.StatusCode != 200 {
      // todo: do something record
      return
   }

   // todo: do something record for status code 200
   return
}

在fetcher.go函数中,定义了Fetcher这个接口类型,HttpFetcher实现了这个接口类型。而ServiceCheck这个函数,可以当作一个服务监控函数,它监控一个对应的url,当这个url返回的结果不为200时,它可以做一些记录或者调用某一个回调函数去报警。现在,如果我们需要对ServiceCheck这个函数去做单测,我们如何才能让我们的单测能够覆盖到请求结果的状态吗不为200的这些分支呢?

甚至先不考虑这些非200分支,你能不能保证自己的单测能够不受环境影响、独立的运行?如果你在单测的时候提供一个指向测试环境服务的url,在你自己的测试环境下自然可以得到响应,但是离开这个测试环境,你的单测还能不能稳定的运行?

考虑到这些问题,我们需要一个mock工具,来解放在这个场景下对网络服务的依赖。如果我们可以mock出一个对象来替代HttpFetcher,并且重写对应的Get函数,就可以按照我们的想法,输出任意的*http.Response对象。

gomock介绍

gomock 是官方提供的 mock 框架,同时还提供了 mockgen 工具用来辅助生成测试代码。

使用如下命令即可安装:

go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen

安装结束之后,$PATH下会新增一个mockgen的可执行文件,通过以下命令:

mockgen -source=./fetcher.go -destination=./mock/mock_fetcher.go -package=mock

可以对Fetcher这个接口生成对应的mock对象,我们不需要关注生成mock_fetcher.go文件的内容(看不懂,而且在使用中不需要关心)。随后对fetcher.go写单测:

src/fetcher/fetcher_test.go

package fetcher

import (
   "net/http"
   "testing"
   fetcherMock"awesomeProject/fetcher/mock"
   "github.com/golang/mock/gomock"
)

func TestHttpFetcher_Get_404(t *testing.T) {
   query := "v4/resolve?dn=www.baidu.com&account_id=100195&ip=1.1.1.1&sign=9cc9723684867d" +
      "5b5eb943431220790a&t=1866666666&cuid=xxxxxyyyyyzzzzz&type=dual_stack"

   ctl := gomock.NewController(t)
   defer ctl.Finish()

   mock_resp := new(http.Response)
   mock_resp.StatusCode = 404
   mockFetcher := fetcherMock.NewMockFetcher(ctl)
   mockFetcher.EXPECT().Get(query).Return(mock_resp, nil)

   ServiceCheck(url, mockFetcher)
}

对应的目录树结构为:

├── fetcher.go
├── fetcher_test.go
└── mock
└── mock_fetcher.go

  1. gomock.NewController:返回 gomock.Controller,它代表 mock 生态系统中的顶级控件。定义了 mock 对象的范围、生命周期和期待值。另外它在多个 goroutine 中是安全的
  2. mock.NewMockFetcher:创建一个新的 mock 实例
  3. mockFetcher.EXPECT().Get(url).Return(mock_resp, nil):这里有三个步骤,EXPECT()返回一个允许调用者设置期望返回值的对象。Get(query) 是设置入参并调用 mock 实例中的方法。Return(mock_resp, nil) 是设置先前调用的方法出参。简单来说,就是设置入参并调用,最后设置返回值

在fetcher/目录下执行go test,有以下输出:

status code is 404 PASS ok awesomeProject/fetcher 0.586s

可见mock成功了。这样的mock方式称为"打桩",有明确的参数和对应的返回值。除此之外还有其它的打桩方式,有需要的可以自己去查询。

编写可测试的代码

如果仔细看以上的用例,可以发现gomock只能mock一个满足interface的类型,它对一个具体的类型是无能为力的。比如,如果我们使用这个函数定义:

func ServiceCheck(query string, fetcher HttpFetcher)

gomock是没办法mock出一个HttpFetcher实例的。而代码的可测试性和代码的可读性、高效性同等重要,因此,将依赖抽象为接口类型,不仅可以降低耦合度,还有利于写出方便测试的代码。

如果再仔细看这个函数,会发现它同样使用了依赖注入的方式降低耦合性。如果Fetcher的实例放在ServiceCheck函数内部去初始化,那么不仅是函数可测试性大打折扣,某个实现了Fetcher的类型也与ServiceCheck函数形成了紧耦合。

posted @ 2020-08-18 21:19  Fucky  阅读(2654)  评论(0编辑  收藏  举报