注册 登录

清河洛

Go的test测试工具

qingheluo2025-02-18清河洛131
test是Go内置的测试工具,主要用于代码的自动化测试、性能评估和质量分析,测试的逻辑编写需要Go的内置官方库"testing" 自动化测试:自动识别和执行项目中的测试用例,验证代码逻辑的正确性及性能表现 性能评估: 通过多次迭代执行和信息统计,评估代码性能 质量分析: 通过覆盖率测试和模糊测试,确保代码的稳定可靠 go test的两种运行模式 1> 本地目录模式(不指定目录或包),会在当前目录中查找名称为"*_test.go"的文件,此模式下默认是禁用缓存的 2> 包列表模式(指定目录或包),会在指定目录中查找名称为"*_test.go"的文件,此模式下默认开启缓存,缓存匹配时将...

test是Go内置的测试工具,主要用于代码的自动化测试、性能评估和质量分析,测试的逻辑编写需要Go的内置官方库"testing"

自动化测试:自动识别和执行项目中的测试用例,验证代码逻辑的正确性及性能表现
性能评估:  通过多次迭代执行和信息统计,评估代码性能
质量分析:  通过覆盖率测试和模糊测试,确保代码的稳定可靠

go test的两种运行模式

1> 本地目录模式(不指定目录或包),会在当前目录中查找名称为"*_test.go"的文件,此模式下默认是禁用缓存的

2> 包列表模式(指定目录或包),会在指定目录中查找名称为"*_test.go"的文件,此模式下默认开启缓存,缓存匹配时将直接显示缓存结果并在打印中以"cached"代替用时信息
    缓存仅会缓存 -benchtime、-cpu、-list、-parallel、-run、-short 和 -v 这些选项
    当这些选项任意选项值发生变化后会重新编译而不使用缓存
    当使用了任意这些选项之外的选项时,永远不会进行结果的缓存,习惯上使用"-count=1"选项

如项目名为"mytest"的项目根目录中打开命令行:
// 本地目录模式
go test
    // 会在当前目录读取所有"*_test.go"文件进行测试
// 包列表模式
go test .
    // 使用点(.)表示当前目录,等同于本地目录模式,但是会对结果进行缓存
go test ./src/demo       // 指定目录
go test ceshi/src/demo   // 指定包名
    // 两者等效,均会读取所有"src/demo/*_test.go"文件进行测试

三个连续的点(...)可作为路径通配符
    go test ./...
        // 会遍历读取当前目录及所有子目录

go test 命令参数

命令格式:go test [flags] [pkg]

可以使用所有的build命令选项,用于在编译测试二进制文件时起效

参数中所有的regexp值表示正则表达式,可以使用斜杠(/)分割多个表达式用于匹配子测试

通用设置
-c  编译所有测试文件为名称为pkg.test的二进制文件但不运行
-exec xprog
    使用xprog运行测试二进制文件,行为类似于go run
-o file  指定测试文件编译后的二进制文件名,默认为pkg.test
-count n
    每个测试(非-fuzz匹配的模糊测试)运行n次(默认1)
    若设置了`-cpu`,则每个GOMAXPROCS值运行n次
    示例(examples)始终只运行一次
-cpu 1,2...
    指定测试(非-fuzz匹配的模糊测试)运行时的GOMAXPROCS值列表,默认为当前GOMAXPROCS值
    当有多个用逗号分割的值时,所有的测试都将进行多次运行以使用不同的CPU配置并在结果中添加cpu配置标志
    如 -cpu 1,2,4 表示测试将执行3次(分别对应1,2,4三个CPU配置)
-failfast
    首个测试失败后不再启动新测试
-fullpath
    在错误信息中显示完整文件路径
-parallel n
    允许并行执行调用了`t.Parallel()`方法的测试函数数量
    在模糊测试时,此标志的值是同时调用模糊函数的子进程最大数,无论是否调用`t.Parallel()`方法
    默认为GOMAXPROCS
    高于GOMAXPROCS的值可能导致CPU争用导致性能下降
-short
    通知长时间运行的测试缩短其执行时间,默认关闭

