目录

Go语言测试

这是Go单测从入门到放弃系列教程的第0篇,主要讲解在Go语言中如何做单元测试以及介绍了表格驱动测试、回归测试,并且介绍了常用的断言工具。

go test工具

go test
_test.gogo testgo build
*_test.go
类型格式作用
测试函数函数名前缀为Test测试程序的一些逻辑行为是否正确
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example为文档提供示例文档
go test*_test.go

单元测试函数

格式

testing
func TestName(t *testing.T){
    // ...
}
Test
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
ttesting.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

单元测试示例

就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。

base_demoSplit
// base_demo/split.go
package base_demo
import "strings"
// Split 把字符串s按照给定的分隔符sep进行分割返回字符串切片
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("expected:%v, got:%v", want, got) // 测试失败输出错误提示
 }
}
split
❯ ls -l
total 16
-rw-r--r--  1 liwenzhou  staff  408  4 29 15:50 split.go
-rw-r--r--  1 liwenzhou  staff  466  4 29 16:04 split_test.go
go test

❯ go test
PASS
ok      golang-unit-test-demo/base_demo       0.005s

go test -v

split_test.go
func TestSplitWithComplexSep(t *testing.T) {
 got := Split("abcd", "bc")
 want := []string{"a", "d"}
 if !reflect.DeepEqual(want, got) {
  t.Errorf("expected:%v, got:%v", want, got)
 }
}
go test-v

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSep
    split_test.go:20: expected:[a d], got:[a cd]
--- FAIL: TestSplitWithComplexSep (0.00s)
FAIL
exit status 1
FAIL    golang-unit-test-demo/base_demo 0.009s

TestSplitWithComplexSep

go test -run

split
package base_demo
import "strings"
// Split 把字符串s按照给定的分隔符sep进行分割返回字符串切片
func Split(s, sep string) (result []string) {
 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
}
go test-rungo test
go test-run=SepTestSplitWithComplexSep
❯ go test -run=Sep -v
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.010s

最终的测试结果表情我们成功修复了之前的Bug。

回归测试

我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,正确的做法应该是完整运行所有的测试用例,保证不会因为修改代码而引入新的问题。

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.011s

测试结果表明我们的单元测试全部通过。

通过这个示例我们可以看到,有了单元测试就能够在代码改动后快速进行回归测试,极大地提高开发效率并保证代码的质量。

跳过某些测试用例

为了节省时间支持在单元测试时跳过某些耗时的测试用例。

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("short模式下会跳过该测试用例")
    }
    ...
}
go test -shortTestTimeConsuming

子测试

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){...})
}

表格驱动测试

介绍

编写好的测试并非易事,但在许多情况下,表格驱动测试可以涵盖很多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。

使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。

t.Run

表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。

示例

官方标准库中有很多表格驱动测试的示例,例如fmt包中的测试代码:

var flagtests = []struct {
 in  string
 out string
}{
 {"%a", "[%a]"},
 {"%-a", "[%-a]"},
 {"%+a", "[%+a]"},
 {"%#a", "[%#a]"},
 {"% a", "[% a]"},
 {"%0a", "[%0a]"},
 {"%1.2a", "[%1.2a]"},
 {"%-1.2a", "[%-1.2a]"},
 {"%+1.2a", "[%+1.2a]"},
 {"%-+1.2a", "[%+-1.2a]"},
 {"%-+1.2abc", "[%+-1.2a]bc"},
 {"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
 var flagprinter flagPrinter
 for _, tt := range flagtests {
  t.Run(tt.in, func(t *testing.T) {
   s := Sprintf(tt.in, &flagprinter)
   if s != tt.out {
    t.Errorf("got %q, want %q", s, tt.out)
   }
  })
 }
}

通常表格是匿名结构体数组切片,可以定义结构体或使用已经存在的结构进行结构体数组声明。name属性用来描述特定的测试用例。

接下来让我们试着自己编写表格驱动测试:

func TestSplitAll(t *testing.T) {
 // 定义测试表格
 // 这里使用匿名结构体定义了若干个测试用例
 // 并且为每个测试用例设置了一个名称
 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 {
  t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
   got := Split(tt.input, tt.sep)
   if !reflect.DeepEqual(got, tt.want) {
    t.Errorf("expected:%#v, got:%#v", tt.want, got)
   }
  })
 }
}
go test -v

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
=== RUN   TestSplitAll
=== RUN   TestSplitAll/base_case
=== RUN   TestSplitAll/wrong_sep
=== RUN   TestSplitAll/more_sep
=== RUN   TestSplitAll/leading_sep
--- PASS: TestSplitAll (0.00s)
    --- PASS: TestSplitAll/base_case (0.00s)
    --- PASS: TestSplitAll/wrong_sep (0.00s)
    --- PASS: TestSplitAll/more_sep (0.00s)
    --- PASS: TestSplitAll/leading_sep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.010s

