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地址:
method层
logic层
rpc层
dal层
mysql
redis
util
mq
rmq
参数校验
req
1.商品信息查询并验证
2.用户信息查询并验证
数据查询
3.扣减缓存库存
库存扣减
4.订单号生成
5.发送库存扣减消息
异步扣减
6.上报日志
resp
method层
logic层
rpc层
dal层
mysql
redis
util
mq
rmq
2.1.1 入参验证
??传入异常参数触发校验代码逻辑,必填项、数量限制、长度、边界值、可接受的枚举类型等等。
req
method
请求
构造异常参数
参数校验
fail
req
method
func TestSecKillHandler_Run_ParamCheck_Fail(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_Succ", t, func() {
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
assert.Equal(t, int32(errno.Req_Param_Illegal), handler.Resp.Code)
assert.Equal(t, "[SkuCode]不能为空", handler.Resp.Msg)
})
}
2.1.2 过程数据验证
??对于函数执行过程中产生的过程数据进行预期验证,将复杂逻辑拆解、细化到每一个子逻辑单元进行,一方面可以提高case验证的颗粒度和透明度,其次可以避免某些测试数据的最终结果符合预期、但过程逻辑错误导致编写case不够健壮、无法暴露问题的情况,此外还可以协助验证调用链路上下游依赖、数据传递等逻辑正确性。
req
method
logic
resp
req
req
handler - 1
过程数据 验证
handler - 2
过程数据 验证
handler - 3
alt
[执行过程]
ack
ack
req
method
logic
resp
func TestSecKillHandler_Run_Stock_NotEnough(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_Stock_NotEnough", t, func() {
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()
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
mqMocker := mockito.Mock(mq.Send).To(func(ctx context.Context, msg string, id string) error {
assert.Equal(t, "ORD123", id)
return nil
}).Build()
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
})
}
2.1.3 最终结果验证
??对最终输出数据Resp的code、msg、error等进行预期验证,对mocker执行验证确保符合预期调用,尤其是存在逻辑分叉的业务中可以验证执行链路的路由准确性。
req
method
logic
resp
req
req
handler - 1
handler - 2
handler - 3
alt
[执行过程]
ack
ack
验证结果/执行次数
req
method
logic
resp
func TestSecKillHandler_Run_Succ(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_Succ", t, func() {
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()
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()
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()
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
mqMocker := mockito.Mock(mq.Send).To(func(ctx context.Context, msg string, id string) error {
assert.Equal(t, "ORD123", id)
return nil
}).Build()
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()
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
assert.Equal(t, 1, queryGoodsInfoRpcMocker.MockTimes())
assert.Equal(t, 1, userInfoQueryDbMocker.MockTimes())
assert.Equal(t, 1, stockCacheMocker.MockTimes())
assert.Equal(t, 1, genOrderMocker.MockTimes())
assert.Equal(t, 1, mqMocker.MockTimes())
assert.Equal(t, 1, reportLogRpcMocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
2.1.4 数据有效性验证
??对于业务数据一定要对其有效性进行校验,数据源一般来源于本地存储、远程调用服务等,可以对数据进行适度构造来验证非法或无效数据对逻辑的影响和破坏性,如下例子是对入参商品编码、用户ID进行有效性校验构造设计
req
method
logic
mysql
rpc
resp
入参
req
handler - 1
数据查询
构造异常数据返回
handler - 2
远程调用
构造异常数据返回
alt
[执行过程]
ack
验证结果/执行次数
req
method
logic
mysql
rpc
resp
func TestSecKillHandler_Run_UserInfo_Invalid(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_UserInfo_Invalid", t, func() {
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()
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{}
v := reflect.ValueOf(dest).Elem()
v.Set(reflect.ValueOf(newObj))
}
return db
}).Build()
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
})
}
2.1.5 异常验证
??异常构造的场景很多,比如RPC调用、数据库访问、MQ发送等的error返回设计,验证对异常处理的健壮性,能否对异常情况进行合理响应处理。
func TestSecKillHandler_Run_Fail(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_Fail", t, func() {
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()
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()
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()
genOrderMocker := mockito.Mock(util.GenOrderNo).Return("ORD123").Build()
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()
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()
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
})
}
2.2 复杂逻辑设计
这里收集了一部分项目实战中场景来分别论述下
2.2.1 业务幂等
??一般业务幂等是通过Redis数据检查、数据库唯一索引进行实现的,因此可以基于此进行数据构造来验证逻辑。 ??如下,是一个根据上游单号BizNo字段进行数据库层单据业务幂等的示例,这里也涉及到一个OrderStatus状态机字段来决定是否重复发起对下游业务的请求的case设计,幂等逻辑是需要当前服务消化和支持的,最终重复请求的Resp返回结果一定是成功且上游无额外感知的,对上游调用没有任何理解成本。
req
method
logic
mysql
resp
req
req
数据插入
幂等数据构造
ack
ack
req
method
logic
mysql
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() {
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()
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()
assert.Equal(t, 1, dbQueryMock.MockTimes())
assert.Equal(t, 0, otherMock.MockTimes())
assert.Equal(t, int32(0), resp.GetBaseResp().GetStatusCode())
})
}
??如下,是一个根据上游单号BizNo字段进行单据业务在Redis层请求幂等的拦截,只是数据源构造不同,逻辑和目标和上一个例子是异曲同工的。
req
method
logic
redis
resp
req
req
数据写入
幂等数据构造
ack
ack
req
method
logic
redis
resp
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() {
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()
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()
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设计主要是面向构造部分锁定失败、部分锁定成功的数据,并且要对释放锁逻辑进行严格判断。
req
method
logic
redis
resp
req
req
批量KEY锁定
部分KEY锁定成功
部分KEY锁定失败
alt
[lock]
已锁定的释放逻辑
验证释放KEY有效性,错乱情况
ack
alt
[unlock]
ack
ack
req
method
logic
redis
resp
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() {
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()
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))
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设计可以在主进程中等待所有子进程执行完毕再进行验证判断,否则可能子进程没有执行完毕但主进程执行完毕导致判断错误的情况。
req
method
logic
resp
req
req
handler - 1
handler - 2
alt
[子线程]
handler - 3
alt
[执行过程]
ack
ack
WAIT
验证结果/执行次数
req
method
logic
resp
func TestSecKillHandler_Run_ReportLog_Fail(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestSecKillHandler_Run_ReportLog_Fail", t, func() {
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()
handler := NewSecKillHandler(context.Background(), &model.SecKillReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "U123",
})
handler.Run()
time.Sleep(3 * time.Second)
assert.Equal(t, 1, reportLogRpcMocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
2.2.4 数据库事务
??当多个表同时参与数据库事务时,可以设计某个表异常执行验证结果。
req
method
logic
mysql
resp
req
req
写操作
构造异常
order表
写操作
ack
flow表
alt
[tx]
fail
ack
验证结果/执行次数
req
method
logic
mysql
resp
txErr := db.GetTransProvider().Transaction(c.Ctx, func(ctx context.Context) error {
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
}
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
}
func TestCreateOrderHandler_Run_Tx_Err(t *testing.T) {
dal.Init()
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:
t.Fatal("不可进入")
}
return db
}).Build()
handler := NewCreateOrderHandler(context.Background(), &model.CreateOrderReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "USR123",
})
handler.Run()
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) {
dal.Init()
mockito.PatchConvey("TestQueryOrderListHandler_Run", t, func() {
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])
time.Sleep(10 * time.Second)
return []*model.GoodsInfo{
{
SkuCode: "SKU123",
SkuName: "商品123",
},
}, errno.NewCodeErrorWithMessage(errno.Internal_Error, "goods rpc err")
}).Build()
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()
handler := NewQueryOrderListHandler(context.Background(), &model.QueryOrderListReq{
OrderNo: "ORD123",
})
handler.Run()
assert.Equal(t, 1, queryGoodsInfoRpcMocker.MockTimes())
assert.Equal(t, 2, queryDbMocker.MockTimes())
assert.Equal(t, int32(errno.Internal_Error), handler.Resp.Code)
})
}
2.2.6 重复调用
??当前引入类似retry.Do()方法可以设计case来构造异常触发重试,通过严格的计数统计辅助验证执行次数和逻辑正确性。
req
method
logic
resp
req
req
err
1 times
err
2 times
ok
loop
[retry最多5次]
ack
ack
验证结果/执行次数
req
method
logic
resp
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) {
dal.Init()
mockito.PatchConvey("TestCreateOrderHandler_Run", t, func() {
cnt := 0
CreateOrderMocker := mockito.Mock((*gorm.DB).Create).To(func(db *gorm.DB, value interface{}) *gorm.DB {
switch value.(type) {
case *model.Order:
if cnt == 1 {
return db
}
cnt++
db.Error = errno.NewCodeErrorWithMessage(errno.Internal_Error, "err")
}
return db
}).Build()
handler := NewCreateOrderHandler(context.Background(), &model.CreateOrderReq{
SkuCode: "SKU123",
SkuNum: 1,
UserId: "USR123",
})
handler.Run()
pretty.Println(handler.Resp)
assert.Equal(t, 2, CreateOrderMocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
3、UT编写格式
单函数测试文件示例如下:
func TestHandler_Run_Mock_Case_1(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestHandler_Run_Mock_Case_1", t, func() {
mocker := mockito.mock(rpc.call).To(func(ctx context.Context,req interface{})error{
assert.Equals(t,'req',req)
}).Build()
handler := NewTestHandler(context.Background(), &model.Req{
})
handler.Run()
pretty.Println(handler.Resp)
assert.Equal(t, 1, mocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
func TestHandler_Run_Mock_Case_2(t *testing.T) {
dal.Init()
mockito.PatchConvey("TestHandler_Run_Mock_Case_2", t, func() {
mocker := mockito.mock(rpc.call).To(func(ctx context.Context,req interface{})error{
assert.Equals(t,'req',req)
}).Build()
handler := NewTestHandler(context.Background(), &model.Req{
})
handler.Run()
pretty.Println(handler.Resp)
assert.Equal(t, 1, mocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
func TestHandler_Run_Case_1(t *testing.T) {
dal.Init()
handler := NewTestHandler(context.Background(), &model.Req{
})
handler.Run()
pretty.Println(handler.Resp)
assert.Equal(t, 1, mocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
func TestHandler_Run_Case_2(t *testing.T) {
dal.Init()
handler := NewTestHandler(context.Background(), &model.Req{
})
handler.Run()
pretty.Println(handler.Resp)
assert.Equal(t, 1, mocker.MockTimes())
assert.Equal(t, int32(errno.SUCCESS), handler.Resp.Code)
})
}
|