Golang单元测试

文章目录

传统测试方法

基本介绍

基本介绍

  • 代码测试是软件开发中的一项重要实践,用于验证代码的正确性、可靠性和预期行为。通过代码测试,开发者可以发现和修复潜在的错误、确保代码按预期工作,并提高系统的质量和稳定性。
  • 单元测试是针对代码中最小的可测试单元(如函数、方法或类)进行的测试,以验证代码单元在给定输入下的行为是否正确。单元测试通常由开发者编写,并使用特定的测试框架和断言库来定义测试用例、执行测试代码,并验证测试结果。

例如,现在要求对cal.go文件中的函数进行代码测试。cal文件中的代码如下:

go 复制代码
package cal

func Add(num1 float64, num2 float64) float64 {
	return num1 + num2
}

func Sub(num1 float64, num2 float64) float64 {
	return num1 + num2 // 代码错误
}

func Mul(num1 float64, num2 float64) float64 {
	return num1 * num2
}

func Div(num1 float64, num2 float64) float64 {
	return num1 * num2 // 代码错误
}

常见的测试方法是将测试代码直接写到main函数中,或在main函数中调用对应的测试函数。如下:

go 复制代码
package main

import (
	"fmt"
	"go_code/UnitTest/cal"
)

func main() {
	if ret := cal.Add(10, 20); ret != 30 {
		fmt.Printf("error: cal.Add(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 30.0)
	}
	if ret := cal.Sub(10, 20); ret != -10 {
		fmt.Printf("error: cal.Sub(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, -10.0)
	}
	if ret := cal.Mul(10, 20); ret != 200 {
		fmt.Printf("error: cal.Mul(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 200.0)
	}
	if ret := cal.Div(10, 20); ret != 0.5 {
		fmt.Printf("error: cal.Div(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 0.5)
	}
}

运行程序后可以得到代码的测试结果。如下:

主要缺点

主要缺点

将测试代码写到main函数中,主要存在如下缺点:

  • 难以维护和扩展:测试代码与程序代码耦合在一起,每次完成代码测试后都需要恢复程序代码,并且每次新增测试函数时都需要手动在main函数中新增调用逻辑。
  • 缺乏测试框架支持:传统的测试方法通常缺乏成熟的测试框架支持,无法提供丰富的断言函数、测试报告等功能,这使得测试编写和执行过程相对繁琐,并缺乏对测试结构体的全面分析。

单元测试

基本介绍

基本介绍

  • Go语言中自带一个轻量级的测试框架testing,其功能被封装到了标准库的testing包中,结合go test命令可以方便地进行单元测试和性能测试。
  • 在Go中,每个源文件的测试代码都单独写到一个对应的测试文件中,测试文件名必须以_test.go为后缀,并将其与被测试文件放在同一个目录下,比如要对cal.go文件进行测试,测试文件通常命名为cal_test.go,并将其放在cal.go所在的目录下。
  • 在测试文件中由三种类型的函数:测试函数、基准测试函数、示例函数,go test命令会遍历所有测试文件中的这些函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

说明一下: 文件名以_test.go为后缀的测试函数,在执行go build时不会被构建成包的一部分。

测试函数

测试函数

  • 在Go中,测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,通常测试函数命名为Test+被测函数名,比如测试Add函数的测试函数命名为TestAdd。

测试函数的签名要求如下:

T是testing包中的一个结构体类型,用于表示测试状态和提供测试功能。其常用方法如下:

方法名 功能
Log 采用与fmt.Println相同的格式化语法输出日志信息
Logf 采用与fmt.Printf相同的格式化语法输出日志信息
Fail 将当前测试标记为失败,但仍继续执行该测试
FailNow 将当前测试标记为失败,并停止执行该测试
Error 输出错误信息,并将当前测试标记为失败(相当于调用Log之后调用Fail)
Errorf 格式化输出错误信息,并将当前测试标记为失败(相当于调用Logf之后调用Fail)
Fatal 输出致命错误信息,并将当前测试标记为失败(相当于调用Log之后调用FailNow)
Fatalf 格式化输出致命错误信息,并将当前测试标记为失败(相当于调用Log之后调用FailNow)
SkipNow 将当前测试标记为跳过,并停止执行该测试
Skip 输出跳过信息,并将当前测试标记为跳过(相当于调用Log之后调用SkipNow)

说明一下:

  • 这里所说的"当前测试"指的是"当前testing测试框架正在执行的测试函数","将当前测试标记为失败"指的是"将当前正在执行的测试函数标记为失败","停止执行当前测试"指的是"停止当前正在执行的测试函数,继续执行下一个测试函数"。
  • 当一个测试被标记为跳过时,如果该测试在此之前已经被标记为失败,那么该测试最终会被判定为FAIL而不是SKIP。
  • SkipNow、Skip和FailNow必须在运行测试的go协程中进行调用,而不能在测试期间创建的go协程中调用,因为SkipNow、Skip和FailNow不会导致其他go协程停止(go协程在后续博客中讲解)。

测试函数案例

比如我们要对之前cal.go文件中的函数进行代码测试,则在cal.go文件所在目录下建立名为cal_test.go的测试文件,并在测试文件中编写对应的测试函数。如下:

go 复制代码
package cal

import (
	"testing"
)

func TestAdd(t *testing.T) {
	if ret := Add(10, 20); ret != 30 {
		t.Errorf("cal.Add(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 30.0)
	}
}

func TestSub(t *testing.T) {
	if ret := Sub(10, 20); ret != -10 {
		t.Errorf("cal.Sub(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, -10.0)
	}
}

func TestMul(t *testing.T) {
	if ret := Mul(10, 20); ret != 200 {
		t.Errorf("cal.Mul(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 200.0)
	}
}

func TestDiv(t *testing.T) {
	if ret := Div(10, 20); ret != 0.5 {
		t.Errorf("cal.Div(%.2f, %.2f) = %.2f, want %.2f\n", 10.0, 20.0, ret, 0.5)
	}
}

在测试文件所在目录执行go test命令,它会在当前目录中查找所有的测试文件,逐个执行测试文件中的测试函数并输出测试结果,包括各个测试函数的测试状态、日志信息、运行时间等。如下:

在任意目录下通过go test 包路径的方式,可以查找指定包路径下的测试文件并执行其测试函数。如下:

说明一下: 只要本次测试中有一个测试函数的测试状态为FAIL,那么本次测试的状态即为FAIL。

显示详细的测试信息

go test命令默认只会输出测试状态为FAIL的测试函数信息,通过携带-v参数可以查看更详细测试信息。如下:

测试指定的测试文件

go test命令默认会执行当前目录下所有测试文件中的测试函数,如果只想执行某个或某几个测试文件中的测试函数,可以在go test命令后指明对应的测试文件名,同时需要指明与测试文件相关的go源文件。如下:

测试指定的测试函数

go test命令默认会执行测试文件中所有的测试函数,通过携带-run参数可以只执行指定的测试函数。如下:

说明一下: go test命令的-run参数可以接受一个正则表达式,它将执行所有与该正则表达式匹配的测试函数。

测试覆盖率

通过携带-cover参数可以查看本次测试的测试覆盖率,即在本次测试中被测试代码至少被运行一次的代码占总代码数的比例。如下:

基准测试

基准测试函数

  • 基准测试(Benchmarking)是一种在计算机编程中用于评估程序性能的技术,其目的是测量代码在给定工作负载下的性能指标,如执行时间、内存消耗等。通过基准测试可以对不同实现方式或优化策略之间的性能差异进行比较和评估,这对于优化程序、找出性能瓶颈以及进行系统比较和选型非常有用。
  • 在Go中,基准测试函数的名字必须以Benchmark开头,可选的后缀名必须以大写字母开头,通常基准测试函数命名为Benchmark+被测函数名,比如测试Add函数的基准测试函数命名为BenchmarkAdd。

基准测试函数的签名要求如下:

B是testing包中的一个结构体类型,用于控制和测量基准测试的执行。其常用方法如下:

方法名 功能
ResetTimer 重置基准测试的计时器
StartTimer 启动基准测试的计时器
StopTimer 停止基准测试的计时器

基准测试函数案例

比如我们要对之前cal.go文件中的各个函数进行基准测试,则在cal_test.go文件中增加对应的基准测试函数即可。如下:

go 复制代码
func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(10, 20)
	}
}

func BenchmarkSub(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Sub(10, 20)
	}
}

func BenchmarkMul(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Mul(10, 20)
	}
}

func BenchmarkDiv(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Div(10, 20)
	}
}

go test命令默认只会运行普通的测试函数,通过携带-bench参数可以指定要运行的基准测试函数。如下:

基准测试结果说明:

  • 第一列:BenchmarkXxx表示基准测试函数的名称,后面的数字表示运行基准测试函数时的GOMAXPROCS的值。
  • 第二列:基准测试函数的执行次数。
  • 第三列:每执行一次基准测试函数的平均耗时(ns/op)。
  • 第四列:每执行一次基准测试函数的平均内存分配的总字节数(B/op)。
  • 第五列:每执行一次基准测试函数的平均内存分配次数(allocs/op)。

基准测试函数的运行机制:

  • testing测试框架为了保证测试结果的稳定性和准确性,在B类型中设置了字段N,在基准函数中可以通过b.N指定操作执行的循环次数。测试框架会自动调整b.N的值,确保每个基准测试函数运行至少达到1秒钟,然后再计算出平均每执行一次操作所需的时间。
  • 由于基准测试驱动器开始时并不知道运行每个基准测试函数所需花费的时间,因此在真正运行每一个基准测试函数之前,基准测试驱动器会先尝试将b.N设置为一个较小的值,如果基准测试函数的运行时间小于1秒,则加大b.N的值,直到基准测试函数的运行时间至少达到1秒钟。
  • 经过验证,在本人的机器上b.N的初始值为1,尝试执行基准测试函数后根据计时器的值判断下一次尝试的b.N值,但下一次尝试的b.N值最多为上一次b.N值的100倍,并且b.N的上限为1000000000,当b.N的值达到上限后,即使基准测试函数的运行时间没有达到1秒,也不再增加b.N的值。

说明一下:

  • 使用go test命令运行基准测试函数时仍然会运行普通的测试函数,如果普通的测试函数中存在测试状态为FAIL的测试函数,那么将不会运行基准测试函数。因此在运行基准测试函数之前请保证所有普通的测试函数能正确通过测试,或将-run参数设置为none表示不运行任何普通测试函数。
  • -bench参数指定为.表示运行所有的基准测试函数,也可以将其指定为一个正则表达式,它将执行所有与该正则表达式匹配的基准测试函数。
  • go test命令的-benchmem参数,用于在运行基准测试函数时报告内存分配的统计信息,包括内存分配次数和内存分配的总字节数,否则基准测试只有前三列结果。

重置计时器

如果基准测试函数中存在一些数据准备工作,并且你不希望这部分工作的时间被计时器统计,则可以在数据准备完毕后,通过调用B类型的ResetTimer方法重置计时器。如下:

go 复制代码
func BenchmarkDemo(b *testing.B) {

	// 基准测试数据准备工作...

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

	// [计时区域]

}

说明一下: 计时器会在基准测试函数调用时自动启动,并在函数执行完毕后自动停止。

指定区域计时

在基准测试函数中,如果你希望统计某些非连续区域的耗时,则可以通过调用B类型的StartTimer和StopTimer方法手动启动或停止计时器。如下:

go 复制代码
func BenchmarkDemo(b *testing.B) {

	// 其他工作1...

	b.ResetTimer() // 重置计时器
	// [计时区域1]
	b.StopTimer() // 停止计时器

	// 其他工作2...

	b.StartTimer() // 启动计时器
	// [计时区域2]
	b.StopTimer() // 停止计时器

	// 其他工作3...
}

注意: 为了避免统计其他工作1...的耗时,需要在[计时区域1]之前调用ResetTimer方法而不是StartTimer方法。

指定基准测试运行时间

通过go test命令的-benchtime参数,可以指定每个基准测试函数至少需要达到的运行时间。如下:

注意: 如果基准测试函数的b.N值达到上限后仍无法达到所指定的运行时间,那么b.N的值也不会继续增加。

指定操作迭代次数

通过go test命令的-benchtime参数,也可以指定每个基准测试函数中操作的迭代次数,即b.N的值。如下:

指定基准测试可用cpu数

通过go test命令的-cpu参数,可以指定运行基准测试时调度器可使用的最大CPU核心数。如下:

性能分析

通过go test命令的-cpuprofile-memprofile参数,可以分别生成CPU分析文件和内存分析文件。如下:

pprof是Go语言中自带的性能分析工具,通过go tool pprof 分析文件 命令即可对指定的分析文件进行解析。以内存性能分析为例,执行go tool pprof mem.out命令后会出现pprof的交互式命令行,在命令行中输入top命令可以查看耗费内存资源最多的函数。如下:

pprof工具分析结果说明:

  • flat:在该函数上直接花费的时间或资源总量(比如CPU时间、内存资源等),不包括该函数调用的其他函数所花费的时间或资源。
  • flat%:flat值占分析总量的百分比,表示该函数直接花费的时间或资源占资源使用的百分比。
  • sum%:这个百分比是累积的,表示到当前行为止所有函数的flat%的总和。
  • cum:该函数及其调用的所有函数总共花费的时间或资源总量。
  • cum%:cum值占分析总量的百分比,表示该函数及其调用的所有函数花费的时间或资源占资源使用的百分比。
go 复制代码
小贴士:如果一个函数的flat%值高,则表明该函数本身可能是性能瓶颈,而如果一个函数的cum%值高,则表明该函数调用链上的某个函数可能存在性能瓶颈。

此外,在pprof的交互式命令行中输入web命令,可以生成调用链路图,在链路图中会展示对应资源的耗费情况。如下:

说明一下:

  • pprof工具的调用链路图显示功能需要安装Graphviz工具,可以在Graphviz官网进行下载,安装并设置PATH环境变量后,即可在pprof的交互式命令行中执行web命令生成调用链路图。

示例函数

示例函数

  • 示例函数是一种用于演示和测试包的功能的特殊函数,其主要目的是提供关于如何使用包中的功能的示例代码以及相应的预期输出,这些示例函数可以作为文档的一部分,帮助其他开发人员理解和学习如何正确使用包中的函数、结构和类型。
  • 在Go中,示例函数没有参数和返回值,其名字必须以Example开头,可选的后缀名必须以大写字母开头,通常示例函数命名为Example+对应函数名,比如Add函数的示例函数命名为ExampleAdd。
  • 通过go test命令运行测试时示例函数也会被执行,如果示例函数内有类似// Output:格式的注释,那么测试工具就会执行这个示例函数,并检查示例函数的标准输出与注释是否匹配,如果匹配则测试通过,否则测试失败。

示例函数案例

比如我们要演示之前cal.go文件中的各个函数的使用,则在cal_test.go文件中增加对应的示例函数即可。如下:

go 复制代码
func ExampleAdd() {
	ret := Add(10, 20)
	fmt.Println(ret)
	// Output: 30
}

func ExampleSub() {
	ret := Sub(10, 20)
	fmt.Println(ret)
	// Output: -10
}

func ExampleMul() {
	ret := Mul(10, 20)
	fmt.Println(ret)
	// Output: 200
}

func ExampleDiv() {
	ret := Div(10, 20)
	fmt.Println(ret)
	// Output: 0.5
}

运行go test命令后可以看到所有测试函数和示例函数均被执行。如下:

如果某个示例函数的// Output:注释中的内容,与实际执行示例函数时的输出不匹配,则该示例函数被标记为失败,从而导致整个测试失败。如下:

相关推荐
码事漫谈4 分钟前
智谱 ZCode 静默上传 Git 历史:48 小时信任危机复盘
后端
掘金码甲哥25 分钟前
当对话模型遇上向量模型,vllm production stack 又该如何应对?
后端
传奇开心果编程1 小时前
【springboot基础语法学与练】第 1 课:从零开始
java·spring boot·后端·学习
IT_陈寒1 小时前
Java线程池用错参数,我的服务居然悄悄崩溃了
前端·人工智能·后端
光影少年1 小时前
Express 中间件原理
后端·node.js·express
aramae2 小时前
MySQL内置函数(7)
开发语言·笔记·后端·mysql·其他
徐小夕3 小时前
存量.doc 文档怎么办?JitWord 开源转换库打通旧文档到在线协同全链路
后端·架构·github
ee又momo3 小时前
一次 JWT Token 过期续期的踩坑记录
后端
五彩小白3 小时前
GO语言装饰器语法
golang
二月龙3 小时前
App 瘦身实战:图片、so 库、资源压缩,安装包体积减小 40%
后端