-shuffle off|on|N
    测试函数的执行顺序,默认off(以源码中的顺序执行)
    若为`on`则使用系统时钟作为随机种子,若为整数N则使用N作为种子
    on或N两种情况下,种子值会被报告以便复现
-timeout d
    若测试二进制文件运行时间超过d,则触发panic,默认10m,0表示禁用超时
-vet list
    配置`go test`运行`go vet`时使用的代码静态检查项列表(逗号分隔)
    默认或设置为空表示使用预设的推荐检查项,为off表示跳过检查

打印相关
-v    详细输出,记录所有运行的测试
-json
    以JSON格式记录详细输出和测试结果
    输出信息与`-v`相同,但格式不同

通用匹配
-list regexp
    列出匹配正则表达式的测试、基准测试、模糊测试或示例
    不会实际运行这些测试
    仅列出顶级测试,不显示子测试
-run regexp
    仅运行匹配正则表达式的测试、示例和模糊测试
    以斜杠(/)分割的表达式会分割并匹配子测试
-skip regexp
    仅运行不匹配正则表达式的测试、示例、模糊测试和基准测试

基准测试
-bench regexp
    仅运行匹配的基准测试,默认不运行任何基准测试,点(.)表示运行所有基准测试
    打印的信息第一列表示执行总次数,第二列表示执行的平均时间
-benchtime t
    指定每个基准测试运行持续时长t以保障足够的迭代次数
    t的格式为time.Duration,如 1h30s,默认1s(1秒)
    特殊语法`Nx`表示明确指定运行基准测试N次
-benchmem   额外打印基准测试的内存分配统计
    额外打印的两列信息分别表示平均每次测试的内存需求和分配内存操作调用次数

模糊测试
-fuzz regexp
    运行匹配正则表达式的模糊测试
    此标志要求命令行参数必须精确匹配主模块中的一个包,且正则表达式必须精确匹配该包中的一个模糊测试
    模糊测试会在所有普通测试、基准测试、其他模糊测试的种子语料库和示例完成后执行

-fuzztime t
    指定模糊测试运行持续时长t以保障足够的迭代次数,默认无限制
    特殊语法`Nx`表示运行模糊目标N次

-fuzzminimizetime t
    每次最小化尝试中运行持续时长t以保障足够的迭代次数,默认60s
    特殊语法`Nx`表示运行模糊目标N次

写入文件
-outputdir directory  
    将性能分析文件输出到指定目录
    默认为`go test`的运行目录
-trace trace.out
    退出前将执行跟踪写入指定文件
    通过命令:go tool trace [-http [host]:prot] trace.out查看
-coverprofile cover.out
    所有测试通过后,将覆盖率分析结果写入文件
    设置此选项会默认开启`-cover`
    通过命令:go test -coverprofile cover.out详细信息
        左上角显示覆盖率, 红色是没有覆盖到的, 绿色是覆盖到的
-blockprofile block.out
    将协程阻塞分析写入指定文件(保留测试二进制文件)
-cpuprofile cpu.out
    将CPU分析结果写入指定文件(保留测试二进制文件)
-memprofile mem.out
    将内存分配分析结果写入文件(保留测试二进制文件)
-memprofilerate n
    设置内存分配采样频率
    每分配 n 字节的内存,就会记录一次采样
    默认为 524288(512KB),即每分配512 KB内存就记录一次采样
    采样率越高,采样点就越少,生成的memprofile文件越小,但可能丢失一些细节
    设为 0 表示禁用内存采样,不会生成任何内存分析数据
-mutexprofile mutex.out
    将互斥锁竞争分析写入指定文件(保留测试二进制文件)
-mutexprofilefraction n
    对持有竞争互斥锁的协程每n个堆栈跟踪采样一次

协程阻塞分析、CPU分析、内存分配分析、互斥锁竞争分析通过命令go tool pprof查看
    go tool pprof -http [host]:port file
    go tool pprof -text file

所有选项参数均可以添加test.前缀使用,如-v等同于-test.v

某些选项会保留测试的二进制文件(文件名为pkg.test),当手动运行这些二进制文件时选项前必须添加test.前缀

