此文档仅供交易组编写 Golang 单元测试提供参考示例
就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。
Go 语言中的测试依赖 go test 命令。编写测试代码和编写普通的 Go 代码过程是类似的,并不需要学习新的语法、规则或工具。
go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以 _test.go 为后缀名的源代码文件都是 go test 测试的一部分,不会被 go build 编译到最终的可执行文件中。
go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
一个好单元测试应该具备以下属性:
编写可测试的代码可能比编写单元测试本身更加重要,可测试的代码简单来说就是指我们可以很容易的为其编写单元测试代码。编写单元测试的过程也是一个不断思考的过程,思考我们的代码是否正确的被设计和实现。 Google 测试大牛 - Misko Hevery 的文章:Writing Testable Code 中提到一个非常实用的观点:在开发时,多想想如何使得自己的代码更方便去测试。如果考虑到这些,那么通常你的代码设计也不会太差。 编写可测试代码需要一定的纪律性、专注力和额外的努力。我们尽管在软件开发中会存在复杂的心理活动,但是也应该时刻小心,避免鲁莽地从头就开始盲目的堆砌代码。 如果我们保证了软件的开发质量,那么我们最终会得到干净的、易于维护的、低耦合的和可重用的 API, 当开发人员理解这些 API 的时候,以致于不会让他们头疼。毕竟,可测试性代码的最终优势不止在于其本身的可测试,更给代码带来了易理解、易维护和易扩展的优点。 参考:编写可测试的代码
sqlmock 是一个实现 sql/driver 的 mock 库。它不需要建立真正的数据库连接就可以在测试中模拟任何 sql 驱动程序的行为。使用它可以很方便的在编写单元测试的时候 mock sql 语句的执行结果。
安装
go get github.com/DATA-DOG/go-sqlmock
示例如下:
gopackage access
//go:generate mockery --name Accessor --filename access.go --with-expecter
type Accessor interface {
dbaccess.Accessor
SearchTagsBySubAccountID(ctx context.Context, tx *gorm.DB, subAccountID string) (SubAccountIDWithTagInfos, error)
}
type access struct {
dbaccess.Accessor
}
func (a access) SearchTagsBySubAccountID(ctx context.Context, tx *gorm.DB, subAccountID string) (SubAccountIDWithTagInfos, error) {
if tx == nil {
tx = mysql.WithContext(ctx, a.DB())
}
var results []SubAccountIDWithTagInfo
sql := `select t3.subAccountId as subAccountId, t1.id as tagId, t1.name as tagName
from tradedb.t_follow_invest_label_basicinfo t1
inner join tradedb.t_follow_invest_client_label t2
inner join tradedb.t_follow_invest_client_basicinfo t3
on t1.id = t2.labelId and t2.clientId = t3.clientId
where t3.subAccountId = ?`
return results, tx.Raw(sql, subAccountID).Scan(&results).Error
}
func New(db *gorm.DB) Accessor {
return access{dbaccess.NewAccessor(db)}
}
gopackage access_test
func TestAccess_SearchTagsBySubAccountID(t *testing.T) {
Convey("access", t, func() {
Convey("SearchTagsBySubAccountID", func() {
// init
// open database stub
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
SoMsg("new sqlmock failed ", err, ShouldBeNil)
defer mock.ExpectationsWereMet()
gormDB, err := mysql.WrapSQLDBWithMode(db, &mysql.Options{SkipInitializeWithVersion: true}, "debug")
SoMsg("new gorm db failed ", err, ShouldBeNil)
Convey("success case", func() {
// define behavior
eq := mock.ExpectQuery(`select t3.subAccountId as subAccountId, t1.id as tagId, t1.name as tagName
from tradedb.t_follow_invest_label_basicinfo t1
inner join tradedb.t_follow_invest_client_label t2
inner join tradedb.t_follow_invest_client_basicinfo t3
on t1.id = t2.labelId and t2.clientId = t3.clientId
where t3.subAccountId = ?`)
eq.WithArgs(sqlmock.AnyArg())
eq.WillReturnRows(sqlmock.NewRows([]string{"subAccountId", "tagId", "tagName"}).
AddRow("001", 1, "美股达人").
AddRow("002", 2, "港股达人"))
dbSearchResult, err := access.New(gormDB).SearchTagsBySubAccountID(context.Background(), nil, "")
So(err, ShouldBeNil)
So(dbSearchResult, ShouldNotBeNil)
So(2, ShouldEqual, len(dbSearchResult))
Printf("db search result is %v\n", dbSearchResult)
Printf("db search result tagIDs is %v\n", dbSearchResult.TagIDs())
})
Convey("RecordNotFound case", func() {
// define behavior
eq := mock.ExpectQuery(`select t3.subAccountId as subAccountId, t1.id as tagId, t1.name as tagName
from tradedb.t_follow_invest_label_basicinfo t1
inner join tradedb.t_follow_invest_client_label t2
inner join tradedb.t_follow_invest_client_basicinfo t3
on t1.id = t2.labelId and t2.clientId = t3.clientId
where t3.subAccountId = ?`)
eq.WithArgs(sqlmock.AnyArg())
eq.WillReturnError(gorm.ErrRecordNotFound)
dbSearchResult, err := access.New(gormDB).SearchTagsBySubAccountID(context.Background(), nil, "")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, gorm.ErrRecordNotFound)
So(dbSearchResult, ShouldBeNil)
})
})
})
}
待补充
使用 gock 可以对外部 API 资源进行模拟,即模拟指定参数返回约定好的响应内容。
安装
go get -u github.com/h2non/gock
代码示例:
gopackage requester
var ErrNotOK = stderr.New("status not ok")
func IsStatusNotOK(err error) bool {
return errors.Is(err, ErrNotOK)
}
//go:generate mockery --name Requester --filename requester.go --with-expecter
type Requester interface {
Get(ctx context.Context, path string, ops ...Ops) (*resty.Response, error)
Post(ctx context.Context, path string, ops ...Ops) (*resty.Response, error)
}
type client struct {
client *resty.Client
host string
}
func New(c *resty.Client, host string) Requester {
return &client{client: c, host: host}
}
func (r client) Get(ctx context.Context, path string, ops ...Ops) (*resty.Response, error) {
path, err := JoinPath(r.host, path)
if err != nil {
return nil, errors.Wrapf(err, "join host(%v) with path(%v) failed", r.host, path)
}
return r.check(r.new(ctx, ops...).Get, path)
}
func (r client) Post(ctx context.Context, path string, ops ...Ops) (*resty.Response, error) {
path, err := JoinPath(r.host, path)
if err != nil {
return nil, errors.Wrapf(err, "join host(%v) with path(%v) failed", r.host, path)
}
return r.check(r.new(ctx, ops...).Post, path)
}
gopackage requester
func TestClient_Get(t *testing.T) {
t.Run("match", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
// define behavior
body := `{
"jsonrpc": "2.0",
"result": [
{
"uin": 1056338,
"loginTime": "2023-02-04 14:54:20"
}
],
"id": "643c075a-6a7e-4985-a8d6-7edf357f9594"
}`
gock.New(host).Get(constant.UCQueryV1LoginHistory).MatchParam("subAccountId", "001").Reply(http.StatusOK).BodyString(body)
resp, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory, WithQueryParams(map[string]string{"subAccountId": "001"}))
require.Nil(t, err)
require.Equal(t, resp.StatusCode(), http.StatusOK)
require.Equal(t, resp.Body(), []byte(body))
})
t.Run("status not 200 but 404", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
// define behavior
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusNotFound)
resp, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, IsStatusNotOK(err))
require.Equal(t, resp.StatusCode(), http.StatusNotFound)
})
t.Run("status not 200 but 502", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
// define behavior
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusBadGateway)
resp, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, IsStatusNotOK(err))
require.Equal(t, resp.StatusCode(), http.StatusBadGateway)
})
t.Run("context timeout", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
// define behavior
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusOK).Delay(time.Second)
ctx, cancel := context.WithTimeout(context.Background(), time.Second/2)
defer cancel()
_, err := r.Get(ctx, constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, restyclient.IsTimeout(err))
})
t.Run("client timeout", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
c.GetClient().Timeout = time.Second / 2
// intercept http client to gock
gock.InterceptClient(c.GetClient())
// define behavior
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusOK).Delay(time.Second)
_, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, restyclient.IsTimeout(err))
})
t.Run("network close", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusOK).SetError(net.ErrClosed)
_, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, errors.Is(err, net.ErrClosed))
})
t.Run("EOF", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusOK).SetError(io.EOF)
_, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.True(t, errors.Is(err, io.EOF))
})
t.Run("path err", func(t *testing.T) {
host := "://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
// intercept http client to gock
gock.InterceptClient(c.GetClient())
gock.New(host).Get(constant.UCQueryV1LoginHistory).Reply(http.StatusOK)
_, err := r.Get(context.Background(), constant.UCQueryV1LoginHistory)
require.NotNil(t, err)
require.IsType(t, &url.Error{}, errors.Cause(err))
})
t.Run("context with requestID", func(t *testing.T) {
host := "http://service-ucquery"
defer gock.Off()
// get resty client
c := restyclient.GetRestyClient()
// new requester
r := New(c, host)
requestID := uuid.NewV4().String()
// intercept http client to gock
gock.InterceptClient(c.GetClient())
gock.New(host).Get(constant.UCQueryV1LoginHistory).MatchHeader(httpconstant.HeaderXRequestID, requestID).Reply(http.StatusOK)
ctx := context.Background()
ctx = contexthelper.Store(ctx, log.RequestIdKey, requestID)
_, err := r.Get(ctx, constant.UCQueryV1LoginHistory)
require.Nil(t, err)
})
}
testify 可以说是最流行的 Go 语言测试库了。 testify 提供了很多方便的函数帮助我们做 assert 和错误信息输出。同时对 mock 也支持的很好;
Mock 简单来说就是构造一个仿对象,仿对象提供和原对象一样的接口,在测试中用仿对象来替换原对象。
mockery 提供了使用 stretr/testify/mock 包轻松生成 Golang 接口模拟的能力。
安装
go install github.com/vektra/mockery
示例代码:
go//go:generate mockery --name Requester --filename requester.go --with-expecter
type Requester interface {
Get(ctx context.Context, path string, ops ...Ops) (*resty.Response, error)
Post(ctx context.Context, path string, ops ...Ops) (*resty.Response, error)
}
go generate ./...
gopackage mocks
// Requester is an autogenerated mock type for the Requester type
type Requester struct {
mock.Mock
}
func (_m *Requester) EXPECT() *Requester_Expecter {
return &Requester_Expecter{mock: &_m.Mock}
}
func (_e *Requester_Expecter) Get(ctx interface{}, path interface{}, ops ...interface{}) *Requester_Get_Call {
return &Requester_Get_Call{Call: _e.mock.On("Get",append([]interface{}{ctx, path}, ops...)...)}
}
func (_e *Requester_Expecter) Post(ctx interface{}, path interface{}, ops ...interface{}) *Requester_Post_Call {
return &Requester_Post_Call{Call: _e.mock.On("Post",append([]interface{}{ctx, path}, ops...)...)}
}
//...
gopackage ucquery
//go:generate mockery --name Client --filename ucquery.go --with-expecter
type Client interface {
SearchUserInfo(ctx context.Context, req *SearchUserInfoReqParams, resp *SearchUserInfoRespData) error
SearchUserLastLoginTime(ctx context.Context, req *SearchUsersLastLoginTimeReqParams, resp *SearchUsersLastLoginTimeRespData) error
}
type client struct {
requester requester.Requester
config *viper.Viper
}
func (p client) SearchUserInfo(ctx context.Context, req *SearchUserInfoReqParams, resp *SearchUserInfoRespData) error {
queryString, err := qs.Marshal(req)
if err != nil {
return errors.Wrapf(err, "marshal query param to str failed,params: %v", req)
}
body := &mapping.BaseResp{Result: resp}
_, err = p.requester.Get(ctx, constant.UCQueryV1AccountInfo, requester.WithQueryString(queryString), requester.WithResult(body))
if err != nil {
return err
}
return nil
}
func New(r requester.Requester, config *viper.Viper) Client {
return &client{requester: r, config: config}
}
gopackage ucquery
func TestClient_SearchUserInfo(t *testing.T) {
t.Run("match", func(t *testing.T) {
ctx := context.Background()
// mock requester
r := requester.NewRequester(t)
r.EXPECT().Get(ctx, constant.UCQueryV1AccountInfo, mock.Anything, mock.Anything).Return(nil, nil)
// params
//searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format(time.DateTime)}
searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format("2006-01-02 15:04:05")}
searchReq := &SearchUserInfoReqParams{SubAccountID: "001"}
// pass mock request to new client
ucqueryClient := New(r, viper.Sub("dependence"))
err := ucqueryClient.SearchUserInfo(ctx, searchReq, searchResp)
require.Nil(t, err)
})
t.Run("timeout", func(t *testing.T) {
ctx := context.Background()
// mock requester
r := requester.NewRequester(t)
r.EXPECT().Get(ctx, constant.UCQueryV1AccountInfo, mock.Anything, mock.Anything).Return(nil,errors.Wrap(context.DeadlineExceeded, "request timeout"))
// params
//searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format(time.DateTime)}
searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format("2006-01-02 15:04:05")}
searchReq := &SearchUserInfoReqParams{SubAccountID: "001"}
// pass mock request to new client
ucqueryClient := New(r, viper.Sub("dependence"))
err := ucqueryClient.SearchUserInfo(ctx, searchReq, searchResp)
require.NotNil(t, err)
require.True(t, restyclient.IsTimeout(err))
})
t.Run("qs marshal failed", func(t *testing.T) {
ctx := context.Background()
// mock requester
r := requester.NewRequester(t)
// params
//searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format(time.DateTime)}
searchResp := &SearchUserInfoRespData{LastLoginTime: time.Now().Format("2006-01-02 15:04:05")}
// pass mock request to new client
ucqueryClient := New(r, viper.Sub("dependence"))
err := ucqueryClient.SearchUserInfo(ctx, nil, searchResp)
require.NotNil(t, err)
})
}
有些时候我们需要对非接口进行模拟,改变默认行为,譬如全局对象,全局函数,未定义接口的对象等,这个时候可以考虑使用 gomonkey
安装
go get github.com/agiledragon/gomonkey/v2
这里以 service 层某个实现函数依赖 clientinfoupdater.Updater 为例
type Updater struct { dbAccess access.Accessor // 数据库交互 ucqueryClient ucquery.Client // UC 请求客户端 productProvider product.Provider // 产品信息查询聚合实体 changeFlowBeforeUpdateContent map[string]interface{} // 记录变更内容 changeFlowAfterUpdateContent map[string]interface{} // 记录变更内容 } func NewUpdater(dbAccess access.Accessor, ucqueryClient ucquery.Client, productProvider product.Provider) *Updater { return &Updater{dbAccess: dbAccess, ucqueryClient: ucqueryClient, productProvider: productProvider, changeFlowBeforeUpdateContent: map[string]interface{}{}, changeFlowAfterUpdateContent: map[string]interface{}{}} }
实际的service 业务代码
func (s *service) ClientInfoUpdate(ctx context.Context, req *mapping.ClientInfoUpdateReq) error { updater := clientinfoupdater.NewUpdater(s.dbAccess, s.ucqueryClient, s.productProvider) // 根据请求内容更新用户基本信息 if err := updater.UpdateClientInfoByReq(ctx, req); err != nil { return errors.WithMessagef(err, "update client info failed") } // 根据请求内容更新标签列表 if err := updater.UpdateClientLabelsByReq(ctx, req); err != nil { return errors.WithMessagef(err, "update client labels failed") } // 根据请求内容更组合及新持仓情况,同步调仓 if err := updater.UpdateClientPortfolioInfoByReq(ctx, req); err != nil { return errors.WithMessagef(err, "update client portfolio info failed") } // 补充 change flow if err := updater.AddChangeFlow(ctx, req); err != nil { return errors.WithMessagef(err, "add client portfolio info update change flow failed") } return nil } func New(db *gorm.DB, client *resty.Client, config *viper.Viper, ucqueryClient ucquery.Client) Service { return &service{dbAccess: access.New(db), config: config, ucqueryClient: ucqueryClient, productProvider: product.NewProvider(config, client, db)} }
测试代码示例:
func TestService_ClientInfoUpdate(t *testing.T) { t.Run("update success", func(t *testing.T) { updater := &clientinfo.Updater{} // 使用 gomonkey 对 updater 的若干函数进行 patch,定义函数返回结果均为 nik patches := gomonkey. ApplyMethodReturn(updater, "UpdateClientInfoByReq", nil). ApplyMethodReturn(updater, "UpdateClientLabelsByReq", nil). ApplyMethodReturn(updater, "UpdateClientPortfolioInfoByReq", nil). ApplyMethodReturn(updater, "AddChangeFlow", nil) // 使用 gomonkey 对 clientinfo.NewUpdater 全局函数进行打桩,声明其返回对象为 patch 之后的 updater patches.ApplyFuncReturn(clientinfo.NewUpdater, updater) defer patches.Reset() // 正常创建 service,无需注入 ser := New(nil, nil, nil, nil) err := ser.ClientInfoUpdate(context.Background(), &mapping.ClientInfoUpdateReq{ Params: mapping.ClientInfoUpdateReqParams{ SubAccountId: "001", ClientInfo: mapping.ClientInfoReqParams{ ClientType: 1, InvestExp: 1, Brief: stringrand.String(100), }, }, }) require.Nil(t, err) }) t.Run("UpdateClientInfoByReq failed on build db query conditions failed", func(t *testing.T) { updater := &clientinfo.Updater{} innerErr := errors.Errorf("build db query conditions by request params failed") updateClientInfoByReqReturnErr := codeerror.New(code.ErrInvalidParams, innerErr) patches := gomonkey. ApplyMethodReturn(updater, "UpdateClientInfoByReq", updateClientInfoByReqReturnErr). ApplyMethodReturn(updater, "UpdateClientLabelsByReq", nil). ApplyMethodReturn(updater, "UpdateClientPortfolioInfoByReq", nil). ApplyMethodReturn(updater, "AddChangeFlow", nil) patches.ApplyFuncReturn(clientinfo.NewUpdater, updater) defer patches.Reset() ser := New(nil, nil, nil, nil) err := ser.ClientInfoUpdate(context.Background(), &mapping.ClientInfoUpdateReq{ Params: mapping.ClientInfoUpdateReqParams{ SubAccountId: "001", ClientInfo: mapping.ClientInfoReqParams{ ClientType: 1, InvestExp: 1, Brief: stringrand.String(100), }, }, }) require.NotNil(t, err) //require.True(t, errors.Is(err, innerErr)) require.ErrorIs(t, err, innerErr) require.Contains(t, err.Error(), "update client info failed") }) }
使用 gomonkey 需要注意执行测试时附带 -gcflags=all=-l
go test -gcflags=all=-l
controller 代码示例:
func (c controller) ClientInfoUpdate(ctx *fiber.Ctx) error { err := c.srv.ClientInfoUpdate(ctx.UserContext(), ctx.Locals(requestparams.Key).(*mapping.ClientInfoUpdateReq)) if err != nil { var serviceCodeError codeerror.CodeError if errors.As(err, &serviceCodeError) { return handleServiceCodeErr(ctx, serviceCodeError) } log.FromContext(ctx.UserContext()).Error("control handle err", zap.NamedError(log.ErrorDetailKey, err)) return code.Response(ctx, code.NewDefaultError(code.ErrUnknow), nil) } return code.OK(ctx, nil) } func New(db *gorm.DB, client *resty.Client, config *viper.Viper, ucqueryClient ucquery.Client) Controller { return &controller{srv: service.New(db, client, config, ucqueryClient)} }
测试示例
package controller func TestController_ClientInfoUpdate(t *testing.T) { t.Run("update failed not with middleware", func(t *testing.T) { t.Run("service update client info failed", func(t *testing.T) { t.Run("database err", func(t *testing.T) { // mock service serviceImpl := servicemocks.NewService(t) serviceImpl.On("ClientInfoUpdate", mock.Anything, mock.Anything).After(time.Second).Return( codeerror.New(code.ErrDatabase, errors.Wrap(gorm.ErrInvalidValue, "update client info failed"))) // stub service.New patches := gomonkey.ApplyFuncReturn(service.New, serviceImpl) defer patches.Reset() // controller controllerImpl := New(nil, nil, nil, nil) // fiber fiberApp := fiber.New() fiberCtx := fiberApp.AcquireCtx(&fasthttp.RequestCtx{}) defer fiberApp.ReleaseCtx(fiberCtx) fiberCtx.Locals(requestparams.Key, &mapping.ClientInfoUpdateReq{Params: mapping.ClientInfoUpdateReqParams{SubAccountId: "001"}}) require.Nil(t, controllerImpl.ClientInfoUpdate(fiberCtx)) require.Equal(t, fiberCtx.GetRespHeader(constant.HeaderRetCode), fmt.Sprint(code.ErrDatabase)) }) t.Run("update client info failed with mock service updater", func(t *testing.T) { // mock service dependents updater := &clientinfoupdater.Updater{} innerErr := errors.Errorf("build db query conditions by request params failed") updateClientInfoByReqReturnErr := codeerror.New(code.ErrInvalidParams, innerErr) patches := gomonkey. ApplyMethodReturn(updater, "UpdateClientInfoByReq", updateClientInfoByReqReturnErr). ApplyMethodReturn(updater, "UpdateClientLabelsByReq", nil). ApplyMethodReturn(updater, "UpdateClientPortfolioInfoByReq", nil). ApplyMethodReturn(updater, "AddChangeFlow", nil) patches.ApplyFuncReturn(clientinfoupdater.NewUpdater, updater) defer patches.Reset() // controller controllerImpl := New(nil, nil, nil, nil) // fiber fiberApp := fiber.New() fiberCtx := fiberApp.AcquireCtx(&fasthttp.RequestCtx{}) defer fiberApp.ReleaseCtx(fiberCtx) fiberCtx.Locals(requestparams.Key, &mapping.ClientInfoUpdateReq{Params: mapping.ClientInfoUpdateReqParams{SubAccountId: "001"}}) require.Nil(t, controllerImpl.ClientInfoUpdate(fiberCtx)) require.Equal(t, fiberCtx.GetRespHeader(constant.HeaderRetCode), fmt.Sprint(code.ErrInvalidParams)) }) }) }) t.Run("update with middleware", func(t *testing.T) { t.Run("not request id set in header", func(t *testing.T) { // controller controllerImpl := New(nil, nil, nil, nil) // fiber fiberApp := fiber.New() // register middleware fiberApp.Use(recover.New(), requestid.New(), trace.Trace()) // register route fiberApp.Group("/followInvestAdmin/:version/"). Post("/ClientInfoUpdate", m.RequestParams((*mapping.ClientInfoUpdateReq)(nil)), m.BodyParser(), m.Validator(), controllerImpl.ClientInfoUpdate) requestID := uuid.NewV4().String() rawReqBody := `{ "jsonrpc": "2.0", "params": { "subAccountId": "001", "clientInfo": { "tags": [1,3] } }, "id": "%v" }` rawReqBody = fmt.Sprintf(rawReqBody, requestID) req := httptest.NewRequest(http.MethodPost, "/followInvestAdmin/v1/ClientInfoUpdate", strings.NewReader(rawReqBody)) resp, err := fiberApp.Test(req, -1) require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, resp.StatusCode, http.StatusOK) require.Equal(t, resp.Header.Get(constant.HeaderRetCode), fmt.Sprint(code.ErrEmptyReqId)) }) t.Run("request data validate failed: not subAccountID", func(t *testing.T) { // controller controllerImpl := New(nil, nil, nil, nil) // fiber fiberApp := fiber.New(fiber.Config{ErrorHandler: code.DefaultErrorHandler}) // register middleware fiberApp.Use(recover.New(), requestid.New(), trace.Trace()) // register route fiberApp.Group("/followInvestAdmin/:version/"). Post("/ClientInfoUpdate", m.RequestParams((*mapping.ClientInfoUpdateReq)(nil)), m.BodyParser(), m.Validator(), controllerImpl.ClientInfoUpdate) requestID := uuid.NewV4().String() rawReqBody := `{ "jsonrpc": "2.0", "params": {}, "id": "%v" }` rawReqBody = fmt.Sprintf(rawReqBody, requestID) req := httptest.NewRequest(http.MethodPost, "/followInvestAdmin/v1/ClientInfoUpdate", strings.NewReader(rawReqBody)) req.Header.Set(httpconstant.HeaderXRequestID, requestID) req.Header.Set(httpconstant.HeaderContentType, httpconstant.MIMEApplicationJSON) resp, err := fiberApp.Test(req, -1) require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, resp.StatusCode, http.StatusOK) require.Equal(t, resp.Header.Get(constant.HeaderRetCode), fmt.Sprint(code.ErrRequest)) }) t.Run("success", func(t *testing.T) { // mock service serviceImpl := servicemocks.NewService(t) serviceImpl.On("ClientInfoUpdate", mock.Anything, mock.Anything).After(time.Second).Return(nil) // stub service.New patches := gomonkey.ApplyFuncReturn(service.New, serviceImpl) defer patches.Reset() // controller controllerImpl := New(nil, nil, nil, nil) // fiber fiberApp := fiber.New() // register middleware fiberApp.Use(recover.New(), requestid.New(), trace.Trace()) // register route fiberApp.Group("/followInvestAdmin/:version/"). Post("/ClientInfoUpdate", m.RequestParams((*mapping.ClientInfoUpdateReq)(nil)), m.BodyParser(), m.Validator(), controllerImpl.ClientInfoUpdate) requestID := uuid.NewV4().String() rawReqBody := `{ "jsonrpc": "2.0", "params": { "subAccountId": "001", "clientInfo": { "tags": [1,3] } }, "id": "%v" }` rawReqBody = fmt.Sprintf(rawReqBody, requestID) req := httptest.NewRequest(http.MethodPost, "/followInvestAdmin/v1/ClientInfoUpdate", strings.NewReader(rawReqBody)) req.Header.Set(httpconstant.HeaderXRequestID, requestID) req.Header.Set(httpconstant.HeaderContentType, httpconstant.MIMEApplicationJSON) resp, err := fiberApp.Test(req, -1) require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, resp.StatusCode, http.StatusOK) require.Equal(t, resp.Header.Get(constant.HeaderRetCode), "0") }) }) }
测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
Go 提供内置功能来检查你的代码覆盖率。我们可以使用 go test -cover 来查看测试覆盖率。
Go 还提供了一个额外的 -coverprofile 参数,用来将覆盖率相关的记录信息输出到一个文件。 go test -cover -coverprofile=c.out
然后我们执行 go tool cover -html=c.out ,使用 cover 工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个 HTML 报告。
由于业务接口统一放在了各自服务模块的 controller 包下,故如果只需要获取业务接口的测试覆盖率可以在项目目录执行命令如下:
go test -v -gcflags=all=-l -cover \go list ./... | grep 'controller'`如果是测试整个项目的覆盖率可以执行如下命令:go test -v -gcflags=all=-l -cover `go list ./... | grep -v 'mocks'``
本文作者:JIeJaitt
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!