img

单测、表驱动测试、Fuzz、Mock、覆盖率… 还有那些测试里用得上的第三方库

简单的单元测试

hello.go中放了我们待测试的函数:

package hello

func Pow(base, exp int) int {
    if exp == 0 {
       return 1
    }
    return base * Pow(base, exp-1)
}

现在来给它写点单元测试:

  • 单测文件的后缀为_test.go,在我们的例子中是hello_test.go
  • 测试函数以Test开头,参数为t *testing.T
  • testing.T中提供了一些辅助函数,比如t.Errorf()t.Fail()t.Skip()t.Parallel()t.TempDir()
  • 如果你想在测试运行前后进行一些初始化之类的操作,可以看看testing.M
package hello

import "testing"

func TestPow(t *testing.T) {
    result := Pow(10, 2)
    if result != 100 {
       t.Errorf("Result was incorrect, got: %d, want: %d.", result, 100)
    }
}

一个极其简单的单元测试就这样完成了!我们可以通过以下命令运行它:

go test # 运行当前目录下的测试
go test ./... # 运行目录下所有包的测试
go test -v # 显示运行细节
go test -count n # 重复运行n次
go test -run REGEX # 只运行名称与REGEX正则匹配的测试

表驱动测试

如果有多个测试用例,我们可以将输入和预期结果提取出来,避免重复的代码,这就是表驱动测试:

func TestPow(t *testing.T) {
    var tests = []struct {
       base int
       exp  int
       want int
    }{
       {2, 0, 1},
       {2, 1, 2},
       {2, 2, 4},
    }
    for _, test := range tests {
       if got := Pow(test.base, test.exp); got != test.want {
          t.Errorf("Pow(%d, %d) = %d, want %d", test.base, test.exp, got, test.want)
       }
    }
}

覆盖率

了解覆盖率的方法非常简单,只需要运行测试时添加-cover就可以了!

go test -cover # 测试运行结束后将会输出覆盖率
go test ./... -coverpkg ./... # 指定coverpkg统计所有包的覆盖率
go test -coverprofile filename # 将覆盖率结果输出到filename中
go tool cover -html filename # 将覆盖率结果转换为html文件以便查看

基准测试

基准测试可以让你的代码运行b.N次并测量运行时间,写起来需要注意以下几点:

  • 也要在后缀为_test.go的文件中
  • 函数名以Benchmark开头,参数为(b *testing.B)
  • 有一个执行b.N次的循环
func BenchmarkHello(b *testing.B) {
   for i := 0; i < b.N; i++ {
      s := "world"
      expected:="hello,world"
      got := Hello(s)
      if  got != expected {
         b.Errorf("Expceted %s but got %s",expected,got)
      }
   }
}

运行go test -bench .即可得到结果!

模糊测试

模糊测试可以使用随机输入来测试代码,写起来也要注意以下几点:

  • 也要在后缀为_test.go的文件中
  • 函数名以Fuzz开头,参数为(f *testing.F)
  • 需要定义模糊目标
func FuzzPow(f *testing.F) {
    f.Fuzz(func(t *testing.T, base, exp int) {
       if exp < 0 {
          t.Skip()
       }
       Pow(base, exp)
    })
}

之后我们可以使用go test -fuzz .运行它,可以添加参数-fuzztime 20s指定运行时间,不指定的话它会一直运行下去直到出现错误

Mock

实际场景中,有些操作如数据库等有较多的依赖,难以直接创建。编写单元测试时,可以使用那些专门用于单测的包(如 redis 的 miniredis),没有的则可以考虑用 Mock 模拟依赖项行为。下面是一个简单的例子:

// hello.go
package hello

import (
    "fmt"
)

type MessageService interface {
    SendNotification(string) 
}

type SMSService struct{}

func (s SMSService) SendNotification(text string)  {
    fmt.Println("Sending Notification via SMS")
    // ...
}

type MyService struct {
    msg MessageService
}

func (s MyService) Hello() error {
    s.msg.SendNotification("hello")
    return nil
}
// hello_test.go
package hello

import "testing"

type MessageServiceMock struct {
}

func (m MessageServiceMock) SendNotification(text string) {
    println("Mocked")
}

func TestMyService_Hello(t *testing.T) {
    service := MyService{MessageServiceMock{}}
    err := service.Hello()
    if err != nil {
       t.Error("Error should be nil")
    }
}

实际编写时可以使用第三方库如 gomock 等辅助生成这部分代码

测试相关的库

参考

Comprehensive Guide to Testing in Go | The GoLand Blog (jetbrains.com)

Understanding Fuzz Testing in Go | The GoLand Blog (jetbrains.com)