目录
- go test工具
- 单元测试
- 测试代码
- go test -run
- 跳过某些测试用例
- 子测试
- 表格驱动测试
- 并行测试
- 报告方法
- 测试覆盖率
- 基准测试
- demo
- 性能比较函数
- 计时方法
- 并行测试
- TestMain
- Setup与Teardown
- httptest
- 简单的 Web 应用
- 测试
Go 语言从开发初期就注意了测试用例的编写。特别是静态语言,由于调试没有动态语言那么方便,所以能最快最方便地编写一个测试用例就显得非常重要了。
- testing 方便进行 Go 包的自动化单元测试、基准测试
- net/http/httptest 提供测试 HTTP 的工具
go test工具
Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
单元测试
格式:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。
func TestXxxx(t *testing.T){
// ...
}
t参数拥有以下方法:
func (c *T) Cleanup(func())
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) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() string
gotest 的变量有这些:
test.short : 一个快速测试的标记,在测试用例中可以使用 testing.Short() 来绕开一些测试
test.outputdir : 输出目录
test.coverprofile : 测试覆盖率参数,指定输出文件
test.run : 指定正则来运行某个 / 某些测试用例
test.memprofile : 内存分析参数,指定输出文件
test.memprofilerate : 内存分析参数,内存分析的抽样率
test.cpuprofile : cpu 分析输出参数,为空则不做 cpu 分析
test.blockprofile : 阻塞事件的分析参数,指定输出文件
test.blockprofilerate : 阻塞事件的分析参数,指定抽样频率
test.timeout : 超时时间
test.cpu : 指定 cpu 数量
test.parallel : 指定运行测试用例的并行数
测试代码
// main.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
// fib_test.go
func TestFib(t *testing.T) {
var (
in = 7
expected = 13
)
actual := Fib(in)
if actual != expected {
t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
}
}
执行 go test .
,(可以为go test命令添加-v参数,让它输出完整的测试结果。)输出:
go test -run
-
在执行go test命令的时候可以添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。
-
例如通过给go test添加-run=Sep参数来告诉它本次测试只运行TestFuckingSep这个测试用例。
跳过某些测试用例
为了节省时间支持在单元测试时跳过某些耗时的测试用例。
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("short模式下会跳过该测试用例")
}
...
}
当执行go test -short时就不会执行上面的TestTimeConsuming测试用例。
子测试
Go1.7+中新增了子测试,支持在测试函数中使用t.Run执行一组测试用例,这样就不需要为不同的测试数据定义多个测试函数了。
func TestXXX(t *testing.T){
t.Run("case1", func(t *testing.T){...})
t.Run("case2", func(t *testing.T){...})
t.Run("case3", func(t *testing.T){...})
}
// 循环实现
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"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{"河有", "又有河"}},
}
for name, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("name:%s expected:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
}
}
}
// 用自测试方法实现
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"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{"河有", "又有河"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
表格驱动测试
测试讲究 case 覆盖,按上面的方式,要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。
表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。
func TestFib(t *testing.T) {
var fibTests = []struct {
in int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}
for _, tt := range fibTests {
actual := Fib(tt.in)
if actual != tt.expected {
t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
}
}
}
并行测试
表格驱动测试中通常会定义比较多的测试用例,而Go语言又天生支持并发,所以很容易发挥自身并发优势将表格驱动测试并行化。 想要在单元测试过程中使用并行测试,可以像下面的代码示例中那样通过添加t.Parallel()来实现。
func TestSplitAll(t *testing.T) {
// 将 TLog 标记为能够与其他测试并行运行
t.Parallel()
// 定义测试表格
// 为每个测试用例设置了一个名称
tests := []struct {
name string
input string
sep string
want []string
}{
{"base case", "a:b:c", ":", []string{"a", "b", "c"}},
{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
{"more sep", "abcd", "bc", []string{"a", "d"}},
{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
}
// 遍历测试用例
for _, tt := range tests {
tt := tt // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)
t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
t.Parallel() // 将每个测试用例标记为能够彼此并行运行
got := Split(tt.input, tt.sep)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("expected:%#v, got:%#v", tt.want, got)
}
})
}
}
报告方法
-
遇到一个断言错误的时候,标识这个测试失败:
Fail() : 测试失败,测试继续 FailNow() : 测试失败,测试中断
-
遇到一个断言错误,只希望跳过这个错误,但是不希望标识测试失败:
SkipNow() : 跳过测试,测试中断
-
只希望打印信息
Log : 输出信息 Logf : 输出格式化的信息
-
希望跳过这个测试,并且打印出信息
Skip : 相当于 Log + SkipNow Skipf : 相当于 Logf + SkipNow
-
希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试继续
Error : 相当于 Log + Fail Errorf : 相当于 Logf + Fail
-
希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试
Fatal : 相当于 Log + FailNow Fatalf : 相当于 Logf + FailNow
测试覆盖率
测试覆盖率是指代码被测试套件覆盖的百分比,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。
使用go test -cover来查看测试覆盖率。
❯ go test -cover
PASS
coverage: 100.0% of statements
ok golang-unit-test-demo/base_demo 0.009s
Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件:
❯ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok golang-unit-test-demo/base_demo 0.009s
然后执行go tool cover -html=c.out
,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。
基准测试
基准测试就是在一定的工作负载之下检测程序性能的一种方法。
func BenchmarkName(b *testing.B){
// ...
}
基准测试以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()
demo
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
通过 go test 命令,加上 -bench 标志来执行。
结果输出:
- 意味着函数执行了 30542277 次,每次循环花费 33.73 纳秒 (ns)。
- 数字16表示GOMAXPROCS的值,这个对于并发基准测试很重要.
还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。
5 B/op表示每次操作内存分配了5字节,1 allocs/op则表示每次操作进行了1次内存分配
性能比较函数
通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
func benchmarkFib(b *testing.B, n int) {
for i := 0; i < b.N; i++ {
Fib(n)
}
}
func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。
可以使用-benchtime标志增加最小基准时间,以产生更准确的结果:
go test -bench=Fib40 -benchtime=20s
计时方法
有三个方法用于计时:
- StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,也可以在调用 StopTimer 之后恢复计时;
- StopTimer:停止对测试进行计时。当需要执行一些复杂的初始化操作,并且不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;
3/ ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。
func BenchmarkSplit(b *testing.B) {
time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
b.ResetTimer() // 重置计时器
for i := 0; i < b.N; i++ {
Split("xx.xx.xx.xx", ".")
}
}
并行测试
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
通过 RunParallel 方法能够并行地执行给定的基准测试。RunParallel会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用 SetParallelism(如 SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。
body 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到 pb.Next 返回 false 值为止。因为 StartTimer、StopTime 和 ResetTimer 这三个方法都带有全局作用,所以 body 函数不应该调用这些方法; 除此之外,body 函数也不应该调用 Run 方法。
func BenchmarkSplitParallel(b *testing.B) {
// b.SetParallelism(1) // 设置使用的CPU数
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Split("xx.xx.xx.xx", ".")
}
})
}
还可以通过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的CPU数量。
TestMain
通过在*_test.go文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试。TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit。
func TestMain(m *testing.M) {
fmt.Println("write setup code here...") // 测试之前的做一些设置
// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
retCode := m.Run() // 执行测试
fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作
os.Exit(retCode) // 退出测试
}
Setup与Teardown
有时候可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。
// 测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:测试之前的setup")
// 返回Teardown
return func(t *testing.T) {
t.Log("如有需要在此执行:测试之后的teardown")
}
}
// 子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:子测试之前的setup")
// 返回Teardown
return func(t *testing.T) {
t.Log("如有需要在此执行:子测试之后的teardown")
}
}
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"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) { // 使用t.Run()执行子测试
teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
defer teardownSubTest(t) // 测试之后执行testdoen操作
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
httptest
由于 Go 标准库的强大支持,Go 可以很容易的进行 Web 开发。为此,Go 标准库专门提供了 net/http/httptest 包专门用于进行 http Web 开发测试。
简单的 Web 应用
// 保存 Topic,没有考虑并发问题
var TopicCache = make([]*Topic, 0, 16)
type Topic struct {
Id int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
http.HandleFunc("/topic/", handleRequest)
http.ListenAndServe(":2017", nil)
}
/topic/ 开头的请求都交由 handleRequest 处理,它根据不同的 Method 执行相应的增删改查
...
测试
func TestHandlePost(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/topic/", handleRequest)
reader := strings.NewReader(`{"title":"The Go Standard Library","content":"It contains many packages."}`)
r, _ := http.NewRequest(http.MethodPost, "/topic/", reader)
// httptest.NewRecorder() 可以获得 httptest.ResponseRecorder 结构,而此结构实现了http.ResponseWriter 接口。
w := httptest.NewRecorder()
mux.ServeHTTP(w, r)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("Response code is %v", resp.StatusCode)
}
}
通过 go test -v 运行测试。