如今的软件复杂性,给开发带来了大量的精力,有两个方式可以有效缓解这个问题:软件发布之前的同行评审(业务、产品、技术)、以及软件有效的测试(自动化测试)
Go基于轻量级的测试方式,基于go工具链以及相关的函数进行,同时测试还涉及压力测试和文档示例
1. Golang相关测试基础
1.1. *_test文件
go test
扫描以*_test.go
结尾的文件(不被作为go build
编译的目标),寻求其中的特殊函数,并生成一个临时的main包,让后编译,运行,汇报结果,清空临时文件,该类文件中有3类函数被特殊对待:
Test
前缀函数:用于功能测试,汇报用例检测程序运行情况;Benchmark
前缀函数:用于基准测试(性能测试),汇报平均执行时间;Example
前缀函数:用户示例,godoc文档结合,以及playground执行示例
1.2. Test函数
每个测试文件必须导入testing包,其函数签名为:
1
2
3
4
5
6
7
| func TestName(t *testing.T)
// 相关信息输出函数
t.Error()
t.Errorf()
t.Logf()
...
|
1.3. go test命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 运行软件包的测试套件, -v相关明细信息输出
go test
// -run 执行指定的测试用例
go test -run "Afn|Bfn" -v
// 测试覆盖情况
go test -v -run TestRandIsPalindrome -covermode=count -coverprofile=c.out
go tool cover -html=c.out
// 运行基准测试,输出CPU相关性能分析
go test -bench=BenchmarkRandPalindrome -cpuprofile=cpu.log .
// 利用pprof查看分析具体的程序性能情况
go tool pprof -text -nodecount=10 ./hw.test cpu.log
|
2. Golang功能测试
2.1. 测试方式 - 表项测试、随机测试
- 基于表项测试,模式遵循:
got := f(x); got != want
- 基于随机输入测试策略:
- 输入是随机的,基于低效但准确的函数做被测试函数或方法验证;
- 输入是有规律的符合某种模式的输入,导致有规律的随机输出做验证
Tips: 基于随机输入,可以利用到rand包,注意保留seed输出,方便错误时候可以复现!
1
2
3
4
5
6
| // 随机函数
seed := time.Now().UinxNano()
rnd := rand.New(rand.NewSource(seed)) // *rand.Rand类型
...
n := rnd.Intn(25) // 生成[0,n)内的整数
c := rune(rnd.Intn(0x1000)) // 随机产生[0, \x10000)的rune字符
|
2.2. 测试类型 - 白盒、黑盒测试
白盒测试:基于对测试包的内部了解程度来做区分,通过包内部代码的观察和改动以及内部函数调用,白盒测试可能对代码和数据有侵入性。
白盒实施:
- 可以通过伪实现替代部分产品功能避免数据侵入的副作用(比如信用卡支付、更新产品数据库、邮件发送),需要注意全局变量替换风险,测试完后需要还回来
- 可以通过数据标记染色后期过滤处理(比如数据打标)
黑盒测试:假设对包的了解仅通过公开的API和文档,对包内部的逻辑规则是不透明的。
2.3. 测试用例代码覆盖率
语句覆盖率:语句覆盖率指部分语句在一次执行中至少执行一次,go中是通过cover工具(被集成到go tes中了)来衡量的。
测试套件覆盖待测试包的比例称为测试的覆盖率,通常我们用语句覆盖率作为简单衡量方式;
Tips:不要一味的追求代码覆盖率,适当的衡量代码错误检测以及测试用例的开发成本!
以下基于命令行中执行代码覆盖率检测,IDE中也可以有类似效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 相关帮助
$ go tool cove
// 以下测试回文函数的代码覆盖率,进入到指定包内执行命令
$ go test -v -run TestRandIsPalindrome -covermode=count -coverprofile=c.out
=== RUN TestRandIsPalindrome
--- PASS: TestRandIsPalindrome (0.00s)
hw_test.go:48: Random seed: 1557913657400098000
hw_test.go:53: hw: ȡਏਏȡ
hw_test.go:53: hw: һցƆହʬޤೆયྡྷྡྷયೆޤʬହƆցһ
hw_test.go:53: hw: ŘԚڋಕ༥ࣚɨຒɨࣚ༥ಕڋԚŘ
hw_test.go:53: hw: ѿഫ/శ߷ଵླྀଵ߷శ/ഫѿ
hw_test.go:53: hw: ଏޥौ)Ϸ˽ஒڍʒϊ̓ϊʒڍஒ˽Ϸ)ौޥଏ
PASS
coverage: 88.9% of statements
ok github.com/tkstorm/example/test/hw 0.007s
$ ls
c.out hw.go hw_bench.go hw_test.go
// 代码覆盖率查看(html查看)
$ go tool cover -html=c.out
|
2.4. 外部测试包打破循环依赖
避免包循环依赖的问题,引入一个外部测试包解决
net/http
依赖 net/url
net/url
包内部依赖 net/http
做功能测试,直接使用会引起循环依赖,该问题如何解决?
因为Go规范禁止循环依赖,以上问题,可以通过导入一个外部测试包net/url_test
来做集成测试,打破循环依赖:
net/url_test
=> 依赖net/http
、net/url
,从而打破循环依赖
2.5. 查看产品代码、包内测试代码、外部测试代码
1
2
3
4
5
6
7
8
9
10
11
| // 产品功能文件列表
$ go list -f={{.GoFiles}} fmt
[doc.go format.go print.go scan.go]
// 测试后门,导出内部文件列表
$ go list -f={{.TestGoFiles}} fmt
[export_test.go]
// 包外部测试文件列表
$ go list -f={{.XTestGoFiles}} fmt
[example_test.go fmt_test.go scan_test.go stringer_test.go]
|
3. 性能测试
基准测试,就是在一定的工作负载之下,检测程序性能的一种方式。通过使用较小的N值检测稳定的运行时间,推断出足够大的N值!
基准测试在程序设计之初,可以很好的体现软件的性能结果,随着时间增长,后续也可以基于此来回顾当初的设计决策!
3.1. Benchmark基准测试
Go中的基准测试,函数命名以Benchmark
前缀开头,也是写在_test.go
文件之中:
1
2
3
4
5
6
7
8
9
| // BenchmarkRandPalindrome压力测试
func BenchmarkRandPalindrome(b *testing.B) {
seed := time.Now().UnixNano()
rnd := rand.New(rand.NewSource(seed))
for i := 0; i < b.N; i++ {
RandPalindrome(rnd)
}
}
|
命令行执行(IDE中也可以执行),压测结果体现有OS操作系统、CPU架构、包信息、基础函数名称,以及压测结果:
1
2
3
4
5
6
7
8
9
10
11
12
| // 压测包下所有基准测试函数
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/tkstorm/example/test/hw
BenchmarkRandPalindrome-4 2000000 631 ns/op
BenchmarkIsPalindrome-4 20000000 85.2 ns/op
PASS
ok github.com/tkstorm/example/test/hw 3.733s
// 压测包下指定基准测试函数
$ go test -bench=BenchmarkIsPalindrome
|
3.2. 基准测试结果说明
- BenchmarkIsPalindrome-4:4代表GOMAXPROCS值,代表CPU个数,对并发基准测试很重要
- 2000000:执行次数
- 631 ns/op: 每次函数调用执行的时间,这块并未执行具体这么多次(而是基于较小的N次运行稳定后,推断出来较大的N执行耗时)
基准测试没有和功能测试混合,一方面是职责分清,另一方面是基准测试可以在循环之前做一些初始化的工作!
最快的程序,通常是那些进行内存分配次数最少的程序,可以基于**-benchmem**查看内存分配情况(allocs/op
),通过类似的方式,我们可以清晰知道应用内存分配情况!
1
2
3
4
5
6
7
8
| $ go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/tkstorm/example/test/hw
BenchmarkRandPalindrome-4 2000000 628 ns/op 99 B/op 2 allocs/op
BenchmarkIsPalindrome-4 20000000 81.5 ns/op 24 B/op 2 allocs/op
PASS
ok github.com/tkstorm/example/test/hw 3.630s
|
3.3. 在基准测试时候,需要考虑的几个问题
- 如果一个函数需要1ms处理1000个元素,那么处理1w、100w需要多久?
- 都知道临时申请内存慢,那如果提前申请,类似I/O缓存最佳大小是多少?
- 对于一个任务,当前的执行程序是否是最优算法?
除此外,我们在针对量级size上面,有如下设计考虑,而不是基于之前的b.N
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // benchmark内部执行IsPalindrome基准测试
func benchmarkIsPalindrome(b *testing.B, size int) {
for i := 0; i < size; i++ {
hw.IsPalindrome("aba")
}
}
func BenchmarkIsPalindrome10(b *testing.B) {
benchmarkIsPalindrome(b, 10)
}
func BenchmarkIsPalindrome100(b *testing.B) {
benchmarkIsPalindrome(b, 100)
}
func BenchmarkIsPalindrome1000(b *testing.B) {
benchmarkIsPalindrome(b, 1000)
}
|
3.4. 性能剖析 - pprof
不要过早优化,但不等同于性能不重要或者放弃优化!!使用性能检测工具,而非直觉!!
性能剖析是通过自动化的手动在程序执行过程中,基于一些性能事件的采样,从多维度来进行性能评测和分析。在获取性能剖析结果后,可以基于pprof工具进行分析。
- CPU性能剖析:识别出执行过程中需要CPU最多的函数(每次系统发生时钟中断,记录出相关堆栈内容)!比如Top10
1
2
3
4
5
6
7
| // 针对hw当前包,执行基准测试,生成cpu性能剖析文件到cpu.log
$ go test -bench=BenchmarkRandPalindrome -cpuprofile=cpu.log .
$ ls
cpu.log hw.go hw.test hw_test.go
// 基于go tool pprof工具,查询性能剖析中,前10条记录
$ go tool pprof -text -nodecount=10 ./hw.test cpu.log
|
- 内存性能剖析, 识别出负责分配最多内存的语句,对协程内部内存分配调用进行采样。
1
2
3
| // 对测试用例,基准测试没有代表性,使用过滤器 -run=none
$ go test -run=none -bench=ConcatStrings100000 -benchmem -cpuprofile=cpu.prof -memprofile=mem.prof ./gomemory/...
$ go tool pprof -http=:8900 mem.prof
|
3.5. 阻塞性能剖析
识别出系统调用、锁获取、通道发生和接收阻塞,性能分析在每次上述产生之一阻塞时候,记录一个事件
4. Example函数3个用途
- Example函数式通过编译的,可以作为可以执行的文档,胜过文字描述;与godoc结合使用,
ExampleFnName
将和FnName
的文档显示在一起,如果整个包有一个Example
,则和包关联 - 可以通过go test执行测试,其中的
// Output: olleh
,就是期望输出 - 提供playground的环境;
Example - test测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // stringutil包
package stringutil_test
import (
"fmt"
"github.com/golang/example/stringutil"
)
func ExampleReverse() {
fmt.Println(stringutil.Reverse("hello"))
// Output: olleh
}
$ go test -v
=== RUN TestReverse
--- PASS: TestReverse (0.00s)
=== RUN: ExampleReverse
--- PASS: ExampleReverse (0.00s)
PASS
ok github.com/golang/example/stringutil 0.009s
|
Example - godoc
Godoc示例是编写和维护代码作为文档的好方法。它们还提供了用户可以建立的可编辑,可运行且可运行的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| package sort_test
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func Example() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
// Output:
// [Bob: 31 John: 42 Michael: 17 Jenny: 26]
// [Michael: 17 Jenny: 26 Bob: 31 John: 42]
}
|
6. 测试方法论
6.1. 测试用例基本要求
- 首先确保期望实现的具体新闻,以及针对关键部分的足够的用例场景覆盖(关键覆盖率)
- 统一、清晰、简洁的测试日志格式输出,记录相关的入参、出参等(适当t.Printf)
- 出现错误时候,确保现场相关数据得以记录,在后续过程中可以重现(比如seed种子等)
- 可以在一次测试运行中报告多处错误,而不会出现中断
- 在测试代码中,不要调用
log.Fatal
和os.Exit
,这两个函数会阻止跟踪的过程,应该考虑即使测试用例本身失败,测试驱动程序应该继续工作。 - 考虑通过伪实现的方式替换部分产品功能(通过共享全局包变量方式)
export_test.go
,通过将内部的功能暴露给外部测试,提供一个后门
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 消息通知用户
notifyUser = func(user,msg string){..}
// 产品函数
func ProdFn(){
...
notifyUser(user, msg)
}
// 测试函数
func TestFn(t *testing.T){
saved := notifyUser
defer func() { notifyUser = saved}() // 测试后还原回来之前的notifyUser
// 伪实现一个测试用例的notifyUser函数方法,模拟邮件发送
notifyUser = func(user, msg string) {
t.Logf(user, msg)
}
}
|
6.2. 提升测试程序的健壮性
- 研发人员:提高程序的健壮性,降低程序的缺陷率(遇到新的合法输入,程序就崩溃)
- 测试人员:提高测试用例的健壮性,降低测试用例的脆弱性(应用程序升级,测试用例就失败)
- 解决方案:Care you Care,仅关注需要重点关注的信息,而不用关注所有(比如相关结构中最初始最重要的元素,而不是结构的每一个元素)
7. 总结
概要介绍了Go语言中的功能测试与性能测试的编写方式,以及在测试用例编写过程中的一些需要注意的基本事项。结合go tool
工具链可以对Go程序的性能进行剖析!
Golang中的测试(Test、Benchmark、Example),其中功能测试可以通过表驱动、随机方式进行,随机测试需要注意seed种子的保留。测试类型支持黑盒和白盒测试,同时结合Golang的接口替换、染色标记可以做到测试对代码无侵入性。
基准测试,也支持表测试方式,支持不同基准下的应用性能测试结果分析。同时结合pprof
的cpu和mem内存分析,可以直观排查和分析应用方面的相关性能。