ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## <span style="font-size:15px">**一、testing包说明**</span> * 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中; * 测试用例的文件名必须以`_test.go`结尾; * 需要使用 import 导入 testing 包; * 测试函数的名称要以`Test`或`Benchmark`开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数; * 单元测试则以`(t *testing.T)`作为参数,性能测试以`(t *testing.B)`做为参数; * 测试用例文件使用`go test`命令来执行,源码中不需要 main() 函数作为入口,所有以`_test.go`结尾的源码文件内以`Test`开头的函数都会自动执行。 ## <span style="font-size:15px">**二、功能测试(t \*testing.T)**</span> | 库函数 | 说明 | | --- | --- | | Log(args ...any) | 打印日志 | | Logf(format string, args ...any) | 格式化其参数打印日志 | | Fatal(args ...any) | 打印日志,执行FailNow()函数 | | Fatalf(format string, args ...any) | 打印日志,执行FailNow()函数 | | Error(args ...any) | 打印错误日志后,执行Fail()函数 | | Errorf(format string, args ...any) | 打印错误日志后,执行Fail()函数 | | Run(name string, f func(t *T)) bool | 运行单测函数 | | Cleanup(f func()) | 作用类似于在函数中执行defer来做程序执行后的后续操作,比如释放资源、清除依赖等。<br>参考:https://www.cnblogs.com/YYRise/p/12376689.html#%E4%BD%BF%E7%94%A8-cleanup | | Deadline() (deadline time.Time, ok bool) | 设置测试的超时时间 | | Fail() | Fail将函数标记为已失败,但仍在继续执行。 | | Failed() | 检查函数是否已失败 | | FailNow() | 将函数标记失败并立即停止执行 | | Helper() | Helper 函数用于将调用函数标记为测试辅助的函数。有时候测试中可能会有一些重复的代码或者需要共享的辅助函数,这时就可以使用 Helper 函数来避免重复代码,提高测试代码的可维护性。Helper 函数可以被测试函数调用,但是它们不会被测试框架视为独立的测试用例。这样可以使测试函数更加清晰和简洁 | | Name() | 返回正在运行的测试或基准测试的名称 | | Parallel() | Parallel 函数用于标记测试函数可以并行执行。当测试函数被标记为 Parallel 时,测试框架会在执行测试时创建多个 goroutine 并行执行这些测试函数,从而加快测试的执行速度 | | Setenv(key, value string) | 设置环境变量 | | Skip(args ...any) | 打印日志,并执行SkipNow() | | Skipf(format string, args ...any) | 打印日志,并执行SkipNow() | | SkipNow() | 跳过当前测试,而不会导致测试失败 | | Skipped() bool | 检查函数是否跳过 | | TempDir() string | 用于创建一个临时目录,辅助测试 | **Cleanup与 defer 的区别:** 1. 执行时机不同:Cleanup 函数会在测试函数执行完毕后立即执行,而 defer 语句会在当前函数返回前执行。 2. 调用方式不同:Cleanup 函数需要在测试函数中显式调用,而 defer 语句可以在任何函数中使用。 3. 多个调用的处理方式不同:Cleanup 函数可以多次调用,每次调用都会按照调用顺序执行清理操作;而 defer 语句只能在当前函数中使用一次,多次调用会按照后进先出的顺序执行。 4. 错误处理方式不同:Cleanup 函数的清理操作不会影响测试函数的错误处理,即使清理操作中发生错误,测试函数的错误仍然会被捕获和报告;而 defer 语句中的清理操作如果发生错误,可能会影响当前函数的错误处理。 5. 总的来说,Cleanup 函数适用于在测试函数执行完毕后进行一些清理操作,而 defer 语句适用于在函数返回前执行一些必要的清理操作。 ``` func TestMD5(t *testing.T) { if MD5("xxxxx") != "fb0e22c79ac75679e9881e6ba183b354" { t.Fatalf("执行失败") } t.Log("执行成功") } ``` ## <span style="font-size:15px">**三、性能测试(b *testing.B)**</span> * 用例需以 Benchmark 为前缀 * 参数必须是 b *testing.B * 函数返回值必须为空 | 库函数 | 说明 | | --- | --- | | Log(args ...any) | 打印日志 | | Logf(format string, args ...any) | 格式化其参数打印日志 | | Fatal(args ...any) | 打印日志,执行FailNow()函数 | | Fatalf(format string, args ...any) | 打印日志,执行FailNow()函数 | | Error(args ...any) | 打印错误日志后,执行Fail()函数 | | Errorf(format string, args ...any) | 打印错误日志后,执行Fail()函数 | | Run(name string, f func(t *B)) bool | 运行单测函数 | | Cleanup(f func()) | 作用类似于在函数中执行defer来做程序执行后的后续操作,比如释放资源、清除依赖等。<br>参考:https://www.cnblogs.com/YYRise/p/12376689.html#%E4%BD%BF%E7%94%A8-cleanup | | Fail() | Fail将函数标记为已失败,但仍在继续执行。 | | Failed() | 检查函数是否已失败 | | FailNow() | 将函数标记失败并立即停止执行 | | Helper() | Helper 函数用于将调用函数标记为测试辅助的函数。有时候测试中可能会有一些重复的代码或者需要共享的辅助函数,这时就可以使用 Helper 函数来避免重复代码,提高测试代码的可维护性。Helper 函数可以被测试函数调用,但是它们不会被测试框架视为独立的测试用例。这样可以使测试函数更加清晰和简洁 | | Name() | 返回正在运行的测试或基准测试的名称 | | Setenv(key, value string) | 设置环境变量 | | Skip(args ...any) | 打印日志,并执行SkipNow() | | Skipf(format string, args ...any) | 打印日志,并执行SkipNow() | | SkipNow() | 跳过当前测试,而不会导致测试失败 | | Skipped() bool | 检查函数是否跳过 | | TempDir() string | 用于创建一个临时目录,辅助测试 | | N | 表示循环的次数,因为需要反复调用测试代码来评估性能。b.N 的值会以1, 2, 5, 10, 20, 50, …这样的规律递增下去直到运行时间大于1秒钟,由于程序判断运行时间稳定才会停止运行 | | ReportAllocs() | ReportAllocs为该基准启用malloc统计信息。它相当于设置test.benchmem,但它只影响调用ReportAllocs的基准函数。 | | ReportMetric(n float64, unit string) | 用于报告性能测试中收集到的指标数据,其中 n 是浮点数类型的指标值,unit 是指标的单位。 | | ResetTimer() | ResetTimer 是 Golang 中 testing 包中的一个函数,用于重置计时器,通常用于基准测试中。在基准测试中,我们通常会使用 StartTimer 来开始计时,然后执行被测试的代码,最后使用 StopTimer 来停止计时。在循环测试中,ResetTimer 可以用于重置计时器,以便在同一段代码上多次运行计时,而不必重新创建计时器。 | | RunParallel(body func(*PB)) | 用于在多个goroutine中并行运行测试函数。在调用 RunParallel 方法时,需要提供一个函数,该函数会在多个goroutine中并行执行。 | | SetBytes(n int64) | SetBytes 方法用于设置每次操作处理的数据大小。这个方法通常用于基准测试中,可以帮助测试者评估在不同数据负载下的性能表现。通过设置数据大小,可以更好地模拟实际应用中的场景,从而更准确地评估代码的性能。 | | SetParallelism(p int) | 用于设置并行测试的数量 | | StartTimer() | 开始计时测试。此函数在测试启动前自动调用,但也可以用于在调用StopTimer后恢复计时 | | StopTimer() |停止计时| **运行命令:** 性能测试命令为 go test [参数],比如 go test -bench=. ,具体的命令参数及含义如下: * -bench regexp 性能测试,运行指定的测试函。 * -bench . 运行所有的benchmark函数测试,指定名称则只执行具体测试方法而不是全部。 * -benchmem 性能测试的时候显示测试函数的内存分配的统计信息。 * -count n 运行测试和性能多少此,默认一次。 * -run regexp 只运行特定的测试函数。 * -timeout t 测试时间如果超过 t 则panic,默认10分钟。 * -v 显示测试的详细信息。 ``` func BenchmarkValid(b *testing.B) { str := `{"foo":"bar"}` b.ResetTimer() for i := 0; i < b.N; i++ { json.Valid(([]byte(str))) } } [root@master-4vfjd util]# go test -bench=BenchmarkValid goos: linux goarch: amd64 pkg: sangfor.com/xdr/xlink/common/util cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkValid-4 9462286 133.8 ns/op PASS ok sangfor.com/xdr/xlink/common/util 3.931s ``` **运行结果含义:** * BenchmarkValid 是性能测试函数名称 * -4 表示 GOMAXPROCS 的值为4 * 9462286 表示一共执行了9462286次,即b.N的值 * 133.8 ns/op 表示平均每次操作花费了 133.8 纳秒 ## <span style="font-size:15px">**四、代码覆盖率测试**</span> * 执行并输出覆盖率:`go test -v -cover` * 将 cover 的详细信息保存到cover.out 中:`go test -cover -coverprofile=cover.out -covermode=count` * -cover 允许代码分析 * -covermode 代码分析模式(set:是否执行;count:执行次数;atomic:次数,并发执行) * -coverprofile 输出结果文件 * go tool cover -func=cover.out ### <span style="font-size:15px">1. 测试单元测试的覆盖率</span> ``` // 结果显示只有43.9%覆盖率 [root@master-4vfjd util]# go test -v -cover === RUN Test2 httpChainUtil_test.go:113: http://127.0.0.1:2563 --- PASS: Test2 (0.00s) === RUN Test1 httpChainUtil_test.go:140: http://127.0.0.1:12707 --- PASS: Test1 (0.12s) === RUN TestBytes2ReadOnlyString === RUN TestBytes2ReadOnlyString/Test_Bytes2ReadOnlyString --- PASS: TestBytes2ReadOnlyString (0.00s) PASS coverage: 43.9% of statements ok sangfor.com/xdr/xlink/common/util 0.367s ``` ### <span style="font-size:15px">2. 查看每个函数的覆盖率</span> ``` [root@master-4vfjd util]# go test -cover -coverprofile=cover.out -covermode=count ... [root@master-4vfjd util]# go tool cover -func=cover.out ... sangfor.com/xdr/xlink/common/util/rsaCrypt.go:19: ReadFile 76.9% sangfor.com/xdr/xlink/common/util/rsaCrypt.go:42: RsaDecrypt 0.0% sangfor.com/xdr/xlink/common/util/rsaCrypt.go:81: RsaEncrypt 64.0% total: (statements) 43.9% ``` ### <span style="font-size:15px">3. 使用html 的方式查看具体的覆盖情况</span> ``` [root@master-4vfjd util]# go tool cover -html=cover.out HTML output written to /tmp/cover629243415/coverage.html ``` ![](https://img.kancloud.cn/02/73/0273f43067f78f8330f33ae45b731bdf_1137x412.png) ## <span style="font-size:15px">**五、并发测试**</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;默认情况下,指定包的测试是按照顺序执行的,但也可以通过在测试的函数内部使用`t.Parallel()`来标志某些测试也可以被安全的并发执行。在并行执行的情况下,只有当那些被标记为并行的测试才会被并行执行,所以只有一个测试函数时是没意义的。 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在并发情况下,同时运行的测试的数量默认取决于`GOMAXPROCS`。它可以通过`-parallel n`被指定(`go test -parallel 4`) **并发测试实例:** ``` package main import ( "fmt" "testing" "time" ) func TestParallel(t *testing.T) { // 设置测试的并行数量 t.Parallel() // 模拟一个需要耗时的测试 time.Sleep(1 * time.Second) // 打印测试结果 fmt.Println("Test completed") } func TestSequential(t *testing.T) { // 模拟一个需要耗时的测试 time.Sleep(1 * time.Second) // 打印测试结果 fmt.Println("Test completed") } func main() { // 运行测试 testing.Main(func(pat, str string) (bool, error) { return true, nil }, []testing.InternalTest{ {Name: "TestParallel", F: TestParallel}, {Name: "TestSequential", F: TestSequential}, }, nil, nil) } ``` ## <span style="font-size:15px">**六. 竞争检查**</span> 数据竞争是指两个或多个goroutine并发访问同一块内存,并且至少其中一个是写操作,可能会导致panic等问题,如`panic: runtime error: invalid memory address or nil pointer dereference` `go run-v -race`:-race 参数是指开启竞争检查,可以检测并发操作是否安全。 `go test -race`:执行测试并开启竞争检查 **实例:** ``` // vim race.go package main import "fmt" func main() { done := make(chan bool) m := make(map[string]string) m["name"] = "world" go func() { m["name"] = "data race" done <- true }() fmt.Println("Hello,", m["name"]) <-done } ``` ``` [root@master-4vfjd util]# go run -race race.go Hello, world ================== WARNING: DATA RACE Write at 0x00c000070150 by goroutine 7: runtime.mapassign_faststr() /root/go1.18.10/go/src/runtime/map_faststr.go:203 +0x0 main.main.func1() /root/XLink/common/util/race.go:10 +0x50 Previous read at 0x00c000070150 by main goroutine: runtime.mapaccess1_faststr() /root/go1.18.10/go/src/runtime/map_faststr.go:13 +0x0 main.main() /root/XLink/common/util/race.go:13 +0x16b Goroutine 7 (running) created at: main.main() /root/XLink/common/util/race.go:9 +0x14e ================== ================== WARNING: DATA RACE Write at 0x00c000100088 by goroutine 7: main.main.func1() /root/XLink/common/util/race.go:10 +0x5c Previous read at 0x00c000100088 by main goroutine: main.main() /root/XLink/common/util/race.go:13 +0x175 Goroutine 7 (running) created at: main.main() /root/XLink/common/util/race.go:9 +0x14e ================== ``` &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在这段代码中,goroutine 7 和主goroutine 同时访问了map m,其中一个是写操作,另一个是读操作,导致了数据竞争。 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;具体来说,goroutine 7 在匿名函数中对map m进行了写操作,而主goroutine 中的 fmt.Println("Hello,", m["name"]) 则是对map m进行了读操作,这就引发了数据竞争。 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在运行时,Go语言的竞争检测器(race detector)检测到了这个问题,并给出了警告信息,指出了具体的数据竞争情况,包括读写的内存地址和涉及的goroutine。 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;要解决这个问题,可以使用互斥锁(mutex)来保护并发访问的map,确保同一时间只有一个goroutine在访问该map,从而避免数据竞争的发生。 ``` // 互斥锁方式修复 package main import ( "fmt" "sync" ) func main() { done := make(chan bool) m := make(map[string]string) m["name"] = "world" var mutex sync.Mutex go func() { mutex.Lock() m["name"] = "data race" mutex.Unlock() done <- true }() mutex.Lock() fmt.Println("Hello,", m["name"]) mutex.Unlock() <-done } ```