Case

1、case设计原则

1.1 面向工程结构设计

这里是一个比较通用的工程结构目录示例,具体项目会有些许差异,但核心思想是相同的
notion image
工程目录结构决定了代码各层级职责和作用关系,推荐面向整体工程目录结构进行全局的case设计,进而梳理哪些模块需要进行测试,哪些需要重点关注进行复杂而周全的用例,哪些可以简单设计等,这里将工程目录划分为两部分:
  • 核心逻辑部分 核心逻辑部分负责串联业务的故事主线,包括*数据访问层(dal)、业务逻辑层(logic)、接口定义层(method)、远程调用层(rpc)、工具包(util)、消息队列(mq) *等
  • 数据支撑部分 数据支撑部分一般为无逻辑、无状态的数据承载传递,包括*配置文件(conf)、常量枚举(consts)、数据对象(model) *等

1.2 围绕函数组织构建

notion image
如上图,函数执行过程从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 争取质量效率平衡

notion image
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 入参验证

传入异常参数触发校验代码逻辑,必填项、数量限制、长度、边界值、可接受的枚举类型等等。

2.1.2 过程数据验证

对于函数执行过程中产生的过程数据进行预期验证,将复杂逻辑拆解、细化到每一个子逻辑单元进行,一方面可以提高case验证的颗粒度和透明度,其次可以避免某些测试数据的最终结果符合预期、但过程逻辑错误导致编写case不够健壮、无法暴露问题的情况,此外还可以协助验证调用链路上下游依赖、数据传递等逻辑正确性。

2.1.3 最终结果验证

对最终输出数据Respcode、msg、error等进行预期验证,对mocker执行验证确保符合预期调用,尤其是存在逻辑分叉的业务中可以验证执行链路的路由准确性

2.1.4 数据有效性验证

对于业务数据一定要对其有效性进行校验,数据源一般来源于本地存储、远程调用服务等,可以对数据进行适度构造来验证非法或无效数据对逻辑的影响和破坏性,如下例子是对入参商品编码、用户ID进行有效性校验构造设计

2.1.5 异常验证

异常构造的场景很多,比如RPC调用、数据库访问、MQ发送等的error返回设计,验证对异常处理的健壮性,能否对异常情况进行合理响应处理

2.2 复杂逻辑设计

这里收集了一部分项目实战中场景来分别论述下

2.2.1 业务幂等

一般业务幂等是通过Redis数据检查、数据库唯一索引进行实现的,因此可以基于此进行数据构造来验证逻辑。
如下,是一个根据上游单号BizNo字段进行数据库层单据业务幂等的示例,这里也涉及到一个OrderStatus状态机字段来决定是否重复发起对下游业务的请求的case设计,幂等逻辑是需要当前服务消化和支持的,最终重复请求的Resp返回结果一定是成功且上游无额外感知的,对上游调用没有任何理解成本。
如下,是一个根据上游单号BizNo字段进行单据业务在Redis层请求幂等的拦截,只是数据源构造不同,逻辑和目标和上一个例子是异曲同工的。

2.2.2 分布式锁竞争

这里示例一个分布式锁产生竞争的case,背景假设为函数一次请求要批量对多个商品进行锁定处理,但是单个商品同一时间又只能被一笔业务请求操作使用,如果在处理过程中有商品被锁定需要进行拦截。这部分的case设计主要是面向构造部分锁定失败、部分锁定成功的数据,并且要对释放锁逻辑进行严格判断。

2.2.3 异步逻辑

模拟异步执行逻辑的耗时操作,case设计可以在主进程中等待所有子进程执行完毕再进行验证判断,否则可能子进程没有执行完毕但主进程执行完毕导致判断错误的情况。

2.2.4 数据库事务

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

2.2.5 多线程

调用方法中存在使用多线程并行的话,需要考虑执行超时异常、子线程逻辑异常等情况的case覆盖。

2.2.6 重复调用

当前引入类似retry.Do()方法可以设计case来构造异常触发重试,通过严格的计数统计辅助验证执行次数和逻辑正确性。

3、UT编写格式

单函数测试文件示例如下:
作者:大摩羯先生链接:https://juejin.cn/post/7089095801388924965来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP