语雀:https://www.yuque.com/konglingfei-vzag4/onex/mvdrrn6vcyd0ewif
有了组织合理的代码结构、符合Go语言代码规范的Go应用代码之后,我们还需要通过一些手段来确保我们开发出的是一个高质量的代码,这可以通过单元测试和Code Review来实现。
单元测试非常重要。我们开发完一段代码后,第一个执行的测试就是单元测试。它可以保证我们的代码是符合预期的,一些异常变动能够被及时感知到。进行单元测试,不仅需要编写单元测试用例,还需要我们确保代码是可测试的,以及具有一个高的单元测试覆盖率。
代码质量:编写可测试的代码
接下来,我就来介绍下如何编写一个可测试的代码。
如果我们要对函数A进行测试,并且A中的所有代码均能够在单元测试环境下按预期被执行,那么函数A的代码块就是可测试的。我们来看下一般的单元测试环境有什么特点:
- 可能无法连接数据库。
- 可能无法访问第三方服务。
如果函数A依赖数据库连接、第三方服务,那么在单元测试环境下执行单元测试就会失败,函数就没法测试,函数是不可测的。
解决方法也很简单:将依赖的数据库、第三方服务等抽象成接口,在被测代码中调用接口的方法,在测试时传入mock类型,从而将数据库、第三方服务等依赖从具体的被测函数中解耦出去。如下图所示:
为了提高代码的可测性,降低单元测试的复杂度,对function和mock的要求是:
- 要尽可能减少function中的依赖,让function只依赖必要的模块。编写一个功能单一、职责分明的函数,会有利于减少依赖。
- 依赖模块应该是易Mock的。
为了协助你理解,我们先来看一段不可测试的代码:
package post
import "google.golang.org/grpc"
type Post struct {
Name string
Address string
}
func ListPosts(client *grpc.ClientConn) ([]*Post, error) {
return client.ListPosts()
}这段代码中的ListPosts函数是不可测试的。因为ListPosts函数中调用了client.ListPosts()方法,该方法依赖于一个gRPC连接。而我们在做单元测试时,可能因为没有配置gRPC服务的地址、网络隔离等原因,导致没法建立gRPC连接,从而导致ListPosts函数执行失败。
下面,我们把这段代码改成可测试的,如下:
package main
type Post struct {
Name string
Address string
}
type Service interface {
ListPosts() ([]*Post, error)
}
func ListPosts(svc Service) ([]*Post, error) {
return svc.ListPosts()
}上面代码中,ListPosts函数入参为Service接口类型,只要我们传入一个实现了Service接口类型的实例,ListPosts函数即可成功运行。因此,我们可以在单元测试中可以实现一个不依赖任何第三方服务的fake实例,并传给ListPosts。上述可测代码的单元测试代码如下:
package main
import "testing"
type fakeService struct {
}
func NewFakeService() Service {
return &fakeService{}
}
func (s *fakeService) ListPosts() ([]*Post, error) {
posts := make([]*Post, 0)
posts = append(posts, &Post{
Name: "colin",
Address: "Shenzhen",
})
posts = append(posts, &Post{
Name: "alex",
Address: "Beijing",
})
return posts, nil
}
func TestListPosts(t *testing.T) {
fake := NewFakeService()
if _, err := ListPosts(fake); err != nil {
t.Fatal("list posts failed")
}
}当我们的代码可测之后,就可以借助一些工具来Mock需要的接口了。常用的Mock工具,有这么几个:
- golang/mock,是官方提供的Mock框架。它实现了基于interface的Mock功能,能够与Golang内置的testing包做很好的集成,是最常用的Mock工具。golang/mock提供了mockgen工具用来生成interface对应的Mock源文件。
- sqlmock,可以用来模拟数据库连接。数据库是项目中比较常见的依赖,在遇到数据库依赖时都可以用它。
- httpmock,可以用来Mock HTTP请求。
- bouk/monkey,猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现。如果golang/mock、sqlmock和httpmock这几种方法都不能满足我们的需求,我们可以尝试通过猴子补丁的方式来Mock依赖。可以这么说,猴子补丁提供了单元测试 Mock 依赖的最终解决方案。
代码质量:高单元测试覆盖率
接下来,我们再一起看看如何提高我们的单元测试覆盖率。
当我们编写了可测试的代码之后,接下来就需要编写足够的测试用例,用来提高项目的单元测试覆盖率。这里我有以下两个建议供你参考:
- 使用gotests工具自动生成单元测试代码,减少编写单元测试用例的工作量,将你从重复的劳动中解放出来。
- 定期检查单元测试覆盖率。你可以通过以下方法来检查:
$ go test -race -cover -coverprofile=./coverage.out -timeout=10m -short -v ./...
$ go tool cover -func ./coverage.out执行结果如下:
在提高项目的单元测试覆盖率时,我们可以先提高单元测试覆盖率低的函数,之后再检查项目的单元测试覆盖率;如果项目的单元测试覆盖率仍然低于期望的值,可以再次提高单元测试覆盖率低的函数的覆盖率,然后再检查。以此循环,最终将项目的单元测试覆盖率优化到预期的值为止。
这里要注意,对于一些可能经常会变动的函数单元测试,覆盖率要达到100%。
说完了单元测试,我们再看看如何通过Code Review来保证代码质量。
Code Review可以提高代码质量、交叉排查缺陷,并且促进团队内知识共享,是保障代码质量非常有效的手段。在我们的项目开发中,一定要建立一套持久可行的Code Review机制。
但在我的研发生涯中,发现很多团队没有建立有效的Code Review机制。这些团队都认可Code Review机制带来的好处,但是因为流程难以遵守,慢慢地Code Review就变成了形式主义,最终不了了之。其实,建立Code Review机制很简单,主要有3点:
- 首先,确保我们使用的代码托管平台有Code Review的功能。比如,GitHub、GitLab这类代码托管平台都具备这种能力。
- 接着,建立一套Code Review规范,规定如何进行Code Review。
- 最后,也是最重要的,每次代码变更,相关开发人员都要去落实Code Review机制,并形成习惯,直到最后形成团队文化。
确保代码性能
项目上线前,我们还需要通过性能优化、并发优化、压力测试等手段,来保证接口的性能:
- 性能优化:项目上线前要对接口性能进行测试,确保接口延时在预期范围内,一般建议是 < 500ms。如果接口性能不符合预期,还要进行性能优化。Go 代码优化一般使用pprof工具。
- 并发优化:测试 Go 程序的并发能力,根据预期优化 Go 的能发能力
- 压力测试:通过压力测试查找性能阈值,并具此评估需要的系统资源、副本数等。