Go语言作为一门21世纪的语言,自然提供了测试工具。这篇文章就介绍一下如何对Go语言所写的程序进行测试。
Go语言测试命令主要就这一条go test
测试源代码文件都以*_test.go
为后缀,这些文件不会被编译成二进制文件。
单元测试
单元测试的测试函数以Test
为函数的前缀名,然后参数为*testing.T类型,比如:
func TestABC(t *testing.T) {
}
以下例子来源李文周的博客
我们对split.go这个文件进行测试
// split/split.go
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
result = make([]string, 0, strings.Count(s, sep)+1)
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
split_test.go
func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
type test struct {
input string
sep string
want []string
}
tests := map[string]test{
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
}
teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
defer teardownTestCase(t) // 测试之后执行testdoen操作
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
defer teardownSubTest(t) // 测试之后执行testdoen操作
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
}
})
}
}
执行go test -v
可以看到下面的效果
write setup code here...
=== RUN TestSplit
TestSplit: split_test.go:77: 如有需要在此执行:测试之前的setup
=== RUN TestSplit/simple
TestSplit/simple: split_test.go:85: 如有需要在此执行:子测试之前的setup
TestSplit/simple: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/wrong_sep
TestSplit/wrong_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
TestSplit/wrong_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/more_sep
TestSplit/more_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
TestSplit/more_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN TestSplit/leading_sep
TestSplit/leading_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
TestSplit/leading_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
TestSplit: split_test.go:79: 如有需要在此执行:测试之后的teardown
--- PASS: TestSplit (0.00s)
--- PASS: TestSplit/simple (0.00s)
--- PASS: TestSplit/wrong_sep (0.00s)
--- PASS: TestSplit/more_sep (0.00s)
--- PASS: TestSplit/leading_sep (0.00s)
=== RUN TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
=== RUN ExampleSplit
--- PASS: ExampleSplit (0.00s)
PASS
write teardown code here...
ok go_learning/split 1.408s
现在来分析一下每个语句做什么
我们只需要关注teardownTestCase下面的内容,这行以上的只是用于存储测试用例
teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
defer teardownTestCase(t) // 测试之后执行testdoen操作
这两句一个是在测试执行前执行,一个是之后执行,我们看一下setupTestCase(t)是如何编写的
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:测试之前的setup")
return func(t *testing.T) {
t.Log("如有需要在此执行:测试之后的teardown")
}
}
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
是将错误信息打印出来,类似的还有
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
t.Run(name, func(t *testing.T){})
是用来执行子测试的
通过go test -v -run=RegExp
可以用来只执行匹配的函数
go test -cover -coverprofile=
可以用来查看测试代码覆盖率
性能测试
进行性能测试的函数以Benchmark_
为前缀
func BenchmarkName(b *testing.B){}
b有如下的方法:
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
执行命令go test -bench=.
还可以加上-benchmem
来测试内存分配
func BenchmarkSplitParallel(b *testing.B) {
//go test -bench=. -cpu1
b.SetParallelism(2)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Split("沙河有沙又有河", "沙")
}
})
}
b.RunParallel(func(b *testing.B){})
是以并行的方式来进行测试,SetParallelism()
是设置cpu数
性能优化
go语言标准库中自带可以进行性能分析的工具
runtime/pprof
net/http/pprof
pprof开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取格格函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。注意只有在需要性能分析的时候才应该引入这些工具,因为它们同样会消耗性能
开启CPU性能分析
pprof.StartCPUProfile(w io.Writer)
关闭CPU性能分析
pprof.StopCPUProfile()
得到采样数据之后,使用go tool pprof
工具进行CPU性能分析。
内存性能分析
pprof.WriteHeapProfile(w io.Writer)
下面给个完整的例子
package main
import (
"flag"
"fmt"
"os"
"runtime/pprof"
"time"
)
// 一段有问题的代码
func logicCode() {
var c chan int
for {
select {
case v := <-c:
fmt.Printf("recv from chan, value:%v\n", v)
default:
}
}
}
func main() {
var isCPUPprof bool
var isMemPprof bool
flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
flag.Parse()
if isCPUPprof {
file, err := os.Create("./cpu.pprof")
if err != nil {
fmt.Printf("create cpu pprof failed, err:%v\n", err)
return
}
pprof.StartCPUProfile(file)
defer pprof.StopCPUProfile()
}
for i := 0; i < 8; i++ {
go logicCode()
}
time.Sleep(20 * time.Second)
if isMemPprof {
file, err := os.Create("./mem.pprof")
if err != nil {
fmt.Printf("create mem pprof failed, err:%v\n", err)
return
}
pprof.WriteHeapProfile(file)
file.Close()
}
}
其他
示例函数
示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
func ExampleSplit() {
fmt.Println(split.Split("a:b:c", ":"))
fmt.Println(split.Split("沙河有沙又有河", "沙"))
// Output:
// [a b c]
// [ 河有 又有河]
}
执行go test -run Example