Go语言:单元测试!

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 单元测试就是对单元进行测试,一个单元可以是一个函数、一个模块等。一般测试的单元应该是一个完整的最小单元,比如一个函数。这样当每个最小单元都被验证通过,那么整个模块就都可以被验证通过。

单元测试

顾名思义,单元测试就是对单元进行测试,一个单元可以是一个函数、一个模块等。一般测试的单元应该是一个完整的最小单元,比如一个函数。这样当每个最小单元都被验证通过,那么整个模块就都可以被验证通过。

Go 语言有自己的单元测试规范,此处我们以 斐波那契数列 为例。斐波那契数列:它的第 0 项是 0;第 1 项是 1;从第 2 项开始,每一项都等于前两项之和。所以它的数列是:0、1、1、2、3、5、8、13、21……

根据上面规律,函数可以总结为:

F(0)=0
F(1)=1
F(2)=F(2 - 1)+F(2 - 2)
F(n)=F(n - 1)+F(n - 2)
复制代码

实现函数:

func F(n int) int {
  if n < 0 {
    return 0
  }
  if n == 0 {
    return 0
  }
  if n == 1 {
    return 1
  }
  return F(n-1) + F(n-2)
}
复制代码

我们通过递归的方式实现了斐波那契数列的计算。

函数编写好后,需要先对它进行单元测试,需要新建一个 go 文件用来存放单元测试代码,比如刚写的函数在 test/main.go 中,测试的代码需要放在 test/main_test.go 中,测试代码如下:

func TestF(t *testing.T) {
  //预先定义的一组斐波那契数列作为测试用例
  fsMap := map[int]int{}
  fsMap[0] = 0
  fsMap[1] = 1
  fsMap[2] = 1
  fsMap[3] = 2
  fsMap[4] = 3
  fsMap[5] = 5
  fsMap[6] = 8
  fsMap[7] = 13
  fsMap[8] = 21
  fsMap[9] = 34
  for k, v := range fsMap {
    fib := F(k)
    if v == fib {
      t.Logf("结果正确:n为%d,值为%d", k, fib)
    } else {
      t.Errorf("结果错误:期望%d,但是计算的值是%d", v, fib)
    }
  }
}
复制代码

测试示例中,通过 map 预定义了一组测试用例,然后通过 F 函数计算结果,通预定义的结果进行比较,如果结果相等,说明 F 函数计算正确,否则说明计算错误。

单元测试命令

go test -v ./test

这里介绍几个常用的参数:

  • -bench regexp 执行相应的 benchmarks,例如 -bench=.;
  • -cover 开启测试覆盖率;
  • -run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
  • -v 显示测试的详细命令。

上面命令表示运行 test 目录下的所有单元测试,此处我们这里只有一个单元测试,运行结果为:

$ go test -v ./test
=== RUN   TestF
    sum_test.go:23: 结果正确:n为0,值为0
    sum_test.go:23: 结果正确:n为2,值为1
    sum_test.go:23: 结果正确:n为4,值为3
    sum_test.go:23: 结果正确:n为5,值为5
    sum_test.go:23: 结果正确:n为6,值为8
    sum_test.go:23: 结果正确:n为7,值为13
    sum_test.go:23: 结果正确:n为1,值为1
    sum_test.go:23: 结果正确:n为3,值为2
    sum_test.go:23: 结果正确:n为8,值为21
    sum_test.go:23: 结果正确:n为9,值为34
--- PASS: TestF (0.00s)
PASS
ok      project/test 0.585s
复制代码

运行结果中,可以看到 PASS 标记,表明单元测试通过,还可以看到单元测试中写的日志。

单元测试是在 Go 语言提供的测试框架下完成的,需要遵循5点规则:

  1. 单元测试的 go 文件必须以 _test.go 结尾,Go 语言测试工具只认符合这个规则的文件。
  2. 单元测试文件名 _test.go 前面的部分最好是被测试的函数所在的 go 文件的文件名。
  3. 单元测试的函数名必须以 Test 开头,是可导出的、公开的函数。
  4. 测试函数的签名必须接收一个指向 testing.T 类型的指针,且不能返回任何值。
  5. 函数名最好是 Test + 被测试的函数名。

单元测试日志

一个测试用例可能会并发执行,使用 testing.T 提供的日志输出可以保证日志跟随这个测试上下文一起打印输出。testing.T 提供了几种日志输出方法,如下:

