1、case设计原则
1.1 面向工程结构设计
这里是一个比较通用的工程结构目录示例,具体项目会有些许差异,但核心思想是相同的
工程目录结构决定了代码各层级职责和作用关系,推荐面向整体工程目录结构进行全局的case设计,进而梳理哪些模块需要进行测试,哪些需要重点关注进行复杂而周全的用例,哪些可以简单设计等,这里将工程目录划分为两部分:
- 核心逻辑部分
核心逻辑部分负责串联业务的故事主线,包括数据访问层(dal)、业务逻辑层(logic)、接口定义层(method)、远程调用层(rpc)、工具包(util)、消息队列(mq)等 - 数据支撑部分
数据支撑部分一般为无逻辑、无状态的数据承载传递,包括配置文件(conf)、常量枚举(consts)、数据对象(model)等
1.2 围绕函数组织构建
如上图,函数执行过程从Req开始,经过method层进入,经过logic层复杂的业务逻辑编排,且可能会产生一些中间数据,联动dal、rpc、tcc、mq等原子能力支持,协同完成业务请求,最终通过Resp返回调用方。
简单举例来说,函数执行过程就像一条河道,决定走向(执行顺序)、深浅宽窄(复杂度)、分叉(执行路径)等;函数中的数据像河水,是真实流动的载体(数据),它一定是有源头(入参)的,流动过程中可能因为河道环境的不确定性戛然而止(异常中断),也可能因为其他河道的汇入而混浊(线程不安全),还可能因为一时间的阻塞而缓慢(性能),但它最终且最好的结果是汇入大海(结果)。
函数执行过程
一般从函数入口到函数结束,大体经历接口定义层(method) -> 业务逻辑层(logic) -> 数据访问层(dal)、远程调用层(rpc)等等,我们可以按照工程结构的分层职责来进行case设计:
- 接口定义层(method) 是函数的入口、出口,它的职责更多是入参校验、出参组装等,因此对应的case设计可以是入参有效性校验、最终函数结果出参的验证
- 业务逻辑层(logic) 是核心逻辑区域,是整个函数过程中代码量最大、逻辑最复杂的部分,是各类子功能单元的聚合组装层,包含各种数据层访问、rpc远程调用、逻辑处理等,因此对应的case设计可以是各类调用结果验证,异常验证,逻辑分叉验证等等,而且可以在不同子单元之间验证调用顺序,过程中间数据的有效性等等
- 数据访问层(dal)、远程调用层(rpc)等 是子功能单元,按照职责单一设计原则,他们承载的逻辑功能不应复杂,一般不需要单独进行case设计和编写,因为他们作为其他组合逻辑的子集,一定会被其他函数case设计覆盖。如果真的需要针对简单逻辑子单元进行复杂case设计和测试,一定是面向更复杂的场景case来支持更全面的测试场景,而不是因为它的不合理职责功能分配来被迫设计
函数参与数据
函数执行过程中的数据,一般有函数入参、函数出参以及过程中间数据,可以在函数入口对函数入参进行参数有效性校验,比如非空、数量限制等;在函数执行过程中对中间数据一般为临时产生的数据进行校验,中间数据一般作为其他子逻辑的前置条件,所以可以在进入子逻辑模块前进行预期验证,适当增加中间数据的验证可以丰富case设计更加饱满充实,提高逻辑的严谨性;最后是对出参结果的预期验证。
1.3 争取质量效率平衡
case设计考虑的场景越多,越能提高覆盖代码的测试覆盖率,从而能够验证代码的健壮性和逻辑的严谨性,但这势必会占用大量的开发时间要去进行设计、编码、调试等。在一般开发过程中需要在质量和效率之间进行平衡,保证交付质量的前提下合理设计case,一般而言,如 mq、tcc、dal、rpc、util 等模块作为最小参与子单元没有复杂逻辑,只是单纯的数据连接、服务调用传递、简易逻辑计算等,可以在集成测试中进行验证测试,像 util 一些方法可能涉及较多逻辑封装比如特殊计算转换支持等可以适当展开case设计进行验证,其余主要测试精力建议可以在 method层 作为统一入口针对接口定义进行case设计和相关子逻辑单元的设计展开即可,因为最上层逻辑组合的复杂度是最高的,它是需要被重点关注和进行case设计的,其case设计理论上是会覆盖到每一个与之相关的子逻辑单元的case差异化场景的。
一般而言只需要在 method层 针对接口定义展开case设计即可,根据逻辑复杂度和功能重要性来适度进行case设计和扩展,下面例举一个清单帮助感知。
数据 |
数据库操作 |
多线程 |
健壮性 |
其他 |
数据直接验证 ☆ |
简易读 ☆ |
并行 ★ |
幂等 ★★ |
边界 ★ |
2、case设计思路
2.1 一般通用设计
这里构造了一个秒杀项目demo来进行case设计描述
Git地址:
2.1.1 入参验证
传入异常参数触发校验代码逻辑,必填项、数量限制、长度、边界值、可接受的枚举类型等等。
//参数校验不通过
func TestSecKillHandler_Run_ParamCheck_Fail(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_Succ", t, func() {
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
//异常参数构造
SkuCode: "",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//【PART-4:结果验证】
//code验证
assert.Equal(t, int32(errno.Req_Param_Illegal), handler.Resp.Code)
//msg验证
assert.Equal(t, "[SkuCode]不能为空", handler.Resp.Msg)
})
}
2.1.2 过程数据验证
对于函数执行过程中产生的过程数据进行预期验证,将复杂逻辑拆解、细化到每一个子逻辑单元进行,一方面可以提高case验证的颗粒度和透明度,其次可以避免某些测试数据的最终结果符合预期、但过程逻辑错误导致编写case不够健壮、无法暴露问题的情况,此外还可以协助验证调用链路上下游依赖、数据传递等逻辑正确性。
//过程数据验证
func TestSecKillHandler_Run_Stock_NotEnough(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_Stock_NotEnough", t, func() {
//1.校验商品有消息 c.checkSkuCodeHandler
//...省略...
//2.校验用户有效性 c.checkUserInfoHandler
//...省略...
//3.扣减缓存库存 c.decreaseCacheStockHandler
stockCacheMocker := mockito.Mock((*redis.RedisClient).Decr).To(func(cli *redis.RedisClient, key string) (int64, error) {
assert.Equal(t, "KEY:STOCK:SKU123", key)
return -1, nil
}).Build()
//4.订单号生成 c.genOrderNoHandler
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
//5.异步扣DB库存 c.asyncDecreaseDbStockHandler
mqMocker := mockito.Mock(mq.Send).To(func(ctx context.Context, msg string, id string) error {
assert.Equal(t, "ORD123", id)
return nil
}).Build()
//6.日志上报 c.reportLogHandler
//...省略...
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//【PART-4:结果验证】
//...省略...
//执行结果验证
//...省略...
})
}
2.1.3 最终结果验证
对最终输出数据Resp的code、msg、error等进行预期验证,对mocker执行验证确保符合预期调用,尤其是存在逻辑分叉的业务中可以验证执行链路的路由准确性。
//最终结果验证
func TestSecKillHandler_Run_Succ(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_Succ", t, func() {
//1.校验商品有消息 c.checkSkuCodeHandler
queryGoodsInfoRpcMocker := mockito.Mock(rpc.QueryGoodsInfo).To(func(ctx context.Context, skuCodes []string) ([]*model.GoodsInfo, error) {
assert.Equal(t, 1, len(skuCodes))
assert.Equal(t, "SKU123", skuCodes[0])
return []*model.GoodsInfo{
{
SkuCode: "SKU123",
SkuName: "商品123",
},
}, nil
}).Build()
//2.校验用户有效性 c.checkUserInfoHandler
userInfoQueryDbMocker := mockito.Mock((*gorm.DB).Find).To(func(db *gorm.DB, dest interface{}, conds ...interface{}) *gorm.DB {
switch dest.(type) {
case **model.UserInfo:
newObj := &model.UserInfo{
UserId: "U123",
UserName: "zhangsan",
}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
}
return db
}).Build()
//3.扣减缓存库存 c.decreaseCacheStockHandler
stockCacheMocker := mockito.Mock((*redis.RedisClient).Decr).To(func(cli *redis.RedisClient, key string) (int64, error) {
assert.Equal(t, "KEY:STOCK:SKU123", key)
return 10, nil
}).Build()
//4.订单号生成 c.genOrderNoHandler
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
//5.异步扣DB库存 c.asyncDecreaseDbStockHandler
mqMocker := mockito.Mock(mq.Send).To(func(ctx context.Context, msg string, id string) error {
assert.Equal(t, "ORD123", id)
return nil
}).Build()
//6.日志上报 c.reportLogHandler
reportLogRpcMocker := mockito.Mock(rpc.ReportLog).To(func(ctx context.Context, logContent string) error {
time.Sleep(1 * time.Second)
assert.Equal(t, "SKUCODE[SKU123]秒杀成功", logContent)
return nil
}).Build()
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//【PART-4:结果验证】
//商品查询RPC调用,执行一次
assert.Equal(t, 1, queryGoodsInfoRpcMocker.MockTimes())
//用户信息DB查询,执行一次
assert.Equal(t, 1, userInfoQueryDbMocker.MockTimes())
//库存缓存数据扣减,执行一次
assert.Equal(t, 1, stockCacheMocker.MockTimes())
//订单号生成,执行一次
assert.Equal(t, 1, genOrderMocker.MockTimes())
//异步扣DB库存MQ,执行一次
assert.Equal(t, 1, mqMocker.MockTimes())
//日志上报RPC,执行一次
assert.Equal(t, 1, reportLogRpcMocker.MockTimes())
//执行结果验证
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
2.1.4 数据有效性验证
对于业务数据一定要对其有效性进行校验,数据源一般来源于本地存储、远程调用服务等,可以对数据进行适度构造来验证非法或无效数据对逻辑的影响和破坏性,如下例子是对入参商品编码、用户ID进行有效性校验构造设计
//数据有效性验证
func TestSecKillHandler_Run_UserInfo_Invalid(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_UserInfo_Invalid", t, func() {
//1.校验商品有消息 c.checkSkuCodeHandler
queryGoodsInfoRpcMocker := mockito.Mock(rpc.QueryGoodsInfo).To(func(ctx context.Context, skuCodes []string) ([]*model.GoodsInfo, error) {
assert.Equal(t, 1, len(skuCodes))
assert.Equal(t, "SKU123", skuCodes[0])
//***** 返回空,意思是商品编码无效,找不到相关商品信息 *****
return []*model.GoodsInfo{}, nil
}).Build()
//2.校验用户有效性 c.checkUserInfoHandler
userInfoQueryDbMocker := mockito.Mock((*gorm.DB).Find).To(func(db *gorm.DB, dest interface{}, conds ...interface{}) *gorm.DB {
switch dest.(type) {
case **model.UserInfo:
//***** 返回空,意思是用户ID无效,找不到相关用户信息 *****
newObj := &model.UserInfo{}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
}
return db
}).Build()
//3.扣减缓存库存 c.decreaseCacheStockHandler
//...省略...
//4.订单号生成 c.genOrderNoHandler
//...省略...
//5.异步扣DB库存 c.asyncDecreaseDbStockHandler
//...省略...
//6.日志上报 c.reportLogHandler
//...省略...
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//【PART-4:结果验证】
//...省略...
})
}
2.1.5 异常验证
异常构造的场景很多,比如RPC调用、数据库访问、MQ发送等的error返回设计,验证对异常处理的健壮性,能否对异常情况进行合理响应处理。
// 异常验证
func TestSecKillHandler_Run_Fail(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_Fail", t, func() {
//1.校验商品有消息 c.checkSkuCodeHandler
queryGoodsInfoRpcMocker := mockito.Mock(rpc.QueryGoodsInfo).To(func(ctx context.Context, skuCodes []string) ([]*model.GoodsInfo, error) {
assert.Equal(t, 1, len(skuCodes))
assert.Equal(t, "SKU123", skuCodes[0])
return nil, errno.NewCodeErrorWithMessage(errno.Internal_Error, "err")
}).Build()
//2.校验用户有效性 c.checkUserInfoHandler
userInfoQueryDbMocker := mockito.Mock((*gorm.DB).Find).To(func(db *gorm.DB, dest interface{}, conds ...interface{}) *gorm.DB {
switch dest.(type) {
case **model.UserInfo:
newObj := &model.UserInfo{
UserId: "U123",
UserName: "用户Test",
}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
}
db.Error = errno.NewCodeErrorWithMessage(errno.Internal_Error,"err")
return db
}).Build()
//3.扣减缓存库存 c.decreaseCacheStockHandler
stockCacheMocker := mockito.Mock((*redis.RedisClient).Decr).To(func(cli *redis.RedisClient, key string) (int64, error) {
assert.Equal(t, "KEY:STOCK:SKU123", key)
return 10, errno.NewCodeErrorWithMessage(errno.Internal_Error, "redis err")
}).Build()
//4.订单号生成 c.genOrderNoHandler
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
//5.异步扣DB库存 c.asyncDecreaseDbStockHandler
mqMocker := mockito.Mock(mq.Send).To(func(ctx context.Context, msg string, id string) error {
assert.Equal(t, "ORD123", id)
return errno.NewCodeErrorWithMessage(errno.Internal_Error, "mq err")
}).Build()
//6.日志上报 c.reportLogHandler
reportLogRpcMocker := mockito.Mock(rpc.ReportLog).To(func(ctx context.Context, logContent string) error {
time.Sleep(1 * time.Second)
assert.Equal(t, "SKUCODE[SKU123]秒杀成功", logContent)
return errno.NewCodeErrorWithMessage(errno.Internal_Error, "log上报异常")
}).Build()
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//【PART-4:结果验证】
//...省略结果验证...
})
}
2.2 复杂逻辑设计
这里收集了一部分项目实战中场景来分别论述下
2.2.1 业务幂等
一般业务幂等是通过Redis数据检查、数据库唯一索引进行实现的,因此可以基于此进行数据构造来验证逻辑。
如下,是一个根据上游单号BizNo字段进行数据库层单据业务幂等的示例,这里也涉及到一个OrderStatus状态机字段来决定是否重复发起对下游业务的请求的case设计,幂等逻辑是需要当前服务消化和支持的,最终重复请求的Resp返回结果一定是成功且上游无额外感知的,对上游调用没有任何理解成本。
//业务幂等
func TestCreateOutboundOrderHandler_Run_HasIssued(t *testing.T) {
ctx := context.Background()
bizNo := logid.GenLogID()
ctx = kitutil.NewCtxWithLogID(ctx, bizNo)
mockito.PatchConvey("TestCreateOutboundOrderHandler_Run_本地已下推 幂等", t, func() {
//mock
dbQueryMock := mockito.Mock((*gorm.DB).Find).To(func(db *gorm.DB, dest interface{}, conds ...interface{}) *gorm.DB {
switch dest.(type) {
//出库单
case *[]*model.OutboundOrder:
newObj := &[]*model.OutboundOrder{
{
BizNo: "inv-1238123127312399",
WarehouseId: int64(common.WarehouseId),
OutboundNo: "123",
OrderStatus: int8(common.BoundOrderStatus_Issued),
Status: int8(common.DataStatus_Valid),
},
}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(*newObj))
//...省略其他数据构造...
return &gorm.DB{}
}).Build()
//...省略复杂mock构造...
//test
req := &inv.CreateOutboundOrderRequest{
MerchantCode: "EY001",
ShipmentSource: common.WarehouseId,
OrderType: common.Sale_Outbound,
OutboundOrder: &inv.OutboundOrder{
BizNo: "inv-1238123127312399",
OutboundOrderCreateTime: time.Now().Unix(),
EstFinishedTime: time.Now().Unix(),
VendorCode: "EY001",
OutboundItems: []*inv.OutboundItem{
{
SkuCode: "EDU0001",
SkuNum: 10,
SkuName: thrift.StringPtr("商品11111"),
},
{
SkuCode: "EDU0002",
SkuNum: 10,
SkuName: thrift.StringPtr("商品22222"),
},
},
},
Remark: "",
}
handler := NewCreateOutboundOrderHandler(ctx, req)
resp := handler.Run()
//check
//...省略复杂验证...
assert.Equal(t, 1, dbQueryMock.MockTimes())
//幂等后,其他所有逻辑一定是不会执行的,起到拦截作用
assert.Equal(t, 0, otherMock.MockTimes())
assert.Equal(t, int32(0), resp.GetBaseResp().GetStatusCode())
})
}
如下,是一个根据上游单号BizNo字段进行单据业务在Redis层请求幂等的拦截,只是数据源构造不同,逻辑和目标和上一个例子是异曲同工的。
//缓存拦截幂等单据
func TestCreateOutboundOrderHandler_Run_OrderLock(t *testing.T) {
ctx := context.Background()
bizNo := logid.GenLogID()
ctx = kitutil.NewCtxWithLogID(ctx, bizNo)
mockito.PatchConvey("TestCreateOutboundOrderHandler_Run_OrderLock 缓存拦截幂等单据", t, func() {
//mock
//...省略复杂业务逻辑...
stockCacheMocker := mockito.Mock((*redis.RedisClient).Exists).To(func(cli *redis.RedisClient, key string) (bool, error) {
assert.Equal(t, "KEY:BIZ_NO:inv-1238123127312399", key)
return true, nil
}).Build()
//test
req := &inv.CreateOutboundOrderRequest{
MerchantCode: "EY001",
ShipmentSource: common.WarehouseId,
OrderType: common.Sale_Outbound,
OutboundOrder: &inv.OutboundOrder{
BizNo: "inv-1238123127312399",
OutboundOrderCreateTime: time.Now().Unix(),
EstFinishedTime: time.Now().Unix(),
VendorCode: "EY001",
OutboundItems: []*inv.OutboundItem{
{
SkuCode: "EDU0001",
SkuNum: 10,
SkuName: thrift.StringPtr("商品11111"),
},
{
SkuCode: "EDU0002",
SkuNum: 10,
SkuName: thrift.StringPtr("商品22222"),
},
},
},
Remark: "",
}
handler := NewCreateOutboundOrderHandler(ctx, req)
resp := handler.Run()
//check
//...省略复杂验证...
assert.Equal(t, 1, dbQueryMock.MockTimes())
//幂等后,其他所有逻辑一定是不会执行的,起到拦截作用
assert.Equal(t, 0, otherMock.MockTimes())
assert.Equal(t, int32(0), resp.GetBaseResp().GetStatusCode())
})
}
2.2.2 分布式锁竞争
这里示例一个分布式锁产生竞争的case,背景假设为函数一次请求要批量对多个商品进行锁定处理,但是单个商品同一时间又只能被一笔业务请求操作使用,如果在处理过程中有商品被锁定需要进行拦截。这部分的case设计主要是面向构造部分锁定失败、部分锁定成功的数据,并且要对释放锁逻辑进行严格判断。
//商品锁拦截
func TestCreateOutboundOrderHandler_Run_SkuLock(t *testing.T) {
ctx := context.Background()
bizNo := logid.GenLogID()
ctx = kitutil.NewCtxWithLogID(ctx, bizNo)
mockito.PatchConvey("TestCreateOutboundOrderHandler_Run_SkuLock 商品锁拦截", t, func() {
//mock
//...省略其他复杂逻辑mock构造...
lockMock := mockito.Mock((*distributelock.RedisLock).Lock).To(func(lock *distributelock.RedisLock) bool {
switch lock.Key {
case "LOCK:SKU_CODE_TX_ID|EY001|EDU0001":
//********商品锁通过,锁定成功********
return true
case "LOCK:SKU_CODE_TX_ID|EY001|EDU0002":
//********商品锁拦截,锁定失败********
return false
}
return false
}).Build()
unlockMock := mockito.Mock((*distributelock.RedisLock).Unlock).To(func(lock *distributelock.RedisLock) bool {
switch lock.Key {
case "LOCK:SKU_CODE_TX_ID|EY001|EDU0001":
return true
case "LOCK:SKU_CODE_TX_ID|EY001|EDU0002":
//********这里是不能被执行的,因为上面锁定逻辑中没有锁定成功,如果能进来说明逻辑存在错误********
t.Fatal("不能执行")
return true
}
return false
}).Build()
//test
req := &inv.CreateOutboundOrderRequest{
MerchantCode: "EY001",
ShipmentSource: common.WarehouseId,
OrderType: common.Sale_Outbound,
OutboundOrder: &inv.OutboundOrder{
BizNo: "inv-1238123127312399",
OutboundOrderCreateTime: time.Now().Unix(),
EstFinishedTime: time.Now().Unix(),
VendorCode: "EY001",
OutboundItems: []*inv.OutboundItem{
{
SkuCode: "EDU0001",
SkuNum: 10,
SkuName: thrift.StringPtr("商品11111"),
},
{
SkuCode: "EDU0002",
SkuNum: 10,
SkuName: thrift.StringPtr("商品22222"),
},
},
},
Remark: "",
}
handler := NewCreateOutboundOrderHandler(ctx, req)
resp := handler.Run()
fmt.Printf("resp:%s", util.StructToJson(resp))
//check
//...省略其他逻辑的验证...
//单据锁 锁定;两个商品锁 一个锁定成功,一个锁定失败
assert.Equal(t, 2, lockMock.MockTimes())
//单据锁 释放;两个商品锁 一个释放,一个不执行
assert.Equal(t, 1, unlockMock.MockTimes())
assert.Equal(t, int32(errno.Request_Too_Frequent), resp.GetBaseResp().GetStatusCode())
assert.Equal(t, true, strings.Contains(resp.GetBaseResp().GetStatusMessage(), "skuCode[LOCK:SKU_CODE_TX_ID|EY001|EDU0002] is processing"))
})
}
2.2.3 异步逻辑
模拟异步执行逻辑的耗时操作,case设计可以在主进程中等待所有子进程执行完毕再进行验证判断,否则可能子进程没有执行完毕但主进程执行完毕导致判断错误的情况。
//异步线程
func TestSecKillHandler_Run_ReportLog_Fail(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestSecKillHandler_Run_ReportLog_Fail", t, func() {
//1.校验商品有消息 c.checkSkuCodeHandler
//...省略mock构建逻辑...
//2.校验用户有效性 c.checkUserInfoHandler
//...省略mock构建逻辑...
//3.扣减缓存库存 c.decreaseCacheStockHandler
//...省略mock构建逻辑...
//4.订单号生成 c.genOrderNoHandler
//...省略mock构建逻辑...
//5.异步扣DB库存 c.asyncDecreaseDbStockHandler
//...省略mock构建逻辑...
//6.日志上报 c.reportLogHandler
reportLogRpcMocker := mockito.Mock(rpc.ReportLog).To(func(ctx context.Context, logContent string) error {
//模拟耗时操作
time.Sleep(1 * time.Second)
assert.Equal(t, "SKUCODE[SKU123]秒杀成功", logContent)
return errno.NewCodeErrorWithMessage(errno.Internal_Error, "log上报异常")
}).Build()
//【PART-3:方法执行】
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
//这里等待可控的异步执行完毕
time.Sleep(3 * time.Second)
//【PART-4:结果验证】
//...省略复杂验证逻辑...
//日志上报RPC,执行1次 reportLogRpcMocker的rpc.ReportLog方法是异步执行的
assert.Equal(t, 1, reportLogRpcMocker.MockTimes())
//执行结果验证 日志上报异步,不影响接口返回结果
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
2.2.4 数据库事务
当多个表同时参与数据库事务时,可以设计某个表异常执行验证结果。
//逻辑代码,一个事务里包含两个表的操作
txErr := db.GetTransProvider().Transaction(c.Ctx, func(ctx context.Context) error {
//order表
orderErr := db.OrderDB.Create(c.Ctx, &model.Order{
SkuCode: c.Req.SkuCode,
UserID: c.Req.UserId,
})
if orderErr != nil {
logs.CtxError(c.Ctx, "Order orderErr:%v", orderErr)
return orderErr
}
//flow表
flowErr := db.FlowDB.Create(c.Ctx, &model.Flow{
OrderNo: util.GenOrderNo(c.Ctx),
FlowId: util.GenOrderNo(c.Ctx),
})
if flowErr != nil {
logs.CtxError(c.Ctx, "Flow flowErr:%v", flowErr)
return flowErr
}
return nil
})
if txErr != nil {
logs.CtxError(c.Ctx, "Order TxErr:%v", txErr)
return txErr
}
//UT CASE
func TestCreateOrderHandler_Run_Tx_Err(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestCreateOrderHandler_Run_Tx_Err", t, func() {
//创建
CreateOrderMocker := mockito.Mock((*gorm.DB).Create).To(func(db *gorm.DB, value interface{}) *gorm.DB {
switch value.(type) {
case *model.Order:
//构造异常
db.Error = errno.NewCodeErrorWithMessage(errno.Internal_Error, "err")
case *model.Flow:
//ok。无异常,但正确逻辑是不会进入的
t.Fatal("不可进入")
}
return db
}).Build()
//【PART-3:方法执行】
handler := NewCreateOrderHandler(context.Background(), &model.CreateOrderReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "USR123",
})
handler.Run()
//【PART-4:结果验证】
//执行结果验证
assert.Equal(t, 1, CreateOrderMocker.MockTimes())
assert.Equal(t, int32(errno.Internal_Error), handler.Resp.Code)
})
}
2.2.5 多线程
调用方法中存在使用多线程并行的话,需要考虑执行超时异常、子线程逻辑异常等情况的case覆盖。
func TestQueryOrderListHandler_Run(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestQueryOrderListHandler_Run", t, func() {
//1.查询订单信息 c.queryOrderListHandler
queryGoodsInfoRpcMocker := mockito.Mock(rpc.QueryGoodsInfo).To(func(ctx context.Context, skuCodes []string) ([]*model.GoodsInfo, error) {
assert.Equal(t, 1, len(skuCodes))
assert.Equal(t, "SKU123", skuCodes[0])
//设置子线程执行超时,触发run timeout 逻辑的处理
time.Sleep(10 * time.Second)
return []*model.GoodsInfo{
{
SkuCode: "SKU123",
SkuName: "商品123",
},
//还可以构造子线程执行异常
}, errno.NewCodeErrorWithMessage(errno.Internal_Error, "goods rpc err")
}).Build()
//2.联动查询关联信息 c.queryRefInfoHandler
queryDbMocker := mockito.Mock((*gorm.DB).Find).To(func(db *gorm.DB, dest interface{}, conds ...interface{}) *gorm.DB {
switch dest.(type) {
case **model.UserInfo:
newObj := &model.UserInfo{
UserId: "U123",
UserName: "zhangsan",
}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
case **model.Order:
newObj := &model.Order{
OrderNo: "ORD001",
SkuCode: "SKU123",
UserID: "U123",
}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
}
return db
}).Build()
//【PART-3:方法执行】
handler := NewQueryOrderListHandler(context.Background(), &model.QueryOrderListReq{
OrderNo: "ORD123",
})
handler.Run()
//【PART-4:结果验证】
//执行结果验证
assert.Equal(t, 1, queryGoodsInfoRpcMocker.MockTimes())
//用户信息 UserInfo -1 / 订单信息 Order -1
assert.Equal(t, 2, queryDbMocker.MockTimes())
assert.Equal(t, int32(errno.Internal_Error), handler.Resp.Code)
})
}
2.2.6 重复调用
当前引入类似retry.Do()方法可以设计case来构造异常触发重试,通过严格的计数统计辅助验证执行次数和逻辑正确性。
//****代码逻辑****
//支持多次重试
retryErr := retry.Do("", 5, 2*time.Second, func() error {
return db.OrderDB.Create(c.Ctx, &model.Order{
SkuCode: c.Req.SkuCode,
UserID: c.Req.UserId,
})
})
if retryErr != nil {
logs.CtxError(c.Ctx, "Order Create retryErr:%v", retryErr)
c.Err = retryErr
return
}
func TestCreateOrderHandler_Run(t *testing.T) {
//【PART-1:方法依赖初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestCreateOrderHandler_Run", t, func() {
//初始计数为0
cnt := 0
//创建
CreateOrderMocker := mockito.Mock((*gorm.DB).Create).To(func(db *gorm.DB, value interface{}) *gorm.DB {
switch value.(type) {
case *model.Order:
//执行两次(cnt=0、1)后返回成功,其他均构造异常返回
if cnt == 1 {
return db
}
cnt++
db.Error = errno.NewCodeErrorWithMessage(errno.Internal_Error, "err")
}
return db
}).Build()
//【PART-3:方法执行】
handler := NewCreateOrderHandler(context.Background(), &model.CreateOrderReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "USR123",
})
handler.Run()
pretty.Println(handler.Resp)
//【PART-4:结果验证】
//执行结果验证
//由于mock构造执行2次,这里是2
assert.Equal(t, 2, CreateOrderMocker.MockTimes())
//触发了重试,但是最终是成功
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
3、UT编写格式
单函数测试文件示例如下:
//MOCK
func TestHandler_Run_Mock_Case_1(t *testing.T) {
//【PART-1:初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestHandler_Run_Mock_Case_1", t, func() {
//各种mock
//rpc mock
// db mock
// redis mock
// mq mock
// ......
mocker := mockito.mock(rpc.call).To(func(ctx context.Context,req interface{})error{
//过程数据验证
assert.Equals(t,'req',req)
}).Build()
//【PART-3:方法执行】
handler := NewTestHandler(context.Background(), &model.Req{
//入参配置
})
handler.Run()
pretty.Println(handler.Resp)
//【PART-4:结果验证】
//mock函数执行次数验证
assert.Equal(t, 1, mocker.MockTimes())
//出参结果验证
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
//MOCK
func TestHandler_Run_Mock_Case_2(t *testing.T) {
//【PART-1:初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法mock】
mockito.PatchConvey("TestHandler_Run_Mock_Case_2", t, func() {
//各种mock
//rpc mock
// db mock
// redis mock
// mq mock
// ......
mocker := mockito.mock(rpc.call).To(func(ctx context.Context,req interface{})error{
//过程数据验证
assert.Equals(t,'req',req)
}).Build()
//【PART-3:方法执行】
handler := NewTestHandler(context.Background(), &model.Req{
//入参配置
})
handler.Run()
pretty.Println(handler.Resp)
//【PART-4:结果验证】
//mock函数执行次数验证
assert.Equal(t, 1, mocker.MockTimes())
//出参结果验证
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
//集成/半集成测试
func TestHandler_Run_Case_1(t *testing.T) {
//【PART-1:初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法执行】
handler := NewTestHandler(context.Background(), &model.Req{
//入参配置
})
handler.Run()
pretty.Println(handler.Resp)
//【PART-3:结果验证】
//mock函数执行次数验证
assert.Equal(t, 1, mocker.MockTimes())
//出参结果验证
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
//集成/半集成测试
func TestHandler_Run_Case_2(t *testing.T) {
//【PART-1:初始化】
//db、redis、es、mongo、tcc、mq、tos、rpc等等
dal.Init()
//【PART-2:方法执行】
handler := NewTestHandler(context.Background(), &model.Req{
//入参配置
})
handler.Run()
pretty.Println(handler.Resp)
//【PART-3:结果验证】
//mock函数执行次数验证
assert.Equal(t, 1, mocker.MockTimes())
//出参结果验证
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}