若要将参数传递给测试二进制文件而不被解释为已知标志或包名,使用-args,-args后的所有参数会原样传递给测试二进制文件

go test -v -args -v -myflags
    // 会编译测试二进制文件并运行
pkg.test -test.v -v -myflags

内置官方"testing"包

Go的内置官方"testing"包提供了多种数据结构和方法用于支持Go的自动化测试

该包中比较重要的数据类型有

testing.T   传递给Test函数的结构体,用于管理测试状态并支持格式化的测试日志

testing.B   传递给Benchmark函数的结构体,用于管理基准测试计时和控制迭代次数

testing.F   传递给模糊测试的结构体

type TB interface

TB接口是T、B 和 F 共有的接口,也就是说T、B 和 F均实现了TB接口,并且根据各自的功能进行了部分方法的新增

type TB interface {}中定义的方法

Cleanup(func())    注册一个清理函数
    当测试及其所有子测试完成时自动调用
    多个清理函数将按后进先出(LIFO)的顺序调用

Name() string     返回正在运行的测试名称
    该名称将包括测试的名称以及任何嵌套的子测试的名称
    如果两个同级子测试具有相同的名称会附加一个后缀以保证名称唯一
Setenv(key, value string)
    调用os.Setenv并使用Cleanup将环境变量在测试后恢复到原来的值
Chdir(dir string)
    调用os.Chdir并使用Cleanup在测试后将当前工作目录恢复原始值
    由于Chdir影响整个过程,因此不能用于并行测试或具有并行祖先的测试
TempDir() string   创建并返回一个临时目录
    当测试及其所有子测试完成时会自动删除该目录
    每次调用都会返回一个唯一的目录
    如果目录创建失败将通过调用Fatal来终止测试

Fail()          将测试函数标记为失败,但仍然继续执行
Error(args ...any)    等同 Log 后跟 Fail
Errorf(format string, args ...any)    等同 Logf 后跟 Fail
FailNow()       将测试函数标记为失败,并停止其执行
    会继续运行当前协程中的所有延迟调用
    会继续执行其他测试函数
    不会影响其他协程并且该方法必须从运行测试函数的协程调用
Fatal(args ...any)    等同 Log 后跟 FailNow
Fatalf(format string, args ...any)    等同 Logf 后跟 FailNow
Failed() bool       此测试函数是否失败

Log(args ...any)    使用默认格式格式化参数,类似于Println
Logf(format string, args ...any)
    根据指定格式格式化参数,类似于Printf
    对于测试,仅当测试失败或设置了 -test.v 标志时,才会打印文本
    对于基准测试,始终打印文本以避免性能取决于 -test.v 标志的值

SkipNow()          将该测试函数标记为已跳过并停止其执行
Skip(args ...any)  等同 Log 后跟 SkipNow
Skipf(format string, args ...any)   等同 Logf 后跟 SkipNow
    如果测试已被标注失败,跳过后仍然被认为是失败的
    SkipNow不会影响其他协程且该方法必须从运行测试函数的协程调用
Skipped() bool     该测试函数是否跳过

Context() context.Context
    返回在调用Cleanup注册的函数之前取消的上下文
    清理函数可以等待在 Context.Done 上关闭的任何资源,然后再完成测试或基准测试
Helper()
    将调用此函数的函数标记为测试辅助函数,用于优化测试日志和错误定位
    当打印文件和行信息时(如打印异常信息),该函数将被跳过,将打印调用该函数的行
    避免打印信息指向底层工具函数,显著减少定位问题的时间
    func assertEqual(t *testing.T, a, b int) {
        t.Helper() // 标记为辅助函数 
        if a != b {
            t.Errorf("值不匹配: %d vs %d", a, b) // 错误将指向测试函数中的调用行
        }
    }

    func TestAdd(t *testing.T) {
        result := Add(2, 3)
        assertEqual(t, result, 5) // 打印的信息报告此行
    }
    使用Helper()的注意事项:
    1、应在辅助函数的起始位置调用t.Helper(),确保后续所有t方法的调用栈修正生效
    2、若辅助函数A调用另一个辅助函数B,两者均需调用t.Helper()
    3、在资源清理函数中使用t.Helper(),使清理失败时能准确定位到测试函数
    4、过度使用可能导致调用栈信息丢失

