单测、表驱动测试、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 等辅助生成这部分代码
测试相关的库
- stretchr/testify,提供
assert
等常用的辅助函数 - uber-go/mock,mock 框架,可以生成相关代码,并方便地验证调用时的参数、设置返回值
- vektra/mockery,另一个 mock 框架
- bradleyjkemp/cupaloy,一个快照式测试的库
- testcontainers/testcontainers-go,在测试时轻松创建基于容器的依赖项
参考
Comprehensive Guide to Testing in Go | The GoLand Blog (jetbrains.com)
Understanding Fuzz Testing in Go | The GoLand Blog (jetbrains.com)