Go 工程管理-Test

2021/10/31 Go

单元测试

**单元测试:顾名思义针对单元进行测试,一个单元指的是函数、模块等;

当每个最小单元都被测试通过,那么整个模块甚至整个程序都是可以被验证通过;

#单元测试 需要遵循五点规则。

  • 含有单元测试代码的 go 文件必须以 _test.go 结尾,Go 语言测试工具只认符合这个规则的文件。

  • 单元测试文件名 _test.go 前面的部分最好是被测试的函数所在的 go 文件的文件名,比如以上示例中单元测试文件叫 main_test.go,因为测试的 Fibonacci 函数在 main.go 文件里。

  • 单元测试的函数名必须以 Test 开头,是可导出的、公开的函数。

  • 测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值。

  • 函数名最好是 Test + 要测试的函数名,比如例子中是 TestFibonacci,表示测试的是 Fibonacci 这个函数。

单元测试的重点在于熟悉业务逻辑、 场景等,以便尽可能全面的测试,确保代码质量。

func Fibonacci(n int) int {

   if n < 0 {

      return 0

   }

   if n == 0 {

      return 0

   }

   if n == 1 {

      return 1

   }

   return Fibonacci(n-1) + Fibonacci(n-2)

}

单元测试覆盖率:

$ go test -v --coverprofile=ch18.cover ./ch18
PASS

coverage: 85.7% of statements

ok      gotour/ch18     0.367s  coverage: 85.7% of statements

-coverprofile 这个 Flag,它可以得到一个单元测试覆盖率文件,运行这行命令还可以同时看到测试覆盖率

测试覆盖率为 85.7%。从这个数字来看,应该没有被全面地测试,这时候就需要查看详细的单元测试覆盖率报告了。

➜ go tool cover -html=ch18.cover -o=ch18.html

命令运行后,会在当前目录下生成一个 ch18.html 文件,使用浏览器打开它: 红色标记的部分是没有测试到的;

这就是单元测试覆盖率报告的好处,通过它你可以很容易地检测自己写的单元测试是否完全覆盖。

结合CI做单元测试及覆盖率的报告

实际工作结合阿里云code up做的测试报告:

# 默认的单元测试命令
# 输出测试报告目录到当前工作目录,可自动上传并展示
mkdir -p test-report-unit
# 如果有集成测试使用`go list ./... | grep -v test`排除test目录,如果没有集成测试使用./...运行所有单元测试
go test -v -json -cover -coverprofile test-report-unit/cover.out `go list ./... | grep -v test` > test-report-unit/report.jsonl
	
go tool cover -html=test-report-unit/cover.out -o test-report-unit/index.html
# 上述命令生产的目录和报告文件需要与测试报告目录、测试报告文件、测试报告入口文件匹配,否则无法生成报告结果

[[基准测试]]

#基准测试 (Benchmark)是一项用于测量和评估软件性能指标的方法,主要用于评估你写的代码的性能。

# -cpu n 指定cpu的核数
# -v 指定目录
# -benchmem 开启内存统计
go test -bench=BenchmarkIntersect -v roaring-bitmap_test.go -cpu=1,2,4,6,8,10 -benchmem

# 结果
BenchmarkIntersect                  4263(执行次数)            269046 ns/op(每次运行时间)          406067 B/op  (每次耗用内存)        9 allocs/op (每次分配内存次数)
BenchmarkIntersect-2                5684            226401 ns/op          405001 B/op          9 allocs/op
BenchmarkIntersect-4                7095            186264 ns/op          404367 B/op          9 allocs/op
BenchmarkIntersect-6                6895            164794 ns/op          404441 B/op          9 allocs/op
BenchmarkIntersect-8                7218            162620 ns/op          404323 B/op          9 allocs/op
BenchmarkIntersect-10               6753            165705 ns/op          404497 B/op          9 allocs/op
PASS
ok      codeup.aliyun.com/qimao/bigdata/rec/internal/pkg/book_attr_bitmap       12.678s

func BenchmarkFibonacci(b *testing.B){

   for i:=0;i<b.N;i++{

      Fibonacci(10)

   }

}
  • 基准测试函数必须以 Benchmark 开头,必须是可导出的;

  • 函数的签名必须接收一个指向 testing.B 类型的指针,并且不能返回任何值;

  • 最后的 for 循环很重要,被测试的代码要放到循环里;

  • b.N 是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。

基准测试:

➜ go test -bench=. ./ch18

goos: darwin

goarch: amd64

pkg: gotour/ch18

BenchmarkFibonacci-8     3461616               343 ns/op

PASS

ok      gotour/ch18     2.230s

