一、单元测试是什么

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

二、单元测试的意义

1.提高代码质量:编写测试用例会迫使开发人员仔细思考代码的设计和必须完成的工作,进而会改善代码质量。

2.简化调试过程:开发人员可以测试单个模块的功能,不依赖外部工程和数据源。

3.保证重构正确:单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。

4.尽早发现问题:单元测试由功能对应的开发者完成,测试考虑的范围更加全面,可以尽早发现大部分问题。

三、Golang单元测试框架

3.1 Golang内置testing包

3.1.1 简单的测试

简单测试用例定义如下:

func TestXXXX(t *testing.T) {
// ...
}
  • Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。
  • 函数名必须以 Test 开头,后面一般跟待测试的函数名
  • 参数为 t *testing.T

3.1.2 Benchmark 基准测试

基准测试用例的定义如下:

func BenchmarkName(b *testing.B){
// ...
}
  • 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
  • 参数为 b *testing.B。

3.1.3 运行测试用例

# 匹配当前目录下*_test.go命令的文件,执行每一个测试函数
go test -v # 执行 calc_test.go 文件中的所有测试函数
go test -v calc_test.go calc.go # 指定特定的测试函数(其中:-count=1用于跳过缓存)
go test -v -run TestAdd calc_test.go calc.go -count=1 # 执行基准测试
go test -benchmem -bench .

3.1.4 简单的测试示例

当前 package 有 calc.go 一个文件,我们想测试 calc.go 中的 Add 和 Substract 函数,那么应该新建 calc_test.go 作为测试文件。

testing/
| -- calc.go
| -- calc_test.go
calc.go
package testing

func Add(a, b int) int {
return a + b
} func Substract(a, b int) int {
return a - b
}
calc_test.go
package testing

import (
"testing"
) func TestAdd(t *testing.T) {
a := 2
b := 3
ret := Add(a, b)
if ret != a+b {
t.Fatalf("Expect:%d Actual:%d", a+b, ret)
}
} func TestSubtests(t *testing.T) {
// 嵌套
t.Run("TestSubstract", func(t *testing.T) {
a := 3
b := 2
ret := Substract(a, b)
if ret != a-b {
t.Fatalf("Expect:%d Actual:%d", a-b, ret)
}
})
}

3.2 GoConvey测试框架

GoConvey 是个相当不错的 Go 测试工具,支持 go test。可直接在终端窗口和浏览器上使用。

3.2.1.安装依赖:

go get github.com/smartystreets/goconvey

3.2.2.测试用例

package/
| --calc.go
| --calc_test.go
calc.go
package goconvey

import "errors"

func Add(a, b int) int {
return a + b
} func Subtract(a, b int) int {
return a - b
} func Multiply(a, b int) int {
return a * b
} func Division(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("被除数不能为 0")
}
return a / b, nil
}
calc_test.go
package goconvey

import (
"testing" . "github.com/smartystreets/goconvey/convey"
) func Test_Add(t *testing.T) {
Convey("将两数相加", t, func() {
So(Add(1, 2), ShouldEqual, 3)
})
} func Test_Substract(t *testing.T) {
// 忽略断言
SkipConvey("将两数相减", t, func() {
So(Subtract(2, 1), ShouldEqual, 1)
})
} func Test_Multiply(t *testing.T) {
Convey("将两数相乘", t, func() {
So(Multiply(3, 2), ShouldEqual, 6)
})
} func Test_Division(t *testing.T) {
Convey("将两数相除", t, func() {
// 嵌套
Convey("除以非 0 数", func() {
num, err := Division(10, 2)
So(err, ShouldBeNil)
So(num, ShouldEqual, 5)
// 忽略断言
SkipSo(num, ShouldNotBeNil)
}) Convey("除以 0", func() {
_, err := Division(10, 0)
So(err, ShouldNotBeNil)
})
})
}

3.2.3. 运行测试

1.终端窗口使用:go test -v

2.Web浏览器使用:在相应的目录执行 goconvey,然后访问http://localhost:8080

3.3 testify测试框架

3.3.1. 安装依赖

go get github.com/stretchr/testify

3.3.2 测试用例

import (
"testing"
"github.com/stretchr/testify/assert"
) func Test_assert(t *testing.T) {
a := 2
b := 3
assert.Equal(t, a+b, 5, "They should be equal")
}

