Go单元测试培训
一、如何写好单元测试
单元测试(Unit Tests, UT) 是一个优秀项目不可或缺的一部分,特别是在一些频繁变动和多人合作开发的项目中尤为重要。或多或少都会有因为自己的提交,导致应用挂掉或服务宕机的经历。如果这个时候你的修改导致测试用例失败,你再重新审视自己的修改,发现之前的修改还有一些特殊场景没有包含,恭喜你减少了一次上库失误。也会有这样的情况,项目很大,启动环境很复杂,你优化了一个函数的性能,或是添加了某个新的特性,如果部署在正式环境上之后再进行测试,成本太高。对于这种场景,几个小小的测试用例或许就能够覆盖大部分的测试场景。而且在开发过程中,效率最高的莫过于所见即所得了,单元测试也能够帮助你做到这一点,试想一下,假如你一口气写完一千行代码,debug 的过程也不会轻松,如果在这个过程中,对于一些逻辑较为复杂的函数,同时添加一些测试用例,即时确保正确性,最后集成的时候,会是另外一番体验。
如何写好单元测试呢?
首先,学会写测试用例。比如如何测试单个函数/方法;比如如何做基准测试;比如如何写出简洁精炼的测试代码;再比如遇到数据库访问等的方法调用时,如何 mock
。
然后,写可测试的代码。高内聚,低耦合
是软件工程的原则,同样,对测试而言,函数/方法写法不同,测试难度也是不一样的。职责单一,参数类型简单,与其他函数耦合度低的函数往往更容易测试。我们经常会说,“这种代码没法测试”,这种时候,就得思考函数的写法可不可以改得更好一些。为了代码可测试而重构是值得的。
接下来将介绍如何使用 Go 语言的标准库 testing
进行单元测试。
二、Go单元测试框架介绍
gotest 详情参考地址(http://c.biancheng.net/view/124.html)
goconvey 详情参考地址(https://github.com/smartystreets/goconvey)
gomock 详情参考地址(https://github.com/golang/mock)
三、Go工作目录介绍
四、Gotesting介绍
- Go 语言推荐测试文件和源代码文件放在一块,测试文件以
*_test.go
结尾。比如,当前 package 有response.go
一个文件,我们想测试response.go
中的response
函数,那么应该新建response_test.go
作为测试文件。
- 测试用例名称一般命名为
Test
加上待测试的方法名。 - 测试用的参数有且只有一个,在这里是
t *testing.T
。 - GoTest单元测试简明教程 (https://geektutu.com/post/quick-go-test.html)
- 测试目录及文件
utils
├── response
├── response.go
└── response_test.go
- 相应的
response.go
文件代码如下:
// Title response.go
// Description 统一返回值包
// Author jiangxincan@hatech.com.cn 2021/1/22 11:36
// update jiangxincan@hatech.com.cn 2021/1/22 11:36
package response
import (
json "encoding/json"
"errors"
log "github.com/sirupsen/logrus"
)
const (
requestSuccess = 200 // 成功返回状态码
requestError = 500 // 失败返回状态码
operateSuccessMessage = "操作成功" // 成功返回信息
operateErrorMessage = "操作失败" // 失败返回信息
)
// Title 统一返回结果结构体
// Description 统一返回结果结构体
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
type Result struct {
Code int `json:"code" example:"200"`
Message string `json:"message" example:"操作成功"`
Count int `json:"count" example:"1"`
Data interface{} `json:"data" example:"nil"`
}
// Title 编码解析函数
// Description application/json编码解析函数
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Param err error "异常对象"
// Return message string "返回消息说明"
func message(err error) string {
if err != nil {
return operateErrorMessage + ": " + err.Error()
}
return operateSuccessMessage
}
// Title 统一返回成功信息日志记录函数(主要是分页)
// Description 统一返回成功信息日志记录函数(主要是分页)
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Param count int "异常对象"
// Param data interface{} "结果对象"
func ResultSuccessManyAndCount(count int, data interface{}) *Result {
return &Result{requestSuccess, message(nil), count, data}
}
- 那么
response_test.go
测试文件代码如下:
// @Title 请填写文件名称(需要改)
// @Description 请填写文件描述(需要改)
// @Author xincan 2021/2/7 16:10
// @Update xincan 2021/2/7 16:10
package goconvey
import (
. "github.com/smartystreets/goconvey/convey"
"hatech.com.cn/istorm-cnbr-operator/utils"
"hatech.com.cn/istorm-cnbr-operator/utils/response"
"hatech.com.cn/istorm-cnbr-operator/vo"
"testing"
)
var ktv = &vo.KubernetesVo {
Result: 2,
}
type UrlInfo struct {
Res http.ResponseWriter
Req *http.Request
Vars map[string]string
}
// go test 测试
func TestUtilsResponse(t *testing.T) {
res := response.ResultSuccessManyAndCount(1, ktv)
data := (res.Data).(*vo.KubernetesVo)
if data.Result == ktv.Result {
log.Printf(" 测试成功:统一返回成功信息日志记录函数(主要是分页)。data.Result != ktv.Result != %v", data.Result)
}
}
- 测试结果
=== RUN TestMockResponseFromKubernetesService
time="2021-02-23T15:27:33+08:00" level=info msg=" 测试成功:统一返回成功信息日志记录函数(主要是分页)。data.Result == ktv.Result == 2"
0 total assertions
--- PASS: TestMockResponseFromKubernetesService (0.01s)
PASS
Process finished with exit code 0
- gotest几种用法,run方法后面是正则表达式(不区分大小写) ```css 第一种:测试该目录下所有包含*_test.go文件 go test
第一种:指定要测试的文件 go test -v -run=kubernetes-response_test.go
第三种:指定文件中的某一个方法,可以模糊匹配,可以多个方法同时测试 go test -v -run=方法1 go test -v -run=方法1|方法2
第三种:如果文件夹下包含其他文件夹,可以添加“./…”做递归测试 go test -v ./…
第四种:测试代码覆盖率 go test -v -cover
第五种:测试覆盖率(测试文件要和业务文件放到一起,通常情况下不会用到) go test -v -coverprofile cover.out user_test.go user.go go tool cover -html=cover.out -o cover.html
7.
以上是gotest的基础用法,还有高级用法,如BenchMark测试,篇幅和时间原因先介绍这里
<a name="f7fa2605"></a>
#### **五、Goconvey安装介绍**
1. 安装命令
```css
go get github.com/smartystreets/goconvey
- 基本使用方法
// @Title 单元测试
// @Description 针对XXXXXX进行单元测试
// @Author xincan 2021/2/23 16:10
// @Update xincan 2021/2/23 16:10
package goconvey
import (
. "github.com/smartystreets/goconvey/convey"
)
func TestMockResponseFromKubernetesService(t *testing.T) {
Convey("测试内容一说明", t, func() {
Convey("测试内容二说明", func() {
So(Object.Id, ShouldEual, "1")
So(Object.Name, ShouldEual, "张三")
})
Convey("测试内容三说明", func() {
......
})
Convey("测试内容四说明", func() {
Convey("测试内容四,第一分支说明", func() {
......
})
Convey("测试内容四,第二分支说明", func() {
......
})
})
})
}
- Goconvey So 部分判断条件说明
六、GoMock安装介绍
- 在Golang的官方Repo(https://github.com/golang/)中有一个单独的工程叫”mock”(https://github.com/golang/mock),虽然star不是特别多,但它却是Golang官方放出来的mock工具。gomock主要是针对我们go代码中的接口进行mock的。
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
如何你设置过$GOPATH/bin到你的$PATH变量中,那么这里就可以直接运行mockgen命令了,否则需要使用绝对路径或者相当于$GOPATH的目录。
准备好代码,在工程下创建自定义测试文件夹goconvey,通过mockgen命令生成*_mock.go文件执行命令为:
mockgen -source=kubernetes/service/kubernetesService.go -destination=goconvey/kubernetes_mock.go -package=goconvey
创建
*_test.go
文件(kubernetes-service_test.go)最终测试文件目录如下:
goconvey
├── kubernetes_mock.go
└── kubernetes-service_test.go
kubernetes
├── service
└── kubernetesService.go
kubernetesService.go
业务代码如下:
// Title kubernetesService.go
// Description kubernetesService服务层
// Author jiangxincan@hatech.com.cn 2021/1/22 11:36
// update jiangxincan@hatech.com.cn 2021/1/22 11:36
package service
import (
"errors"
"github.com/sirupsen/logrus"
"hatech.com.cn/istorm-cnbr-operator/vo"
"log"
)
// Title kubernetesService服务接口
// Description application/json编码解析函数
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
type IKubernetesService interface {
// 加法运算
Add(num1 int, num2 int) (*vo.KubernetesVo, error)
// 减法运算
Sub(num1 int, num2 int) (*vo.KubernetesVo, error)
// 乘法运算
Mul(num1 int, num2 int) (*vo.KubernetesVo, error)
// 除法运算
Div(num1 int, num2 int) (*vo.KubernetesVo, error)
// 平均值运算
Ave(num1 int, num2 int) (*vo.KubernetesVo, error)
}
// Title kubernetesService服务实现结构体
// Description kubernetesService服务实现结构体
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
type kubernetesService struct {
}
func KubernetesServiceImpl() *kubernetesService {
return &kubernetesService{}
}
// Title 加法运算方法
// Description 加法运算
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Return kubernetesVo *vo.KubernetesVo "返回kubernetesVo结构体"
func (service *kubernetesService) Add(num1 int, num2 int) (*vo.KubernetesVo, error) {
log.Printf("service: num1: %v, num2: %v", num1, num2)
return &vo.KubernetesVo{Result: num1 + num2}, nil
}
// Title 减法运算方法
// Description 减法运算
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Return kubernetesVo *vo.KubernetesVo "返回kubernetesVo结构体"
func (service *kubernetesService) Sub(num1 int, num2 int) (*vo.KubernetesVo, error) {
return &vo.KubernetesVo{Result: num1 - num2}, nil
}
// Title 乘法运算方法
// Description 乘法运算
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Return kubernetesVo *vo.KubernetesVo "返回kubernetesVo结构体"
func (service *kubernetesService) Mul(num1 int, num2 int) (*vo.KubernetesVo, error) {
return &vo.KubernetesVo{Result: num1 * num2}, nil
}
// Title 除法法运算方法
// Description 除法运算
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Return kubernetesVo *vo.KubernetesVo "返回kubernetesVo结构体"
func (service *kubernetesService) Div(num1 int, num2 int) (v *vo.KubernetesVo, err error) {
logrus.WithFields(logrus.Fields{"num1": num1, "num2": num2}).Info("求两个数的商", ": ", "除数不能为0")
if num2 == 0 {
return nil, errors.New("除数不能为0")
}
return &vo.KubernetesVo{Result: num1 / num2}, nil
}
// Title 平均值运算方法
// Description 平均值运算
// Auth jiangxincan@hatech.com.cn 时间(2021/1/22 11:36)
// Return kubernetesVo *vo.KubernetesVo "返回kubernetesVo结构体"
func (service *kubernetesService) Ave(num1 int, num2 int) (*vo.KubernetesVo, error) {
return &vo.KubernetesVo{Result: (num1 + num2) / 2}, nil
}
kubernetes-service_test.go
测试文件代码如下:
// @Title 请填写文件名称(需要改)
// @Description 请填写文件描述(需要改)
// @Author xincan 2021/2/7 16:10
// @Update xincan 2021/2/7 16:10
package goconvey
import (
"errors"
_ "github.com/gavv/httpexpect/v2"
"github.com/golang/mock/gomock"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
"hatech.com.cn/istorm-cnbr-operator/dto"
"hatech.com.cn/istorm-cnbr-operator/vo"
"testing"
)
var param = dto.KubernetesDto {
Num1: 36,
Num2: 4,
}
var rest = struct {
addRes int
subRes int
mulRes int
divHaveRes int
divEqRes int
aveRes int
}{
addRes: 40,
subRes: 32,
mulRes: 144,
divHaveRes: 9,
divEqRes: 0,
aveRes: 20,
}
// mock service 测试
func TestMockServiceKubernetesService(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockIKubernetesService(ctrl)
Convey("两个数处理,单元测试--service", t, func() {
Convey("将两数相加", func() {
mock.EXPECT().Add(param.Num1, param.Num2).Return(&vo.KubernetesVo{Result: rest.addRes}, nil)
data, _ := mock.Add(param.Num1, param.Num2)
if data.Result == rest.addRes {
log.Printf("测试成功:两个数相加。结果为:%v + %v = %v", param.Num1, param.Num2, data.Result)
}
})
Convey("将两数相减", func() {
mock.EXPECT().Sub(param.Num1, param.Num2).Return(&vo.KubernetesVo{Result: rest.subRes}, nil)
data, _ := mock.Sub(param.Num1, param.Num2)
if data.Result == rest.subRes {
log.Printf("测试成功:两个数相减。结果为:%v - %v = %v", param.Num1, param.Num2, data.Result)
}
})
Convey("将两数相乘", func() {
mock.EXPECT().Mul(param.Num1, param.Num2).Return(&vo.KubernetesVo{Result: rest.mulRes}, nil)
data, _ := mock.Mul(param.Num1, param.Num2)
if data.Result == rest.mulRes {
log.Printf("测试成功:两个数相乘。结果为:%v * %v = %v", param.Num1, param.Num2, data.Result)
}
})
Convey("将两数相除", func() {
Convey("将两数相除,除数不为:0", func() {
mock.EXPECT().Div(param.Num1, param.Num2).Return(&vo.KubernetesVo{Result: rest.divHaveRes}, nil)
data, _ := mock.Div(param.Num1, param.Num2)
if data.Result == rest.divHaveRes {
log.Printf("测试成功:两个数相除,除数不为:0。结果为:%v ➗ %v = %v", param.Num1, param.Num2, data.Result)
}
})
Convey("将两数相除,除数是为:0", func() {
mock.EXPECT().Div(param.Num1, 0).Return(&vo.KubernetesVo{Result: 0}, errors.New("除数不能为0"))
_, err := mock.Div(param.Num1, 0)
if err.Error() == "除数不能为0" {
log.Print("测试成功:两个数相除,除数为:0。目前除数为:", 0)
}
})
})
Convey("求两个数的平均值", func() {
mock.EXPECT().Ave(param.Num1, param.Num2).Return(&vo.KubernetesVo{Result: rest.aveRes}, nil)
data, _ := mock.Ave(param.Num1, param.Num2)
if data.Result == rest.aveRes {
log.Printf("测试成功:求两个数的平均值。结果为:(%v + %v) / 2 = %v", param.Num1, param.Num2, data.Result)
}
})
})
}
- idea测试工具测试结果如下:
=== RUN TestMockServiceKubernetesService
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:两个数相加。结果为:36 + 4 = 40"
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:两个数相减。结果为:36 - 4 = 32"
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:两个数相乘。结果为:36 * 4 = 144"
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:两个数相除,除数不为:0。结果为:36 ➗ 4 = 9"
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:两个数相除,除数为:0。目前除数为:0"
time="2021-02-23T15:46:36+08:00" level=info msg="测试成功:求两个数的平均值。结果为:(36 + 4) / 2 = 20"
0 total assertions
--- PASS: TestMockServiceKubernetesService (0.02s)
PASS
Process finished with exit code 0
- goconvey测试结果如下:
2021/02/23 15:47:59 goconvey.go:61: Initial configuration: [host: 127.0.0.1] [port: 8080] [poll: 250ms] [cover: true]
2021/02/23 15:47:59 tester.go:19: Now configured to test 10 packages concurrently.
2021/02/23 15:47:59 goconvey.go:178: Serving HTTP at: http://127.0.0.1:8080
2021/02/23 15:47:59 goconvey.go:105: Launching browser on 127.0.0.1:8080
2021/02/23 15:47:59 integration.go:122: File system state modified, publishing current folders... 0 9684099642
2021/02/23 15:47:59 goconvey.go:118: Received request from watcher to execute tests...
2021/02/23 15:47:59 goconvey.go:111: exec: "start": executable file not found in %PATH%
2021/02/23 15:47:59 goconvey.go:113:
2021/02/23 15:48:00 executor.go:69: Executor status: 'executing'
2021/02/23 15:48:00 coordinator.go:46: Executing concurrent tests: hatech.com.cn/istorm-cnbr-operator/goconvey
2021/02/23 15:48:01 parser.go:24: [passed]: hatech.com.cn/istorm-cnbr-operator/goconvey
2021/02/23 15:48:01 executor.go:69: Executor status: 'idle'
七、总结规范
1. 测试文件必须是业务`文件名称_test.go`结尾
2. 必要时,将测试文件放到业务文件同级文件夹下
3. 测试文件中的方法或函数必须是`Test*.go`开头
4. 测试代码块必须以Convey()函数进行包裹,内部多个分支用Convey()进行隔开
5. 测试文件中的函数可以是多个,也可以是单个,视情况而定
八、Idea测试展示
如图:针对kubernetes-endpoint_test.go进行测试,覆盖率是100%
如图:针对覆盖率,在每个被测试的文件后面都有测试比率,利用idea提供的遍历,可以进行文件测试和查看分析
如图:最终除了dto,vo之外的业务代码全部覆盖