自动化测试

自动运行所有"_test.go"文件中的Test函数,函数签名为func TestXxx(t testing.T)

Test  所有测试函数名称必须以"Test"开头
Xxxx  表示自定义测试函数名称,必须以大写字母开头

*testing.T在实现TB接口之外新增的方法

Deadline() (deadline time.Time, ok bool)
    获取通过-timeout设置的截止时间,如果设置为0,ok返回false
    可以在程序中重新设置超时时间
        deadline, _ := t.Deadline()
        deadline = time.Now().Add(30 * time.Second)

Run(name string, f func(t *T)) bool
    将 f 作为名为 name 的 t 的子测试运行
    在单独的协程中运行 f 并阻塞,直到 f 返回或调用 t.Parallel 成为并行测试
    Run 报告 f 是否成功,或者至少在调用 t.Parallel 之前没有失败
    Run 可以从多个协程中同时调用,但所有此类调用必须在 t 的外部测试函数返回之前返回

Parallel()
    将测试函数标记为并行函数
    同一次测试运行期间仅所有标记为并行函数的函数才会并行运行
    当由于使用 -test.count 或 -test.cpu 而多次运行测试时,
    单个测试的多个实例永远不会彼此并行运行(如使用-test.count或-test.cpu)

TestMain函数

函数签名:func TestMain(m *testing.M)

如果测试文件中包含函数TestMain,那么测试时将调用TestMain(m)而不是直接运行测试函数

func (m *testing.M) Run() (code int)
    调用m.Run()触发所有测试的执行,如果所有测试均没有失败,返回code为0

TestMain函数主要用于在调用m.Run()前后做一些额外的准备和回收工作

基准测试

基准测试函数签名为func BenchmarkXxx(b *testing.B)

当使用了-bench选项时会根据其正则表达式运行匹配的基准测试函数

Benchmark  所有基准测试函数名称必须以"Benchmark"开头
Xxxx       表示自定义测试函数名称,必须以大写字母开头

*testing.B结构体有一个导出的字段"N",用于表示当前所在基准测试函数的迭代运行次数

*testing.B在实现TB接口之外新增的方法

Loop() bool     基准测试是否要继续迭代,该函数在版本1.24被引入
    首次运行Loop会重置基准测试计时器
    如果函数中运行了Loop,那么在首次运行之前的所有执行不会计入基准测试结果中
    当返回false时会停止基准测试计时器,因此测试结果不含清理函数的执行
    在引入Loop之前是使用"0到b.N"迭代循环,两者均可使用但不能同时使用
    Loop提供了对基准测试计时器的更多自动管理,并且每次测量仅运行每个基准测试函数一次,而基于b.N的基准测试必须多次运行基准测试函数以及任何关联的设置和清理
ReportAllocs()
    为调用该方法的基准测试函数启用内存分配统计信息
    等效于设置-test.benchmem但只影响调用ReportAllocs的基准测试函数
ReportMetric(n float64, unit string)
    添加自定义的基准测试指标
    n表示显示的值,unit表示单位
    如果unit是基准测试框架本身已使用的单位(如“allocs/op”)将覆盖该指标
    由于基准测试是多次迭代后的一些指标的平均值,所以在自定义时通常需要考虑迭代次数b.N
ResetTimer()  将基准测试时间和内存分配计数器归零,不影响计时器是否正在运行
StopTimer()   暂时停止对测试计时
StartTimer()  开始对测试进行计时,在调用 B.StopTimer 后恢复计时
    此函数在基准测试开始之前自动调用
Run(name string, f func(t *T)) bool
    将 f 作为名为 name 的 t 的子基准测试运行
    存在子基准测试的函数不会计入基准测试结果,仅仅会在b.N=1时调用一次
    基准测试函数实际上会运行两次,第一次b.N会设为1,当运行完毕后未发现子基准测试时才会将b.N设置为要迭代的次数并第二次运行基准测试函数,如果第一次运行发现子基准测试,不会进行第二次运行,且第一次b.N=1的父基准测试函数不会记入基准测试结果