require包提供与assert包相同的全局函数,相比assert没有返回值,而是终止当前测试

import (
"testing"
"github.com/stretchr/testify/require"
) func Test_require(t *testing.T) {
var name = "dazuo"
var age = 24
require.Equal(t, "dazuo", name, "they should be equal")
require.Equal(t, 24, age, "they should be equal")
}
mock Package
package testify

import (
"fmt"
"testing" "github.com/stretchr/testify/mock"
) type Storage interface {
Store(key, value string) (int, error)
Load(key string) (string, error)
} // 测试用例,当真实对象不可用时,使用mock对象代替
type mockStorage struct {
mock.Mock
} func (ms *mockStorage) Store(key, value string) (int, error) {
args := ms.Called(key, value)
return args.Int(0), args.Error(1)
} func (ms *mockStorage) Load(key string) (string, error) {
args := ms.Called(key)
return args.String(0), args.Error(1)
} func Test_mock(t *testing.T) {
mockS := &mockStorage{}
mockS.On("Store", "name", "dazuo").Return(20, nil).Once() var storage Storage = mockS
i, e := storage.Store("name", "dazuo")
if e != nil {
panic(e)
}
fmt.Println(i)
}

四、Stub/Mock框架

4.1 GoStub框架

4.1.1.安装依赖

go get github.com/prashantv/gostub

4.1.2.使用场景

  • 为全局变量打桩
  • 为函数打桩
  • 为过程打桩(当一个函数没有返回值时,该函数我们一般称为过程)

4.1.3.测试示例

import (
"fmt" "github.com/prashantv/gostub"
) // 1.为全局变量打桩
var counter = 100 func stubGlobalVariable() {
stubs := gostub.Stub(&counter, 200)
defer stubs.Reset()
fmt.Println("Counter:", counter)
} // 2.为函数打桩
var Exec = func() {
fmt.Println("Exec")
} func stubFunc() {
stubs := gostub.Stub(&Exec, func() {
fmt.Println("Stub Exec")
})
Exec()
defer stubs.Reset()
} // 3.为过程打桩(当一个函数没有返回值时,该函数我们一般称为过程。很多时候,我们将资源清理类函数定义为过程。)
var CleanUp = cleanUp func cleanUp(val string) {
fmt.Println(val)
} func stubPath() {
stubs := gostub.StubFunc(&CleanUp)
defer stubs.Reset()
CleanUp("Hello go")
} func main() {
//stubGlobalVariable()
//stubFunc()
stubPath()
}

4.2 GoMock框架

4.2.1 GoMock简介

gomock 是官方提供的 mock 框架,同时还提供了 mockgen 工具用来辅助生成测试代码。

使用如下命令即可安装:

go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen

4.2.2 测试示例

db.go
// db.go
type DB interface {
Get(key string) (int, error)
} func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err != nil {
return value
}
return -1
}

2.使用 mockgen 生成 db_mock.go

mockgen -source=db.go -destination=db_mock.go -package=PACKAGE_NAME

3.写测试用例 TestGetFromDB

import (
"errors"
"testing" "github.com/golang/mock/gomock"
) func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用 m := NewMockDB(ctrl)
// 打桩
m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist")) if v := GetFromDB(m, "Tom"); v != -1 {
t.Fatal("expected -1, but got", v)
}
}

4.3 gomonkey框架

4.3.1 安装依赖

go get github.com/agiledragon/gomonkey

4.3.2 测试用例

example_test.go
import (
"fmt"
"testing" "github.com/agiledragon/gomonkey"
"github.com/smartystreets/goconvey/convey"
) // 假设networkFunc是一个网络调用
func networkFunc(a, b int) int {
return a + b
} // 本地单测一般不会进行网络调用,所以要mock住networkFunc
func Test_MockNetworkFunc(t *testing.T) {
convey.Convey("123", t, func() {
p := gomonkey.NewPatches()
defer p.Reset() p.ApplyFunc(networkFunc, func(a, b int) int {
fmt.Println("in mock function")
return a + b
})
_ = networkFunc(10, 20)
})
}

4.3.3 运行测试

如果启用了内联,将无法mock,可以通过添加命令行参数:

  • go1.10以下的:-gcflags=-l
  • go1.10以上的:-gcflags=all=-l
go test -v example_test.go -gcflags=all=-l

五、参考资料