目录
golang 单元测试、基准测试、子测试、并发测试基础教程
一、go test基础
用法: go test [build/test flags] [packages] [build/test flags & test binary flags]
Go test
ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...
Go test* _test.go
每个列出的包都执行单独的测试二进制文件。其中名称以“_”(包括“_test.go”)或“.”开头的文件将被忽略。而后缀为“_test”的测试文件将被编译为单独的程序包,并与主测试文件链接并执行。
go工具将忽略名为“testdata”的目录,该目录可用来存放测试所需的辅助数据。
go testrun vetgo vetgo vetgo vet-vet=off
所有测试输出和摘要行都打印到go命令的标准输出,即使测试将其打印为自己的标准错误。(go命令的标准错误保留用于打印建立测试时出错)。
Go test
go testgo test
go testgo test
go test
二、准备
接下来,将通过一个个例子说明单元测试、基准测试、子测试、并发测试到底该如何写,在此之前,我们需要先准备一个待测试的小功能。这里,我们以一个对全局map变量执行增删改查的功能为例,开发对应的测试代码。具体如下:
var Cash = make(map[string]string)
func Add(key,value string){
if _,ok := Cash[key];!ok{
Cash[key] = value
}
}
func Delete(key string){
if _,ok := Cash[key];ok{
delete(Cash,key)
}
}
func Update(key,value string){
Cash[key] = value
}
func Get(key string) string{
if v,ok := Cash[key];ok{
return v
}
return ""
}
func Clean(){
Cash = make(map[string]string)
}
这里我们可以没有为Cash变量添加锁机制,目的是为了在验证阶段通过并发测试找出这个’bug‘。另外,还额外提供了一个Clean()方法,用于清空变量的内容。
三、单元测试
单元测试是最简单,也是最基本的测试。例如,我们想要测试add和get功能是否正常,通常我们会这样写。
func TestAddAndGet(t *testing.T){
Add("a","aa")
fmt.Println(Get("a"))
}
这是最基本的测试方法,但是效率太低,同时需要开发者自己去判断结果是否正确。通常会通过Table-driven的方式实现这种简单重复的测试内容,我们可以按批次执行测试,并通过程序化的方式验证测试结果,具体如下:
func TestAdd(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
quary := rand.Int()
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
如果某次插入操作出现异常会返回错误信息:
=== RUN TestAdd
basic_test.go:48: add d:dd len = 4; except 4
--- FAIL: TestAdd (0.00s)
FAIL
四、基准测试
基准测试,通常也被成为压测Benchmark。压测通常会自动的顺序执行多次,然后返回平均的压测参数。例如我们测试get方法的性能:
func BenchmarkGet(b *testing.B) {
b.Log("start")
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
for _,v := range addTests{
Add(v.key,v.value)
}
//启动内存统计
b.ReportAllocs()
//重新计时
b.ResetTimer()
for i:=0;i<b.N;i++{
var result []string
for _,v := range addTests{
value := Get(v.key)
if value != v.value{
b.Errorf("get %s:%s, except %s",v.key, value,v.value)
}
result = append(result,value)
}
}
}
测试结果为:
goos: darwin
goarch: amd64
pkg: test-learn
BenchmarkGet
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
basic_test.go:185: start
BenchmarkGet-12 2162974 546 ns/op 496 B/op 5 allocs/op
PASS
b.ResetTimer()
如果我们关注的流程在测试代码前半段时呢?testing包提供了更加灵活的手动计时功能。
b.StartTimer()
b.StopTimer()
b.ReportAllocs()go test-benchmem
2162974 :基准测试的迭代总次数 b.N
546 ns/op:平均每次迭代所消耗的纳秒数
496 B/op:平均每次迭代内存所分配的字节数
5 allocs/op:平均每次迭代的内存分配次数
五、并发测试
t.Parallel()
1、可并发执行的测试用例
t.Parallel()
func TestCanParallelExecAdd(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
t.Parallel()
quary := rand.Int()
t.Logf("[goroutine:%d] start",quary)
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
func TestCanParallelExecAdd2(t *testing.T){
var addTests = []struct{
key string
value string
expected int
}{
{"a","aa",1},
{"b","bb",2},
{"c","cc",3},
{"c","cc",3},
{"c","cc",3},
{"d","dd",4},
{"e","ee",5},
{"f","ff",6},
{"g","gg",7},
{"h","hh",8},
{"i","ii",9},
{"j","jj",10},
}
t.Parallel()
quary := rand.Int()
t.Logf("[goroutine:%d] start",quary)
for _,v := range addTests{
Add(v.key,v.value)
t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
if len(Cash) != v.expected{
t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
}
}
Clean()
}
go testfatal error: concurrent map read and map writet.Parallel()
go test
到这里,基准并发测试的实现方式已经十分清晰,但是这需要十分冗余的测试代码,如果想要测试10并发量的测试难道要写10份逻辑一样的测试代码吗?
当然不是,testing包提供了子测试的概念,可以便于我们实现更加复杂的测试逻辑。
2、基于子测试的并发单元测试
子测试是指我们可以在单元测试中启动多个测试用例,具体如下:
func TestParallelAdd1(t *testing.T){
for i:=0;i<10;i++{
t.Run(fmt.Sprintf("g-%d",i), TestAdd)
}
}
我们通过t.run函数,批量启动了10个子测试用例。通过日志我们发现,这10个子测试用例是按顺序执行的,这是因为TestAdd单元测试并不支持并发执行,接下来我们对上述代码做出修改,为子测试用例添加可并发属性。
func TestParallelAdd(t *testing.T){
for i:=0;i<10;i++{
t.Run(fmt.Sprintf("g-%d",i), func(t *testing.T) {
t.Parallel()
TestAdd(t)
})
}
}
fatal error: concurrent map writes
3、并发基准测试
b.RunParallel
func BenchmarkParallelAdd(b *testing.B) {
b.Log("start")
var process uint32 = 0
var count uint64 = 0
b.SetParallelism(2)
b.RunParallel(func(pb *testing.PB) {
temp := atomic.AddUint32(&process,1)
b.Logf("[goroutine:%d] start",temp)
for pb.Next() {
atomic.AddUint64(&count,1)
// The loop body is executed b.N times total across all goroutines.
b.Logf("[goroutine:%d] count=%d",temp,atomic.LoadUint64(&count))
}
b.Logf("[goroutine:%d] end",temp)
})
}
通过 RunParallel 方法能够并行地执行给定的基准测试。RunParallel会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用SetParallelism(如 SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。
六、示例功能
ExampleXxx_xxx
func ExampleGet() {
Add("a","aa")
Add("b","bb")
fmt.Println(Get("a"))
fmt.Println(Get("b"))
// Output:
// aa
// bb
}
七、总结
通常测试用例文件被建议与源文件写在同一个包中。
测试常用命令
go testgo test -bench=.go test -vgo test -racego test -cover
最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:包括,软件学习路线图,50多天的上课视频、16个突击实战项目,80余个软件测试用软件,37份测试文档,70个软件测试相关问题,40篇测试经验级文章,上千份测试真题分享,还有2021软件测试面试宝典,还有软件测试求职的各类精选简历,希望对大家有所帮助…
想要获取上方这套学习资料(都是免费获取的~)
添加我们的小姐姐即可
可不能撩我们的小姐姐哦
码字不易,文章对你有帮助的话,点个赞收个藏,给作者一个鼓励。也方便你下次能够快速查找。