Go语言测试:覆盖率、基准测试与性能分析
在软件开发中,测试是确保程序质量和性能的关键环节。本文将深入探讨Go语言中的测试相关技术,包括测试覆盖率、基准测试和性能分析,通过具体的代码示例和操作步骤,帮助大家更好地掌握这些技术。
1. 测试覆盖率
测试覆盖率是衡量测试套件对被测包的执行程度的指标。正如计算机科学家Edsger Dijkstra所说:“测试只能表明缺陷的存在,而不能证明缺陷的不存在。” 无论进行多少测试,都无法保证一个包完全没有缺陷,但测试可以增加我们对包在各种重要场景下正常工作的信心。
1.1 语句覆盖率
语句覆盖率是最简单且最广泛使用的覆盖率度量方法,它表示测试套件在测试过程中至少执行一次的源语句的比例。在Go语言中,我们可以使用Go的
cover
工具(集成在
go test
中)来测量语句覆盖率,并帮助识别测试中的明显漏洞。
以下是一个表达式求值器的表驱动测试示例:
package main
import (
"fmt"
"testing"
)
// 假设这些函数和类型已经定义
type Env map[string]float64
type Var string
type Expr interface {
Eval(env Env) float64
Check(vars map[Var]bool) error
}
func Parse(input string) (Expr, error) {
// 实现解析逻辑
return nil, nil
}
func TestCoverage(t *testing.T) {
var tests = []struct {
input string
env Env
want string // expected error from Parse/Check or result from Eval
}{
{"x % 2", nil, "unexpected '%'"},
{"!true", nil, "unexpected '!'"},
{"log(10)", nil, `unknown function "log"`},
{"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"},
{"sqrt(A / pi)", Env{"A": 87616, "pi": 3.141592653589793}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
}
for _, test := range tests {
expr, err := Parse(test.input)
if err == nil {
err = expr.Check(map[Var]bool{})
}
if err != nil {
if err.Error() != test.want {
t.Errorf("%s: got %q, want %q", test.input, err, test.want)
}
continue
}
got := fmt.Sprintf("%.6g", expr.Eval(test.env))
if got != test.want {
t.Errorf("%s: %v => %s, want %s",
test.input, test.env, got, test.want)
}
}
}
1.2 操作步骤
- 检查测试是否通过 :
go test -v -run=Coverage gopl.io/ch7/eval
- 显示覆盖率工具的使用信息 :
go tool cover
- 运行测试并收集覆盖率数据 :
go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
这个命令会通过对生产代码进行插桩来收集覆盖率数据。插桩会修改源代码的副本,使得在每个语句块执行之前设置一个布尔变量(每个语句块一个变量)。在修改后的程序退出之前,它会将每个变量的值写入指定的日志文件
c.out
,并打印执行语句的比例摘要。如果只需要摘要信息,可以使用
go test -cover
。
-
使用
-covermode=count标志 :
go test -run=Coverage -covermode=count -coverprofile=c.out gopl.io/ch7/eval
当使用
-covermode=count
标志运行
go test
时,每个语句块的插桩会递增一个计数器而不是设置一个布尔变量。这样得到的每个语句块的执行计数日志可以进行定量比较,区分出执行更频繁的“热点”块和执行较少的“冷点”块。
- 生成HTML报告 :
go tool cover -html=c.out
运行这个命令后,
cover
工具会处理日志,生成一个HTML报告,并在新的浏览器窗口中打开它。在报告中,每个语句如果被覆盖则显示为绿色,未被覆盖则显示为红色。
通过分析覆盖率报告,我们可以发现测试用例中未覆盖的代码部分,例如上述示例中,没有输入测试一元运算符的
Eval
方法。我们可以添加新的测试用例来提高覆盖率:
{"-x * -x", Env{"x": 2}, "4"}
需要注意的是,实现100%的语句覆盖率在实践中通常是不可行的,也不一定是值得投入精力的目标。仅仅执行一个语句并不意味着它没有缺陷,包含复杂表达式的语句需要使用不同的输入多次执行才能覆盖各种有趣的情况。
2. 基准测试
基准测试是衡量程序在固定工作负载下性能的实践。在Go语言中,基准测试函数看起来像测试函数,但以
Benchmark
为前缀,并带有一个
*testing.B
参数,该参数提供了与
*testing.T
大多数相同的方法,以及一些与性能测量相关的额外方法。此外,它还暴露了一个整数字段
N
,用于指定要执行被测量操作的次数。
2.1 基准测试示例
以下是一个
IsPalindrome
函数的基准测试示例:
package main
import (
"testing"
)
func IsPalindrome(s string) bool {
// 实现回文判断逻辑
return true
}
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
2.2 操作步骤
- 运行基准测试 :
cd $GOPATH/src/gopl.io/ch11/word2
go test -bench=.
与测试不同,默认情况下不会运行任何基准测试。
-bench
标志的参数用于选择要运行的基准测试,它是一个正则表达式,匹配
Benchmark
函数的名称,默认值不匹配任何函数。上述命令中的
'.'
模式会匹配
word
包中的所有基准测试。
基准测试名称的数字后缀(如上述示例中的
8
)表示
GOMAXPROCS
的值,这对于并发基准测试很重要。报告告诉我们,在1,000,000次运行中,每次调用
IsPalindrome
大约需要1.035微秒。
2.3 性能优化
有了基准测试后,我们可以尝试一些优化方案来提高程序的性能。例如,对
IsPalindrome
函数的第二个循环进行优化,使其在中点停止检查,避免每次比较两次:
n := len(letters)/2
for i := 0; i < n; i++ {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
但通常情况下,明显的优化并不总是能带来预期的收益,这个优化在一次实验中只带来了4%的性能提升。
另一个优化思路是预先分配足够大的数组来存储字母,而不是通过连续调用
append
来扩展数组:
letters := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
这个优化带来了近35%的性能提升,基准测试运行次数增加到2,000,000次。
我们可以使用
-benchmem
命令行标志在报告中包含内存分配统计信息,比较优化前后的内存分配情况:
| 优化前 | 优化后 |
| ---- | ---- |
|
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
|
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
|
从结果可以看出,将分配合并到一次
make
调用中消除了75%的分配,并将分配的内存量减半。
2.4 比较基准测试
在很多情况下,我们更关心不同操作的相对性能。比较基准测试通常采用单个参数化函数的形式,从多个
Benchmark
函数中使用不同的值调用它:
func benchmark(b *testing.B, size int) {
// 实现基准测试逻辑
}
func Benchmark10(b *testing.B) {
benchmark(b, 10)
}
func Benchmark100(b *testing.B) {
benchmark(b, 100)
}
func Benchmark1000(b *testing.B) {
benchmark(b, 1000)
}
需要注意的是,要避免使用参数
b.N
作为输入大小,除非将其解释为固定大小输入的迭代次数,否则基准测试结果将毫无意义。
比较基准测试揭示的模式在程序设计过程中特别有用,并且在程序运行过程中,随着程序的演进、输入的增长或部署在具有不同特性的新操作系统或处理器上,我们可以重新使用这些基准测试来重新审视设计决策。
以下是一个简单的流程图,展示基准测试的流程:
graph TD;
A[编写基准测试函数] --> B[运行基准测试];
B --> C[分析基准测试结果];
C --> D{是否需要优化};
D -- 是 --> E[进行性能优化];
E --> B;
D -- 否 --> F[结束];
通过以上内容,我们了解了Go语言中测试覆盖率和基准测试的相关知识和操作方法。在下半部分,我们将继续探讨Go语言中的性能分析技术,包括CPU分析、堆分析和阻塞分析等。
Go语言测试:覆盖率、基准测试与性能分析
3. 性能分析
基准测试对于测量特定操作的性能很有用,但当我们试图让一个缓慢的程序变得更快时,往往不知道从哪里开始。Donald Knuth曾说过关于过早优化的名言,虽然这句话常被误解为性能不重要,但在其原始语境中,我们可以理解为:程序员不应在非关键部分浪费大量时间思考或担心程序的速度,过早优化可能会在调试和维护时产生负面影响,我们应该忽略大约97%的小效率问题,但也不应错过那关键的3%。而要仔细查看程序的速度,最好的技术就是性能分析。
性能分析是一种基于在执行期间对大量性能事件进行采样,然后在后期处理步骤中进行推断的自动化性能测量方法,得到的统计摘要称为性能分析结果。Go支持多种类型的性能分析,每种都关注性能的不同方面,但都涉及记录一系列感兴趣的事件,每个事件都有一个伴随的堆栈跟踪。
go test
工具内置了对几种类型性能分析的支持。
3.1 不同类型的性能分析
- CPU分析 :识别执行所需CPU时间最多的函数。操作系统会每隔几毫秒周期性地中断每个CPU上当前运行的线程,每次中断都会记录一个性能分析事件,然后恢复正常执行。
- 堆分析 :识别负责分配最多内存的语句。分析库会对内部内存分配例程的调用进行采样,平均每分配512KB内存记录一个性能分析事件。
- 阻塞分析 :识别导致goroutine阻塞时间最长的操作,如系统调用、通道发送和接收以及锁的获取。每当goroutine被这些操作阻塞时,分析库就会记录一个事件。
3.2 收集性能分析数据
收集被测代码的性能分析数据非常简单,只需启用以下标志之一即可。但要注意,同时使用多个标志时,收集一种类型分析数据的机制可能会影响其他类型的结果。
go test -cpuprofile=cpu.out
go test -blockprofile=block.out
go test -memprofile=mem.out
对于非测试程序,也可以很容易地添加性能分析支持,不过具体实现细节会因短期运行的命令行工具和长期运行的服务器应用程序而有所不同。在长期运行的应用程序中,性能分析尤其有用,因为可以使用Go运行时的API在程序员的控制下启用性能分析功能。
3.3 分析性能分析数据
收集到性能分析数据后,需要使用
pprof
工具进行分析。
pprof
是Go发行版的标准部分,但由于它不是日常工具,需要通过
go tool pprof
间接访问。它有数十个功能和选项,基本使用只需要两个参数:生成性能分析数据的可执行文件和性能分析日志。
由于日志中不包含函数名,而是通过函数地址来标识函数,所以
pprof
需要可执行文件才能理解日志内容。虽然
go test
通常在测试完成后会丢弃测试可执行文件,但启用性能分析时,它会将可执行文件保存为
foo.test
,其中
foo
是被测包的名称。
以下是收集和显示简单CPU性能分析数据的步骤:
go test -run=NONE -bench=ClientServerParallelTLS64 \
-cpuprofile=cpu.log net/http
上述命令选择了
net/http
包中的一个基准测试进行性能分析,通常对经过精心构造以代表感兴趣工作负载的特定基准测试进行分析会更好。使用
-run=NONE
过滤掉测试用例,因为测试用例的性能分析通常不具有代表性。
go tool pprof -text -nodecount=10 ./http.test cpu.log
-text
标志指定输出格式为文本表格,每行对应一个函数,按“热点”函数(消耗CPU周期最多的函数)优先排序。
-nodecount=10
标志将结果限制为10行。对于严重的性能问题,这种文本格式可能足以找出原因。
下面是一个示例输出:
| flat | flat% | sum% | cum | cum% | 函数名 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 1730ms | 48.19% | 48.19% | 1750ms | 48.75% | crypto/elliptic.p256ReduceDegree |
| 230ms | 6.41% | 54.60% | 250ms | 6.96% | crypto/elliptic.p256Diff |
| 120ms | 3.34% | 57.94% | 120ms | 3.34% | math/big.addMulVVW |
| 110ms | 3.06% | 61.00% | 110ms | 3.06% | syscall.Syscall |
| 90ms | 2.51% | 63.51% | 1130ms | 31.48% | crypto/elliptic.p256Square |
| 70ms | 1.95% | 65.46% | 120ms | 3.34% | runtime.scanobject |
| 60ms | 1.67% | 67.13% | 830ms | 23.12% | crypto/elliptic.p256Mul |
| 60ms | 1.67% | 68.80% | 190ms | 5.29% | math/big.nat.montgomery |
| 50ms | 1.39% | 70.19% | 50ms | 1.39% | crypto/elliptic.p256ReduceCarry |
| 50ms | 1.39% | 71.59% | 60ms | 1.67% | crypto/elliptic.p256Sum |
从这个性能分析结果可以看出,椭圆曲线密码学对这个特定的HTTPS基准测试的性能很重要。相反,如果性能分析结果主要由运行时包中的内存分配函数主导,那么减少内存消耗可能是一个值得尝试的优化。
下面是一个性能分析的流程图:
graph TD;
A[选择要分析的基准测试] --> B[运行测试并收集性能分析数据];
B --> C[使用pprof工具分析数据];
C --> D[查看分析结果];
D --> E{是否需要优化};
E -- 是 --> F[进行性能优化];
F --> B;
E -- 否 --> G[结束];
4. 总结
通过本文,我们全面了解了Go语言中的测试相关技术,包括测试覆盖率、基准测试和性能分析。
-
测试覆盖率
:使用
go test和go tool cover可以方便地测量和分析语句覆盖率,帮助我们发现测试用例中的漏洞,提高代码的测试完整性。 -
基准测试
:通过编写基准测试函数并使用
go test -bench命令,可以测量程序在固定工作负载下的性能,并通过优化方案不断提高程序的性能。 -
性能分析
:利用
go test的性能分析标志和pprof工具,可以找出程序中消耗CPU时间、内存和导致阻塞的关键部分,从而有针对性地进行优化。
在实际开发中,合理运用这些测试技术可以帮助我们提高代码的质量和性能,确保程序在各种场景下都能稳定、高效地运行。希望这些内容能对大家在Go语言开发中的测试工作有所帮助。
超级会员免费看
92

被折叠的 条评论
为什么被折叠?