-bench 这个 Flag,它作为参数,以匹配基准测试的函数,”.”表示运行所有基准测试。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。

函数后面的 -8 表示运行基准测试时对应的 GOMAXPROCS 的值。接着的 3461616 表示运行 for 循环的次数,也就是调用被测试代码的次数,最后的 343 ns/op 表示每次需要花费 343 纳秒。

基准测试的时间默认是 1 秒,也就是 1 秒调用 3461616 次、每次调用花费 343 纳秒。如果想让测试运行的时间更长,可以通过 -benchtime 指定,比如 3 秒,代码如下所示:

$ go test -bench=. -benchtime=3s ./ch18

计时方法

进行基准测试之前会做一些准备,比如构建测试数据等,这些准备也需要消耗时间,所以需要把这部分时间排除在外。这就需要通过 ResetTimer 方法重置计时器:

func BenchmarkFibonacci(b *testing.B) {

   n := 10

   b.ResetTimer() //重置计时器

   for i := 0; i < b.N; i++ {

      Fibonacci(n)

   }

}

避免因为准备数据耗时造成的干扰。除了 ResetTimer 方法外,还有 StartTimer 和 StopTimer 方法,帮你灵活地控制什么时候开始计时、什么时候停止计时。

内存统计

在基准测试时,还可以统计每次操作分配内存的次数,以及每次操作分配的字节数,这两个指标可以作为优化代码的参考。要开启内存统计也比较简单,代码如下,即通过 ReportAllocs() 方法,与 go test 使用 -benchmem 标志类似,但 ReportAllocs 只影响那些调用了该函数的基准测试。

func BenchmarkFibonacci(b *testing.B) {

   n := 10

   b.ReportAllocs() //开启内存统计

   b.ResetTimer() //重置计时器

   for i := 0; i < b.N; i++ {

      Fibonacci(n)

   }

}

再运行这个基准测试,就可以看到如下结果:

➜ go test -bench=.  ./ch18

goos: darwin

goarch: amd64

pkg: gotour/ch18

BenchmarkFibonacci-8  2486265  486 ns/op  0 B/op  0 allocs/op

PASS

ok      gotour/ch18     2.533s

可以看到相比原来的基准测试多了两个指标,分别是 0 B/op 和 0 allocs/op。前者表示每次操作分配了多少字节的内存,后者表示每次操作分配内存的次数。这两个指标可以作为代码优化的参考,尽可能地越小越好。

以上两个指标是否越小越好?这是不一定的,因为有时候代码实现需要空间换时间,所以要根据自己的具体业务而定,做到在满足业务的情况下越小越好。

并发基准测试

Go 语言还支持并发基准测试,你可以测试在多个 goroutine 并发下代码的性能。还是以 Fibonacci 为例,它的并发基准测试代码如下:

func BenchmarkFibonacciRunParallel(b *testing.B) {

   n := 10

   b.RunParallel(func(pb *testing.PB) {

      for pb.Next() {

         Fibonacci(n)

      }

   })

}

Go 语言通过 RunParallel 方法运行并发基准测试。RunParallel 方法会创建多个 goroutine,并将 b.N 分配给这些 goroutine 执行。

单元测试是保证代码质量的好方法,但单元测试也不是万能的,使用它可以降低 Bug 率,但也不要完全依赖。除了单元测试外,还可以辅以 Code Review、人工测试等手段更好地保证代码质量。

代码检查

测试包

testing

使用testing可以完成大部分单元测试需求,而为了更优雅及高效地完成单元测试,需要借助一些第三方包。

断言

标准库为我们提供了一个还不错的测试框架,但是没有提供断言的功能。

goconvey https://github.com/smartystreets/goconvey (支持原生go test)

testify https://godoc.org/github.com/stretchr/testify/assert 包含了 断言、mock、suite 三个功能,

打桩

实际的单元测试中,某个功能模块往往会有很多的依赖项:

数据库连接、文件I/O、其他函数模块、全局变量等,为了专注于主要对象的测试,我们可以使用一个模拟对象来代替次要模块,以简化测试。

  • GoMock

https://github.com/golang/mock

gomock是官方提供的模拟框架,可以与testing很好的集成。主要针对的是对象+接口的数据结构。也就是框架完成了繁琐的实现接口的工作。

  • gomonkey

https://github.com/agiledragon/gomonkey

https://cloud.tencent.com/developer/article/1872029

gomonkey实现了单元测试中的猴子补丁,可以很方便地为方法(成员方法也可)、全局变量打桩,同时可以指定行为序列。

Reference

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.2.html

Go工程化- 单元测试 https://lailin.xyz/post/go-training-week4-unit-test.html

Search

    Table of Contents