单元测试,一个不断被强调,但又不断被忽略的话题,从一名码农成长为一名优秀的工程师,单元测试,是必不可少的技能。单元测试是在所有测试环节中最先完成的,在我们写代码的过程中就需要写单元测试,也可能在未写代码前先写测试。个人认为编码能力,代码评审和单元测试是一个研发同学必备的三大重要技能。
Go常用测试框架
框架名称 | 特征 | 优点 | 缺点 | 备注 |
---|---|---|---|---|
Testing | table-driver测试 支持基准测试 | Go原生 简单易用 | 不支持mock 不支持assert | testing包文档: http://cngolib.com/testing.html |
Testify | table-driver测试 支持mock 支持assert 兼容go test | 支持assert,功能强大易用 支持mock 兼容go test 社区活跃度高 | mock能力较弱 | 官方文档:https://pkg.go.dev/github.com/stretchr/testify |
GoConvey | 支持assert 支持web界面 支持colorized输出 支持测试用例层级嵌套 兼容go test | 支持web界面 支持colorized输出 支持测试用例层级嵌套 | 不支持mock 社区不太活跃 | 项目地址: https://github.com/smartystreets/goconvey 安装:go get http://github.com/smartystreets/goconvey |
Ginkgo | 支持基准测试 支持并发测试 兼容go test | 支持并发测试 | 手册:https://www.ginkgo.wiki/ 安装: go get http://github.com/onsi/ginkgo/ginkgo |
Mock
在写单测过程中,常会遇到有些不容易构造或者不容易获取的对象,这时候我们要创建一个虚拟的对象以便测试。
常用的Mock可以归纳为两类:
打桩Stub
代表框架:GoMonkey、monkey、GoStub
主要通过打补丁(Patch)的方式替换函数、方法、变量等等
monkey.PatchInstanceMethod(reflect.TypeOf(personService), "GetPersonById", func(_ *PersonServiceImpl, _ string) (*Person, error) {
return &Person{
Name: "test_name",
}, nil
})
依赖注入+GoMock
这种方法要求业务代码必须以“依赖倒置”的方式实现。关于什么是“依赖倒置原则”,参考官方的定义:
- 高层模块不应该依赖底层模块,两个都应该依赖抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。“抽象”是指接口(interface)或者抽象类(abstract class),“细节”指的是实现(struct或者class)。
举例来说,比如在业务层我们会定义一些接口,如获取用户信息:
type UserStorager interface {
GetUserInfo(context.Context, *UserParam) (User, error)
}
对于这些接口使用gomock进行模拟,通过mockgen -source=user.go -destination=user_mock.go 可自动生成接口应的mock实现
// Code generated by MockGen. DO NOT EDIT.
// Source: user.go
// Package mock_user is a generated GoMock package.
package mock_user
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
ibasic "icode.baidu.com/baidu/bfe-private-cloud/control-api/api-server/module/ibasic"
)
// MockUserStorager is a mock of UserStorager interface.
type MockUserStorager struct {
ctrl *gomock.Controller
recorder *MockUserStoragerMockRecorder
}
// MockUserStoragerMockRecorder is the mock recorder for MockUserStorager.
type MockUserStoragerMockRecorder struct {
mock *MockUserStorager
}
// NewMockUserStorager creates a new mock instance.
func NewMockUserStorager(ctrl *gomock.Controller) *MockUserStorager {
mock := &MockUserStorager{ctrl: ctrl}
mock.recorder = &MockUserStoragerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserStorager) EXPECT() *MockUserStoragerMockRecorder {
return m.recorder
}
// GetUserInfo mocks base method.
func (m *MockUserStorager) GetUserInfo(arg0 context.Context, arg1 *ibasic.UserParam) (ibasic.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserInfo", arg0, arg1)
ret0, _ := ret[0].(ibasic.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserInfo indicates an expected call of GetUserInfo.
func (mr *MockUserStoragerMockRecorder) GetUserInfo(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockUserStorager)(nil).GetUserInfo), arg0, arg1)
}