干货分享 | UT case设计与实战

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
日志服务 SLS,月写入数据量 50GB 1个月
简介: case设计原则、面向工程结构设计、case设计思路、一般通用设计、入参验证、过程数据验证、最终结果验证、数据有效性验证


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 最终结果验证


对最终输出数据Respcode、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)

       })

}

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
kettle开发篇-switch case
kettle开发篇-switch case
197 0
kettle开发篇-switch case
|
3月前
|
Go
Go 重构:尽量避免使用 else、break 和 continue
Go 重构:尽量避免使用 else、break 和 continue
|
2月前
|
存储 算法
RT-mutex 实现设计【ChatGPT】
RT-mutex 实现设计【ChatGPT】
|
数据可视化 测试技术 Go
ChIP-seq 分析:GO 功能测试与 Motifs 分析(12)
ChIP-seq 分析:GO 功能测试与 Motifs 分析(12)
282 0
|
测试技术 API 数据库
如何设计自动化测试Case?
测试工作的本质是尽可能以更高的效率保障交付产出物的质量满足甚至超出预期,这是所有测试工作的最终目标。
如何设计自动化测试Case?
|
存储 数据可视化 Linux
ChIP-seq 分析:教程简介(1)
[本课程](https://rockefelleruniversity.github.io/RU_ChIPseq/ "Source")介绍 Bioconductor 中的 ChIPseq 分析。该课程由 4 个部分组成。这将引导您完成正常 ChIPseq 分析工作流程的每个步骤。它涵盖比对、QC、`peak calling`、基因组富集测试、基序富集和差异 ChIP 分析。
296 0
|
程序员 Go
Go pointer & switch fallthrough 详解和实战
首先说明pointer指针和switch是两个并没有直接关系的知识点,放在一篇文章中将的原因是,这两个知识点在学习和使用的过程中往往被大家忽视。
104 0
Go pointer & switch fallthrough 详解和实战
|
存储 开发工具
CASE 工具有哪些
<h2 style="color:rgb(18,18,20); font-weight:normal; letter-spacing:-1px; margin:0.2em 0.2em 0.2em 0px; font-size:1.7em; line-height:1.5em; padding:0px; position:relative; left:0px; font-family:Ver
3716 0
|
算法 编译器 C语言
if else 和 switch的效率
switch在判断分支时,没有判断所有的可能性,而是用一个静态表来解决这个问题,所以速度要比if-else快。 但是,switch对较复杂的表达式进行判断,所以当我们需要判断一些简单数值时,用switch较好。
3416 0
封装一个RxCondition,告别if else和switch case
封装一个RxCondition,告别if else和switch case
123 0