单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。
单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
testing 提供对 Go 包的自动化测试的支持。通过 go test 命令,能够自动执行如下形式的任何函数:
func TestXxx(*testing.T)
- 测试用例文件不会参与正常源码编译,不会被包含到可执行文件中。
- 测试用例文件使用 go test 指令来执行,没有也不需要 main() 作为函数入口。所有在以_test结尾的源码内以Test开头的函数会自动被执行。
- 测试用例可以不传入 *testing.T 参数
在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。
要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。
如果有需要,可以调用 *T 和 *B 的 Skip 方法,跳过该测试或基准测试:
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
...
}
Go语言的单元测试对文件名和方法名,参数都有很严格的要求。
-
文件名必须以xxx_test.go命名
-
测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头。
func TestAdd(t *testing.T){ ... } func TestSum(t *testing.T){ ... }
-
*方法参加必须 t testing.T
-
使用go test执行单元测试
testing.T的拥有的方法如下:
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
go test参数解读
go test是go语言自带的测试工具,其中包含的是两类,单元测试和性能测试
通过go help test可以看到go test的使用说明:
格式形如:
go test [-c] [-i] [build/test flags] [packages] [build/test flags & test binary flags]
-c : 编译go test成为可执行的二进制文件,但是不运行测试。
-i : 安装测试包依赖的package,但是不运行测试。
-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
-test.run pattern: 只跑哪些单元测试用例。
示例
我们定义一个split的包,包中定义了一个Split函数,具体实现如下:
// 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) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+1:]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:
// split/split_test.go
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
}
}
在split包路径下,执行go test命令,可以看到输出结果如下:
split $ go test
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
基准测试
基准测试可以测试一段程序的运行性能及耗费 CPU 的程度。Go 语言中提供了基准测试框架,使用方法类似于单元测试,使用者无须准备高精度的计时器和各种分析工具,基准测试本身即可以打印出非常标准的测试报告。
压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,但需要注意以下几点:
-
压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母
func BenchmarkXXX(b *testing.B) { ... }
-
go test不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench=“test_name_regex”,例如go test -test.bench=".*"表示测试全部的压力测试函数
-
在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行
-
文件名也必须以_test.go结尾
基准测试以Benchmark为前缀,需要一个\*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。
testing.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()
示例
我们为split包中的Split函数编写基准测试如下:
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
Split("沙河有沙又有河", "沙")
}
}
基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:
split $ go test -bench=Split
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8 10000000 203 ns/op
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.255s
其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000和203ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。
我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。
split $ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.394s
其中,112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。
我们将我们的Split函数优化如下:
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):] // 这里使用len(sep)获取sep的长度
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:
split $ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8 10000000 127 ns/op 48 B/op 1 allocs/op
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 1.423s
性能测试
Go 性能测试的函数基本格式:
func BenchmarkXxx(*testing.B)
Benchmark测试通过 go test 命令来启动,通过 -bench 这样一个标签来标明是Benchmark测试,启动后会按照顺序执行。
示例:
创建一个benchmark.go
func Hello() {
fmt.Sprintf("hello")
}
按照规范,我们需要一个benchmark_test.go的文件来测试。
benchmark_test.go
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
Hello()
}
}
这里的b.N是必须运行的次数,在benchmark运行期间,这个数值会调整,直到可以稳定地评估出性能。
$ go test -bench BenchmarkHello
goos: darwin
goarch: amd64
pkg: demo/benchmark
BenchmarkHello-4 20000000 59.4 ns/op
PASS
ok demo/benchmark 1.264s
判定失败接口
- Fail 失败继续
- FailNow 失败终止
打印信息接口
-
Log 数据流 (cout 类似)
-
Logf format (printf 类似)
-
SkipNow 跳过当前测试
-
Skiped 检测是否跳过
综合接口产生: -
Error / Errorf 报告出错继续 [ Log / Logf + Fail ]
-
Fatel / Fatelf 报告出错终止 [ Log / Logf + FailNow ]
-
Skip / Skipf 报告并跳过 [ Log / Logf + SkipNow ]
其他参数:
-test.bench patten: 只跑那些性能测试用例
-test.benchmem : 是否在性能测试的时候输出内存情况
-test.benchtime t : 性能测试运行的时间,默认是1s
-test.cpuprofile cpu.out : 是否输出cpu性能分析文件
-test.memprofile mem.out : 是否输出内存性能分析文件
-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。
-test.timeout t : 如果测试用例运行时间超过t,则抛出panic
-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表。
-test.short : 将那些运行时间较长的测试用例运行时间缩短