方 法 释义
Log 打印日志,同时结束测试
Logf 格式化打印日志,同时结束测试
Error 打印错误日志,同时结束测试
Errorf 格式化打印错误日志,同时结束测试
Fatal 打印致命日志,同时结束测试
Fatalf 格式化打印致命日志,同时结束测试

单元测试覆盖率

上面示例中的 F 函数是否被全面测试到了呢? 我们可以使用命令来查看覆盖率:

go test -v --coverprofile=test.cover ./test运行结果:

$ go test -v --coverprofile=test.cover ./test
=== RUN   TestF
    sum_test.go:23: 结果正确:n为9,值为34
    sum_test.go:23: 结果正确:n为0,值为0
    sum_test.go:23: 结果正确:n为1,值为1
    sum_test.go:23: 结果正确:n为6,值为8
    sum_test.go:23: 结果正确:n为7,值为13
    sum_test.go:23: 结果正确:n为8,值为21
    sum_test.go:23: 结果正确:n为2,值为1
    sum_test.go:23: 结果正确:n为3,值为2
    sum_test.go:23: 结果正确:n为4,值为3
    sum_test.go:23: 结果正确:n为5,值为5
--- PASS: TestF (0.00s)
PASS
coverage: 85.7% of statements
ok      project/test 0.521s  coverage: 85.7% of statements
复制代码

可以看到,测试覆盖率为 85.7% ,说明 F 函数没有被全面地测试,我们再查看详细的单元测试覆盖率报告来看下:go tool cover -html=test.cover -o=test.html

运行命令后,会在当前目录下生成一个 test.html 文件,我们用浏览器打开它,可以看到:

image.png

其中红色标记的部分是没有测试到的,绿色标记的部分是已经测试到的。单位测试覆盖率报告可以很容易地检测单元测试是否完全覆盖。

根据报告,再修改下单元测试代码,把没有覆盖的代码逻辑覆盖到:

fsMap[-1] = 0
复制代码

再运行这个单元测试,查看它的单元测试覆盖率,就会发现已经是 100% 了。

基准测试

基准测试可以测试一段程序的运行性能及耗费 CPU 的程度,基准测试和单元测试的规则基本一样,只是测试函数的命名规则不一样。以上面的函数 F() 为例,基准测试代码:

func BenchmarkF(b *testing.B) {
    for i:=0;i<b.N;i++{
      F(10)
   }
}
复制代码

基准测试和单元测试的区别:

  1. 基准测试函数必须以 Benchmark 开头,必须是可导出的;
  2. 函数的签名必须接收一个指向 testing.B 类型的指针,并且不能返回任何值;
  3. 被测试的代码要放到 for 循环里;
  4. b.N 是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。

运行基准测试同样是使用 go test 命令,并且需要加上 -bench 这个 Flag ,它接收一个表达式作为参数,"." 表示运行所以的基准测试。命令为:go test -bench=. ./test运行结果:

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
BenchmarkF-12            4042413               280.1 ns/op
PASS
ok      project/test 2.208s
复制代码

结果解析:

  • 函数后的 "-12" 表示运行基准测试时对应的 GOMAXPROCS 的值。
  • 4042413 表示运行 for 循环的次数,也就是调用被测试代码的次数。
  • 280.1 ns/op 表示每次需要花费 280.1 纳秒。

基准测试的时间默认是 1 秒,也就是 1 秒调用 4042413 次、每次调用花费 280.1 纳秒。如果想让测试运行的时间更长,可以通过 -benchtime 指定,比如 5 秒:go test -bench=. -benchtime=5s ./test

计时方法

  1. ResetTimer 方法用来重置计时器
  2. StartTimer 方法控制开始计时
  3. StopTimer 方法控制停止计时

进行基准测试之前可能会做一些准备,比如构建测试数据等,这些准备也需要消耗时间,所以需要把这部分时间排除在外。这时候我们可以使用 ResetTimer 方法来重置计时器,避免准备数据的耗时对测试数据造成干扰:

func BenchmarkF(b *testing.B) {
   n := 20
   b.ResetTimer() //重置计时器
   for i := 0; i < b.N; i++ {
      F(n)
   }
}
复制代码

内存统计

在基准测试时,还可以统计每次操作分配内存的次数与字节数,这两个指标可以作为优化代码的参考。要开启内存统计需要通过 ReportAllocs() 方法:

func BenchmarkF(b *testing.B) {
  n := 20
  b.ReportAllocs() //开启内存统计
  b.ResetTimer() //重置计时器
  for i := 0; i < b.N; i++ {
    F(n)
  }
}
复制代码

运行结果:

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
BenchmarkF-12    166467    35043 ns/op    0 B/op   0 allocs/op
PASS
ok      project/test 6.724s
复制代码

结果中比之前的基准测试多了两项指标:

  1. 0 B/op ,表示每次操作分配了多少字节的内存。
  2. 0 allocs/op ,表示每次操作分配内存的次数。

并发基准测试

除了上面介绍的基准测试,Go 语言还支持并发基准测试,可以测试在多个 gorouting 并发下代码的性能。并发基准测试需要通过 RunParallel 方法,RunParallel 方法会创建多个 goroutine,并将 b.N 分配给这些 goroutine 执行:

func BenchmarkFibonacciRunParallel(b *testing.B) {
  n := 10
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      F(n)
    }
  })
}
复制代码

F 函数优化

上面我们写的斐波那契数列函数 F ,因为使用了递归,一定会有重复计算,这是影响递归的主要因素。解决重复计算我们可以使用缓存,把已经计算好的结果先保存起来,以便后续重复使用。

优化后的函数:

func F(n int) int {
  if v, ok := cache[n]; ok {
    return v
  }
  result := 0
  switch {
  case n < 0:
    result = 0
  case n == 0:
    result = 0
  case n == 1:
    result = 1
  default:
    result = F(n-1) + F(n-2)
  }
  cache[n] = result
  return result
}
复制代码

运行结果:

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
BenchmarkFibonacciRunParallel-12    563681175   2.123 ns/op
PASS
ok      project/test 2.006s
复制代码

可以看到,结果为 2.123 纳秒,相比优化前的 280.1 纳秒,性能上有很大的提升。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
8天前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
1天前
|
安全 Java Go
探索Go语言在高并发环境中的优势
在当今的技术环境中,高并发处理能力成为评估编程语言性能的关键因素之一。Go语言(Golang),作为Google开发的一种编程语言,以其独特的并发处理模型和高效的性能赢得了广泛关注。本文将深入探讨Go语言在高并发环境中的优势,尤其是其goroutine和channel机制如何简化并发编程,提升系统的响应速度和稳定性。通过具体的案例分析和性能对比,本文揭示了Go语言在实际应用中的高效性,并为开发者在选择合适技术栈时提供参考。
|
5天前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
22 1
|
5天前
|
SQL 关系型数据库 MySQL
Go语言中使用 sqlx 来操作 MySQL
Go语言因其高效的性能和简洁的语法而受到开发者们的欢迎。在开发过程中,数据库操作不可或缺。虽然Go的标准库提供了`database/sql`包支持数据库操作,但使用起来稍显复杂。为此,`sqlx`应运而生,作为`database/sql`的扩展库,它简化了许多常见的数据库任务。本文介绍如何使用`sqlx`包操作MySQL数据库,包括安装所需的包、连接数据库、创建表、插入/查询/更新/删除数据等操作,并展示了如何利用命名参数来进一步简化代码。通过`sqlx`,开发者可以更加高效且简洁地完成数据库交互任务。
13 1
|
11天前
|
XML JSON Go
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
|
5天前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
6天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
11天前
|
存储 编译器 Go
Go语言中的逃逸分析
Go语言中的逃逸分析
|
7天前
|
安全 Go API
go语言中的Atomic操作与sema锁
在并发编程中,确保数据一致性和程序正确性是关键挑战。Go语言通过协程和通道提供强大支持,但在需精细控制资源访问时,Atomic操作和sema锁变得至关重要。Atomic操作确保多协程环境下对共享资源的访问是不可分割的,如`sync/atomic`包中的`AddInt32`等函数,底层利用硬件锁机制实现。sema锁(信号量锁)控制并发协程数量,其核心是一个uint32值,当大于零时通过CAS操作实现锁的获取与释放;当为零时,sema锁管理协程休眠队列。这两种机制共同保障了Go语言并发环境下的数据完整性和程序稳定性。
|
8天前
|
算法 Go
Go 语言 实现冒泡排序
冒泡排序是大家熟知的经典算法。在Go语言中实现它,关键在于理解其核心思想:通过不断比较并交换相邻元素,让序列中的最大值像泡泡一样“浮”至顶端。每一轮比较都能确定一个最大值的位置。外层循环控制排序轮数,内层循环负责比较与交换。随着每轮排序完成,未排序部分逐渐缩小,直至整个数组有序。以下是Go语言实现示例及说明。
17 1