并行测试

表格驱动测试中通常会定义比较多的测试case,在Go语言中很容易发挥自身并发优势将表格驱动测试并行化,可以查看下面的代码示例。

func TestSplitAll(t *testing.T) {
 t.Parallel()  // 将 TLog 标记为能够与其他测试并行运行
 // 定义测试表格
 // 这里使用匿名结构体定义了若干个测试用例
 // 并且为每个测试用例设置了一个名称
 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)
   }
  })
 }
}

使用工具生成测试代码

gotests

安装

go get -u github.com/cweill/gotests/...

执行

gotests -all -w split.go
split.gosplit_test.go

生成的测试代码大致如下:

package base_demo
import (
 "reflect"
 "testing"
)
func TestSplit(t *testing.T) {
 type args struct {
  s   string
  sep string
 }
 tests := []struct {
  name       string
  args       args
  wantResult []string
 }{
  // TODO: Add test cases.
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   if gotResult := Split(tt.args.s, tt.args.sep); !reflect.DeepEqual(gotResult, tt.wantResult) {
    t.Errorf("Split() = %v, want %v", gotResult, tt.wantResult)
   }
  })
 }
}

代码格式与我们上面的类似,只需要在TODO位置添加我们的测试逻辑就可以了。

测试覆盖率

测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。

go test -cover
❯ go test -cover
PASS
coverage: 100.0% of statements
ok      golang-unit-test-demo/base_demo 0.009s

从上面的结果可以看到我们的测试用例覆盖了100%的代码。

-coverprofile
❯ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok      golang-unit-test-demo/base_demo 0.009s
c.out
❯ tree .
.
├── c.out
├── split.go
└── split_test.go
go tool cover -html=c.outcover

上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

testify/assert

testify/asserttestify/require

安装

go get github.com/stretchr/testify

使用示例

if...else...testify/assert
TestSplitreflect.DeepEqual
t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
 got := Split(tt.input, tt.sep)
 if !reflect.DeepEqual(got, tt.want) {
  t.Errorf("expected:%#v, got:%#v", tt.want, got)
 }
})
testify/assert
t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
 got := Split(tt.input, tt.sep)
 assert.Equal(t, got, tt.want)  // 使用assert提供的断言函数
})
Testing.T
func TestSomething(t *testing.T) {
  assert := assert.New(t)
  // assert equality
  assert.Equal(123, 123, "they should be equal")
  // assert inequality
  assert.NotEqual(123, 456, "they should not be equal")
  // assert for nil (good for errors)
  assert.Nil(object)
  // assert for not nil (good when you expect something)
  if assert.NotNil(object) {
    // now we know that object isn't nil, we are safe to make
    // further assertions without causing any errors
    assert.Equal("Something", object.Value)
  }
}
testify/assert
testify/requiretestify/asserttestify/require
testify

总结

您可能感兴趣的文章: