最下面是单元测试,单元测试对代码进行测试。再而上是集成测试,它对一个服务的接口进行测试。继而是端到端的测试,我也称他为链路测试,它负责从一个链路的入口输入测试用例,验证输出的系统的结果。再上一层是我们最常用的UI测试,就是测试人员在UI界面上根据功能进行点击测试。
单元测试
所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中,xxx_test.go
测试文件代码和被测试的源代码文件xxx.go
放在同一个包内,一一对应。
比如项目下有某个包内有个源代码文件xxx.go
有这么一个函数:
Copy func lengthOfNonRepeatingSubStr(s string) int {
lastOccurred := make(map[rune]int)
start := 0
maxLength := 0
for i, ch := range []rune(s) {
if lastI, ok := lastOccurred[ch]; ok && lastI >= start {
start = lastI + 1
}
if i-start+1 > maxLength {
maxLength = i - start + 1
}
lastOccurred[ch] = i
}
return maxLength
}
那么单元测试的文件xxx_test.go
就放在相同的包内,测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头,需要一个*testing.T
类型的参数,可以像下面这样按组测试,又称表格测试法:
Copy func TestSubstr(t *testing.T) {
tests := []struct {
s string
ans int
}{
// Normal cases
{"abcabcbb", 3},
{"pwwkew", 3},
// Edge cases
{"", 0},
{"b", 1},
{"bbbbbbbbb", 1},
{"abcabcabcd", 4},
// Chinese support
{"一二三二一", 3},
{"黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花", 8},
}
for _, tt := range tests {
actual := lengthOfNonRepeatingSubStr(tt.s)
if actual != tt.ans {
t.Errorf("got %d for input %s; "+
"expected %d",
actual, tt.s, tt.ans)
}
}
}
执行测试程序
在项目根目录下直接执行go test .
含义是:仅从当前目录下查找以_test.go
为后缀的以Test
开头的单元测试函数。
go test ./...
含义是:找出当前的目录以及所有子目录(包括多级子目录)下所有的 xxx_test.go 测试代码。
go test ./xxx/xxx
含义是:找出./xxx/xxx
路径下的 xxx_test.go 测试代码,如果想执行指定目录以及其子目录的单元测试,需要指定目录为:./xxx/xxx/...
,即在路径后加上/...
,注意是 3 个.
。
默认不执行基准测试,加上-bench
参数来执行,如:go test -bench=.
。
测试项目某个模块下的指定单元测试和指定的基准测试,如:go test -v service/*.go -bench BenchMethodName -benchmem -run TestMethodName
MethodName
可以写正则表达式匹配,比如如果只想执行benchmark
,可以把-run
参数写成none
或者^$
,不存在正则匹配的单元测试也就只会执行基准测试了。
基准测试
基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试函数以Benchmark
为前缀,需要一个*testing.B
类型的参数。
Copy func BenchmarkSubstr(b *testing.B) {
s := "黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花"
for i := 0; i < 13; i++ {
s = s + s
}
b.Logf("len(s) = %d", len(s))
ans := 8
b.ResetTimer() // 重置时间起始计算点,上面参数的初始化不计入
for i := 0; i < b.N; i++ { // b.N 的值是系统根据实际情况去调整的
actual := lengthOfNonRepeatingSubStr(s)
if actual != ans {
b.Errorf("got %d for input %s; "+
"expected %d",
actual, s, ans)
}
}
}
可以为基准测试添加-benchmem
参数,来获得内存分配的统计数据:
Copy zhhnzw$ go test -v -bench=Substr -benchmem
=== RUN TestSubstr
--- PASS: TestSubstr (0.00s)
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-7920HQ CPU @ 3.10GHz
BenchmarkSubstr
nonrepeating_test.go:41: len(s) = 491520
nonrepeating_test.go:41: len(s) = 491520
nonrepeating_test.go:41: len(s) = 491520
BenchmarkSubstr-8 231 5152279 ns/op 655590 B/op 2 allocs/op
PASS
BenchmarkSubstr-8
中数字8
表示GOMAXPROCS
的值,即并发的线程数(并发的协程比该值大)
数字231
的含义:是本次Benchmark
循环体共执行的次数
5152279 ns/op:
本次Benchmark
循环体每循环一次耗费5152279
纳秒
655590 B/op:
本次Benchmark
循环体每循环一次分配了655590
字节
2 allocs/op
:本次Benchmark
循环体每循环一次进行了2次内存分配
-benchtime=3s
为基准测试限定执行时间。
benchstat
Copy $ go test -v -run=^$ -bench=Substr$ -benchmem -count=10 | tee before.txt # -count=10表示执行10次
$ go test -v -run=^$ -bench=Substr$ -benchmem -count=10 | tee after.txt
$ benchstat before.txt after.txt
如果没有benchstat
就先安装:go get golang.org/x/perf/cmd/benchstat
集成测试
suite
和上面的assert
在同一个包下:github.com/stretchr/testify
假设有如下2个函数需要编写单元测试:
Copy func foo() {
fmt.Println("foo...")
}
func goo() {
fmt.Println("goo...")
}
单元测试文件代码如下:
Copy import (
"fmt"
"github.com/stretchr/testify/suite"
"testing"
)
type _Suite struct {
suite.Suite
}
func (s *_Suite) SetupSuite() {
fmt.Println("SetupSuite()...")
}
func (s *_Suite) SetupTest() {
fmt.Println("SetupTest()...")
}
func (s *_Suite) BeforeTest(suiteName, testName string) {
fmt.Printf("BeforeTest: suiteName=%s, testName=%s\n", suiteName, testName)
}
func (s *_Suite) TestFoo() {
foo()
}
func (s *_Suite) TestGoo() {
goo()
}
func (s *_Suite) AfterTest(suiteName, testName string) {
fmt.Printf("AfterTest: suiteName=%s, testName=%s\n", suiteName, testName)
}
func (s *_Suite) TearDownTest() {
fmt.Println("TearDownTest()...")
}
func (s *_Suite) TearDownSuite() {
fmt.Println("TearDownSuite()...")
}
// 让 go test 执行测试
func TestGooFoo(t *testing.T) {
suite.Run(t, new(_Suite))
}
Copy $ go test -v main.go main_test.go
=== RUN TestGooFoo
SetupSuite()...
=== RUN TestGooFoo/TestFoo
SetupTest()...
BeforeTest: suiteName=_Suite, testName=TestFoo
foo...
AfterTest: suiteName=_Suite, testName=TestFoo
TearDownTest()...
=== RUN TestGooFoo/TestGoo
SetupTest()...
BeforeTest: suiteName=_Suite, testName=TestGoo
goo...
AfterTest: suiteName=_Suite, testName=TestGoo
TearDownTest()...
TearDownSuite()...
--- PASS: TestGooFoo (0.00s)
--- PASS: TestGooFoo/TestFoo (0.00s)
--- PASS: TestGooFoo/TestGoo (0.00s)
PASS
ok command-line-arguments 3.063s
SetupSuite()、TearDownSuite() 仅执行一次,典型使用场景是各个单元测试共用的数据库连接
而 SetupTest()、BeforeTest()、AfterTest()、TearDownTest()对套件中的每个测试都会执行一次
测试覆盖率
取得coverprofile
:go test -v -coverprofile=coverage.out
查看:go tool cover -html=coverage.out
编译到服务器上执行单元测试
编译: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go test -c *.go -o testxxx
连上服务器后执行单元测试: ./testxxx -test.run TestXxx_Xxx
gomock
gomock
是对interface
方法功能的模拟。
通常有的功能的单元测试不好写,比如依赖了第三方服务提供的接口,但它还没实现好,此时单元测试的编写就需要mock
第三方服务接口的调用。
通过mock
还可以模拟异常场景。
安装
go install github.com/golang/mock/mockgen@v1.6.0
使用
新建person/male.go
,写入如下内容:
Copy package person
type Male interface {
Get(id int64) error
}
新建user/user.go
,写入以下内容:
Copy package user
import "github.com/zhhnzw/go_demos/mock_test/person"
type User struct {
Person person.Male
}
func NewUser(p person.Male) *User {
return &User{Person: p}
}
func (u *User) GetUserInfo(id int64) error {
return u.Person.Get(id)
}
生成mock
代码: mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock
新建user/user_test.go
,写入如下内容:
Copy package user
import (
"errors"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/zhhnzw/go_demos/mock_test/mock"
"testing"
"time"
)
func TestUser_GetUserInfo(t *testing.T) {
ctl := gomock.NewController(t)
var id int64 = 1
mockMale := mock.NewMockMale(ctl) // 实现了male.go中的interface的实例
mockMale.EXPECT().Get(id).Return(nil) // 模拟正常情况,给Get方法传了id=1,return nil。那么后面GetUserInfo对于Get的调用就是这个mock的调用情况。
user := NewUser(mockMale)
err := user.GetUserInfo(id)
assert.Nil(t, err)
id = 999
mockMale = mock.NewMockMale(ctl)
mockMale.EXPECT().Get(id).DoAndReturn(func(id int64) error {
time.Sleep(3 * time.Second)
return errors.New("timeout")
}) // 用一个匿名函数模拟Get方法的执行,模拟经过3s时间返回了timeout的err
user = NewUser(mockMale)
err = user.GetUserInfo(id)
assert.Equal(t, "timeout", err.Error())
}
文件目录:
Copy ├── mock
│ └── male_mock.go
├── person
│ └── male.go
└── user
├── user.go
└── user_test.go
gomonkey
上一节介绍的gomock
是需要在设计了interface
的前提下的mock
,但如果没有设计interface
,要怎么mock
呢?此时就需要用到gomonkey
了。
gomonkey
的实现原理是通过reflect
包实现,在运行时替换指定变量、指定函数。
注:gomonkey
不是线程安全的,所以不要把它用到并发的单元测试中。
安装
go get github.com/agiledragon/gomonkey/v2@v2.9.0
在Mac
下直接使用gomonkey
会有如下这样的错误抛出:
Copy panic: permission denied [recovered]
panic: permission denied
Copy cd `go env GOPATH`
git clone https://github.com/eisenxp/macos-golink-wrapper.git
mv `go env GOTOOLDIR`/link `go env GOTOOLDIR`/original_link
cp `go env GOPATH`/macos-golink-wrapper/link `go env GOTOOLDIR`/link
chmod +x `go env GOTOOLDIR`/link
注意:使用gomonkey
在执行单元测试时需要禁用内联go test -gcflags=all=-l
,否则mock
不会成功。
典型用法
Copy package mock_test
import (
"fmt"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
"testing"
)
var num = 10
func networkCompute(a, b int) (int, error) {
c := a + b
return c, nil
}
func Compute(a, b int) (int, error) {
sum, err := networkCompute(a, b)
return sum, err
}
type Person struct {
name string
}
func (p *Person) eat() {
fmt.Sprintf("%s is eating", p.name)
}
func TestFunc(t *testing.T) {
patches := gomonkey.ApplyGlobalVar(&num, 150)
assert.Equal(t, num, 150)
patches = gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
return 2, nil
})
defer patches.Reset()
sum, err := Compute(1, 2)
assert.Nil(t, err)
assert.Equal(t, 2, sum)
}
// 把gomonkey.ApplyMethod的例子写上