IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> UT case设计与实战 -> 正文阅读

[大数据]UT 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地址:

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) {
   //【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不够健壮、无法暴露问题的情况,此外还可以协助验证调用链路上下游依赖、数据传递等逻辑正确性。

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) {
   //【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执行验证确保符合预期调用,尤其是存在逻辑分叉的业务中可以验证执行链路的路由准确性

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) {
   //【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进行有效性校验构造设计

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) {
   //【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返回结果一定是成功且上游无额外感知的,对上游调用没有任何理解成本。

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() {
                //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层请求幂等的拦截,只是数据源构造不同,逻辑和目标和上一个例子是异曲同工的。

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() {
                //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设计主要是面向构造部分锁定失败、部分锁定成功的数据,并且要对释放锁逻辑进行严格判断。

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() {
                //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设计可以在主进程中等待所有子进程执行完毕再进行验证判断,否则可能子进程没有执行完毕但主进程执行完毕导致判断错误的情况。

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) {
   //【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 数据库事务

??当多个表同时参与数据库事务时,可以设计某个表异常执行验证结果。

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 {
   //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来构造异常触发重试,通过严格的计数统计辅助验证执行次数和逻辑正确性。

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) {
   //【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)
        })
}
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-04-23 10:53:19  更:2022-04-23 10:56:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:09:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码