RunParallel(body func(*PB))
    并行运行基准测试,创建多个协程并在它们之间分配b.N迭代
    协程的数量默认为GOMAXPROCS
    body将在每个协程中运行
    PB结构体用于并行基准测试,该结构体仅有一个方法
        func (pb *PB) Next() bool  表示是否要继续迭代
    最终将ns/op值报告为整个基准测试的时间,而不是每个并行协程的时间之和
    要改变并行协程的数量需要在调用RunParallel之前调用B.SetParallelism或者在命令行中使用-cpu标志
SetParallelism(p int)  设置调用了RunParallel的并行数量
SetBytes(n int64)
    强制设置在单次迭代中处理的字节数
    调用此函数后基准测试将追加一列(MB/s)表示每秒处理数据大小
    字节到兆的进制为1000而非1024
    每次迭代中处理的数据会被强制认为设置的大小即使实际并非该大小
    多用于读取文件,每次迭代读取固定大小,以此来计算读取速度
    如果每次迭代处理的数据大小不同,最终的统计结果可能会有误差

模糊测试

模糊测试主要是用来自动生成输入数据,以测试函数的健壮性,特别是处理边界情况和异常输入的能力

边界条件覆盖:自动探索代码未处理的边缘情况
持续压力测试:单次执行可生成数百万测试用例
崩溃自动记录:失败输入自动保存至testdata目录

模糊测试函数签名为func FuzzXxx(f *testing.F)

当使用了-fuzz选项时会根据其正则表达式运行匹配的模糊测试函数

Fuzz    所有模糊测试函数名称必须以"Fuzz"开头
Xxxx    表示自定义测试函数名称,必须以大写字母开头

模糊测试的运行流程

模糊测试实际就是通过迭代运行f.Fuzz(func (t *testing.T,args ...any))指定的测试函数

每次迭代运行后分析运行的代码覆盖率、执行路径及错误信息,根据分析的信息智能生成下一次测试函数运行的变异输入

1、在模糊测试之前检查是否有设置的语料(使用f.Add方法)
2、如果有设置语料,使用语料执行测试
3、如果没有设置语料,根据数据类型生成随机输入执行测试
持续迭代开始
4、监控执行测试的代码覆盖率
5、分析执行测试的执行路径
6、生成变异输入并执行测试
7、记录可能出现的错误信息

*testing.F在实现TB接口之外新增的方法

Add(args ...any)  将参数添加到模糊测试的种子语料库中
    种子语料库设置核心用途是针对迭代生成的输入有一个引导作用
    如字符串相关函数模糊测试一般会设置的人工语料
        f.Add("Hello")    // 典型有效输入
        f.Add(" ")        // 边界条件输入
        f.Add("!@#$,/.")  // 特殊字符组合
        f.Add("中文繁体")  // 特殊字符组合
    人工设置的语料对于执行的覆盖率分析和执行路径可能有更全面的覆盖,便于快速发现漏洞
    如果不设置人工语料,会自动生成语料,但自动生成的语料对于执行的覆盖率分析和执行路径覆盖可能不够全面
    报告数据显示,相较于纯随机生成,合理的人工种子语料可将漏洞发现速度提升3-5倍

Fuzz(func (t *testing.T,args ...any))  每次迭代要运行的测试函数

示例测试

示例测试函数签名为func ExampleName()(没有参数和返回值)

Example    所有示例测试函数名称必须以"Example"开头
Name       表示要测试的函数名称,必须与要测试的函数名称相同

示例函数有三个用处:

1、作为函数文档:一个示例函数可以简洁直观的演示函数的用法,godoc的web文档服务器会将示例函数关联到某个具体函数,因此示例函数将是函数文档的一部分
2、如果示例函数内含有 "// Output:somecont" 格式的注释
    那么在go test执行测试期间也会运行示例函数,然后检查标准输出与注释是否匹配

使用示例

//demo.go
func Demo() {
    fmt.Println("Demo is called")
}

//test_demo.go
func ExampleDemo() {
    fmt.Println("Example start")
    Demo()
    fmt.Println("Example end")
    // Output:Example start
    // Output:Demo is called
    // Output:Example end
}


网址导航