自动化工具 货拉拉营销自动化框架 Mutation 演变与升级之路

货拉拉质量星火 · 2024年10月31日 · 267 次阅读

作者:质量保障部增长中台测试组自动化专项成员出品
贡献者: 张丙振、蔡辉、侯佳刚、伍菊红、蒋竹茹、康好龙、卢华阳、周光远、晋超颖

引言

"架构设计的艺术在于找到最简单的解决方案来满足业务需求。" —— John Gall
“Mutation”:含义为 “突变”,指基因序列的改变,这种改变可能导致生物体特性的变化。这个名称寓意着框架能够适应和应对营销领域不断变化的接口和系统需求,具有高度的灵活性和适应性。


自动化全景图(mutation)

背景与挑战

众所周知,接口自动化在质量保障体系中起着至关重要的作用,不仅能提高测试效率,还能显著降低人力成本。它是一种高效且简洁的手段。那么,如何在这一通用且朴实的方法基础上,结合具体业务需求,构建一个适用于营销领域的框架,以解决我们面临的独特问题呢?
营销作为推动业务增长的重要手段,通过各种丰富的活动、任务、奖励派发和补贴等方式,帮助企业实现市场目标并提升竞争力。目前,营销手段包括优惠券(如满减券、折扣券)、积分、红包、实物奖励以及第三方权益等,支持了各类营销与运营场景。其特点如下:

  1. 时效性:营销活动大多具有阶段性和一次性特点,通常伴随着大促或不同力度的活动。每次测试的页面或功能玩法往往是新开发的逻辑。

  2. 灵活性与频繁变更:由于市场需求和竞争环境的变化,营销策略和活动需要频繁更新和调整。支持车型的差异化配置能力,不同用户画像使用的营销策略和投放奖励内容形式多样,底层逻辑路径也各不相同。

  3. 资损:优惠券、奖励派发、补贴和实物直接发放给用户,如果发放过多或错误,将直接导致资金损失和舆情事件。

面对营销如此特点,测试如何在最短的时间覆盖的更全,做到 “好又快”, 接下来我们将结合 业务发展的三个重要阶段详细探讨 “烟囱时代->中台化->稳定时代” 时期,自动化框架的演变与升级过程

一:V1.0 烟囱时代:“构建基础自动化框架”

1.1 烟囱时代的挑战

在初期建设阶段以 “快” 为目标,系统基本呈现出了烟囱式架构的特点。由于业务需求的多样性,各功能模块存在一定差异,导致定制化开发较多,配置和接入流程过于繁琐,重复建设和维护的现象频繁出现。对于测试团队来说,每次业务迭代都需要投入大量的测试成本,面对庞大的测试数据、灵活的场景组合和频繁的变更,挑战尤为突出。
此时,接口自动化框架可以对这些新功能进行全面的自动化测试,确保其逻辑正确、功能完备。在上线前进行自动化回归,可以发现潜在的问题,从而减少上线后的风险。

1.2 框架需要解决问题

  • 快速响应和验证:由于营销活动通常具有阶段性,并伴随着大促和不同的活动力度,我们需要接口自动化能够快速响应并支持验证各种营销玩法类型。通过自动化测试,我们期望在短时间内验证大量的营销玩法,确保每个阶段的营销活动都能顺利进行。

  • 灵活适应变化:阶段性营销活动的需求和功能可能会频繁变化,因此接口自动化框架需要具备快速适应这些变化的能力。我们需要抽象出适合复用和重组的结构,确保每次变化后的功能都能正常运行。

  • 数据驱动优化:无论营销玩法如何改变,营销的核心流程如奖励发放、任务完成和任务领取等行为是相似的。我们需要将营销玩法的数据进行结构化和组件化,以便更好地进行优化和管理

1.3 框架设计的亮点

  • 方便协作和维护:格式统一,约定大于配置,component/ flow 是标准结构化

  • 高复用:支持与多种工具对接,并转换成 component 和 flow,一次投入可以同时支持自动化测试、造数、核心场景链路验证等多种测试需求

  • 插件化:支持 http、soa、kafka、mysql,并提供可扩展插槽

  • 模板化 & 参数化:针对 json 支持模板化、参数化、继承等特性

  • 灵活性强:支持多种数据提取方式以及多格式转换,如:正则表达式/Jsonpath/Jmespath/对象提取,支持 json、yaml、xml 等多种格式转换;内置表达式引擎,支持 Aviator 和 BeanShell 脚本,能轻松实现复杂动态业务逻辑

  • 支持集成和持续交付(CI/CD:接口自动化框架可以集成到 CI/CD 流水线中,支持快速迭代和持续交付。

  • 集成 pict:数据驱动解决大量营销配置类型的验证

1.4 框架详情介绍

1.4.1 分层设计

  通过分层设计,可以提高测试框架的可维护性、可扩展性和可读性。以下是我们当前框架的分层结构。

  • a. 组件层,用于单个组件的定义,不局限于 api、消息、DB 等中间件,亦或能抽象成组件的。
    • 接口定义
      接口定义描述有:componentName、url、operation、request(headers、queryParams、cookies、bodyType、body)
{
   "componentName": "Just test-genorderids",
   "url": "${trade4showcase.tradeplatformUrl}/order",
   "operation": "POST",
   "request": {
      "headers": {},
      "queryParams": {},
      "cookies": {},
      "bodyType": "JSON",
      //example JSON OR FORM
      "body": {
         "method": "genOrderIds",
         "jsonrpc": 1,
         "id": 1,
         "params": [
            ${userId},
            ${genOrderIdsRequest}
         ]
      }
   }
}
    • 组件实现
@Override
@LLRequestSpec(value = "classpath:json/trade4showcase/createOrder.json")
public void createOrder(LLContext context) {
    execute(context);
}
    • 非接口组件
public interface PreposeService {
    // 替换JSON中文件
    void externalFileReplace(LLContext context);
    // 获取token
    void getSSOToken(LLContext context);
    // 获取当前环境
    void getCurrentEnv(LLContext context);

    void triggerJob(LLContext context);
}
  • b. 业务流程层,具有具体业务含义,如下单流程,由生成订单号、创建订单、enable 订单、订单回调、订单查询这几个组件组成。
    • flow 实现
@Override
public void showcaseOrder(LLContext context) {
    // get current env
    preposeService.getCurrentEnv(context);
    // before get token
    preposeService.getSSOToken(context);
    // 处理JSON文件依赖
    preposeService.externalFileReplace(context);
    // 生成订单号
    tradePlatform4ShowCaseService.genOrderIds(context);
    // 创建订单
    tradePlatform4ShowCaseService.createOrder(context);
    // enable订单
    tradePlatform4ShowCaseService.enableOrder(context);
    // 查询订单
    tradePlatform4ShowCaseService.queryOrderById(context);

}
  • c. 测试用例层,一个包含测试场景、数据准备、调用场景、断言的标准自动化测试用例。
/**
 * 场景:下单主流程
 * steps:
 * genOrderIds
 * createOrder
 * enableOrder
 * queryOrderById
 */
@Test(dataProvider = "data4ShowCaseTestSync", dataProviderClass = TradePlatform4ShowCaseDataProvider.class)
public void testOrderWithDataProvider(String userId) {
    // 数据准备 & 构造context
    LLContext context = new LLContext();
    context.getInput().put("userId", userId);
    context.getReplaceMap().put("genOrderIdsRequest", "JSON.FILE:json/trade4showcase/genOrderIds_request.json");
    context.getReplaceMap().put("subOrderInfos", "JSON.FILE:json/trade4showcase/subOrderInfos.json");
    // 调用场景
    order4ShowCaseFlow.showcaseOrder(context);
    // 从上下文获取断言信息
    String orderInfo = (String) context.getOutput().get("queryOrderById_result");
    String orderId = (String) context.getOutput().get("genOrderIds_orderId");
    String actualOrderId = JsonPathUtils.getResult(orderInfo, "$.result.data.orderId");
    String actualUserId = JsonPathUtils.getResult(orderInfo, "$.result.data.buyerId");
    // 断言
    assertThat(actualOrderId).as("校验订单号").isEqualTo(orderId);
    assertThat(actualUserId).as("校验用户信息").isEqualTo(userId);

}
  • d. Factory 层,定义为业务数据工厂类,如 dataprovider 入参构造。构造源头有 qatools 和 DB。
public static String getDriverFidId() {
    GetDriverFidId getDriverFidId = new GetDriverFidId();
    apiReModule a = getDriverFidId.handle();
    return a.getData().toString();
}

public static String getOrderId() {
    return new JdbcTemplate(staticSataSource4OpsOrderSharding).queryForObject("select order_id from order_info_0 where buyer_id='19915511' limit 1;", String.class);
}

1.4.2 模块化实现

  • a. 模板化,组件请求模板化(JSON/Yaml),抽象出 componentName、url、operation、request 对象等。
{
   "componentName": "Just test-genorderids",
   "url": "${trade4showcase.tradeplatformUrl}/order",
   "operation": "POST",
   "request": {
      "headers": {},
      "queryParams": {},
      "cookies": {},
      "bodyType": "JSON",
      //exapmle JSON OR FORM
      "body": {
         "method": "genOrderIds",
         "jsonrpc": 1,
         "id": 1,
         "params": [
            ${userId},
            ${genOrderIdsRequest}
         ]
      }
   }
}

这里的请求不局限于 http 接口,也可以是 kafka 消息,亦或是 sql。拿 kafka 举例,url 是 topic ,operation 是 produce、send、load 等。

{
   "componentName": "Just test kafka",
   "url": "this is kafka topic",
   "operation": "produce",
   "request": {
      "async": true,
      "records":[
            {
                "key": "hello world",
                "value": "hello kitty"
            }
        ]
   }
}
  • b. 参数化,通过@LLRequestSpec注解实现,实现过程会将 context 中 input 和 output 合并成一个 map,然后逐个替换用例 json 文件中的参数。简单来说就是 context 中 map(input+output)的 ${key} 都可用于 json 文件中关键字的替换

方式一,关键字替换

{
   "componentName": "getActiveCouponList",
   "operation": "POST",
   "request": {
      "headers": {},
      "queryParams": {
         "_m": "coupon_api",
         "_a": "get_active_coupon_lists"
      },
      "cookies": {},
      "bodyType": "FORM",
      //exapmle JSON OR FORM
      "body": {
         "_args": {
            "user_id": "${userId}",    //被替换成11156
            "account_type": "${accountType}"    // 被替换成3
         }
      }
   }
}

方式二,文件替换

context.getReplaceMap().put("genOrderIdsRequest", "JSON.FILE:json/trade4showcase/genOrderIds_request.json");
context.getReplaceMap().put("subOrderInfos", "JSON.FILE:json/trade4showcase/subOrderInfos.json");

  • c. 集成 pict:通过 dataProvider 对需要被验证的数据进行入参管理 通过 pict 把配置类型枚举值生成写入到 csv 文件中:RedPackageTask.csv
@Test(dataProvider = "getRedPackageTaskData", dataProviderClass = RedPackageTaskDataProvider.class)



@Component
@Slf4j
public class RedPackageTaskDataProvider {

    private static final String RedPackageTask_FILE = "config/usertask/RedPackageTask.csv";

    @DataProvider(name = "getRedPackageTaskData")
    public static Object[] getRedPackageTaskData() {
        Object[] datas = null;
        //读文件
        try {
            InputStream inputStream = RedPackageTaskDataProvider.class.getClassLoader().getResourceAsStream(RedPackageTask_FILE);
            List<String> dataList = IOUtils.readLines(inputStream);
            List<String> heads = Splitter.on(",").splitToList(dataList.get(0));
            dataList.remove(0);
            datas = new Object[dataList.size()];
            int index = 0;
            //取数据
            for (String data : dataList) {
                List<String> line = Splitter.on(",").splitToList(data);
                Map<String, Object> map = Maps.newHashMap();
                for (int i = 0; i < line.size(); i++) {
                    map.put(heads.get(i), line.get(i));
                }
                datas[index++] = map;
            }
        } catch (Exception ex) {
            log.error("getRedPackageTaskData error", ex);
        }
        return datas;
    }
}

1.5 收益

  1. 质量效率提升:每个需求节约回归效率提升 20%;发现 bug 数 178 个 bug(2 年时间内);

  2. 覆盖情况:截至目前,我们的接口自动化测试总数已经达到了5756个,这些测试覆盖了系统的主要功能和关键业务流程,确保了系统的稳定性和可靠性;其中测试环境全链路 case 数量 74 个,生产环境全链路 case 数量 101 个;

  3. 软性提升:加深理解业务逻辑和需求,编写更高质量的测试用例,进一步提升编程技能,提高问题定位和调试能力

二: V2.0 中台化时代:“升级框架,聚焦营销降低成本”

2.1 中台化的挑战

业务中台化的主要目的是对业务整体流程进行管控。然而,对于测试来说烟囱式架构存在大量与烟独立且业务逻辑复杂的测试用例,独立性很强。然而,如果中台化将共性的服务抽象出来,形成通用能力,将大大提升研发效率。但是中台化抽象后接口发生改变时,自动化测试用例往往会大面积失效,无法复用。链路变长,数据的稳定性也面临严峻挑战。此外,中台化带来的研发成本和复用速度的变化,对测试的时效性提出了巨大的挑战。测试团队需要找到更快、更灵活的解决方案,以应对这些变化。

2.2 框架需要解决的问题

  • 功能抽象:用例功能都是领域独立,无法大量复用,外部依赖不统一,我们需要对营销链路组件进行抽象,对外部对接进行统一简化测试链路,来提高稳定性和可维护性

  • 执行链路优化:用例执行链路长、步骤多、高耦合,我们需要降低长链路、强关联、强依赖

  • 数据保鲜与成本控制:通过数据池和自动生成数据机制保持数据新鲜,降低测试数据过期风险

  • 代码编写成本:通过自动生成组件代码降低编写成本如 json 或者 component

2.3 自动化设计的特点

  1. 基于策略对共性业务模型进行分类:通过不同策略模式对自动化进行分层。举例说明营销行为的触发是通过用户行为事件进行触发(完单、估价、下单、完单、接单等),营销派发(满足预制条件与规则等),不同种类奖励下发(优惠券、会员券、会员卡、加油券、加油金、现金等)。

  2. 数据保鲜:搭建数据池,数据每天按照定义规则,自动生成;数据来源主要来自数据工厂、大数据、流量回放等,自动化涉及到人群相关来源数据池,保障了数据的可靠性

  3. 底层代码自动生成:对所有被测试对象代码自动生成,无论增量、全量接口自动生成组件代码

  4. 组件能力下沉:废弃 flow 层硬编码组装场景的模式,在 case 层直接串联组件,组件化基础能力(前置处理器、校验器、原子请求、后置处理器)

2.4 架构详情介绍

2.4.1 策略模型结构

Flow 层
策略接口 MarketingStrategy
策略接口下定义自动化分层方法 create、filter、doSomeThing

Component 层
业务领域接口实现
业务接口定义实现,即自动化原来的组件接口实现定义方式。

断言
位于 Component 组件层,分为策略模型断言、差异性断言两类。
Test 层

  1. 数据准备,参数传入

  2. 调用业务执行 flow

  3. 执行结果存储,用例断言

例子:

2.4.2 数据保鲜

搭建数据池,数据每天按照定义规则,自动生成;数据来源主要来自数据工厂、大数据、流量回放等,自动化涉及到人群相关来源数据池,保障了数据的可靠性

2.4.3 代码自动生成组件

  • OSA 规格的数据类型自动生成接口测试代码

  • 多种模板配置支持不同代码风格的框架生成

  • 策略执行支持可配置化的代码自动生成

代码生成流程

代码自动生成实现方式是与 mutation 项目交互,自动生产不同分层的代码文件,我们把代码生成拆解为以下步骤

1、源数据获取:

    通过 ldoc 提供的对外接口获取指定 appid 的接口信息 :判断当前 appid 下的服务语言类型;支持获取一个 appid 下的所有接口信息;支持获取一个 appid 下、指定协议类型的接口信息:支持获取一个 appid 下、指定方法的接口信息

2、源数据解析:将 ldoc 源数据通过 ldoc 解析框架解析为自定义格式的元数据

  • 解析 easyopen 类型的接口数据

  • 解析 soa 类型的接口数据

  • 解析 restful 类型的接口数据

3、元数据适配 mutation
将元数据通过模版引擎生成适配于 mutation 项目的接口 json、component:支持全量、增量、指定协议、指定方法

4、配置项支持 maven 插件化

  • 配置数据来源:

    • 数据来源接口:ldoc
    • 指定服务名称:appId
    • 具体分支:指定某分支,默认最新分支
  • 配置文件生成路径:配置不同文件相对路径:json、component、flow、testcase

执行流程

1、代码生成的项目打包成 jar 文件被 mutation 项目所引入和使用

gen-1.0.5-SNAPSHOT.jar

2、通过 yml 管理需要被本地代码生成的策略

入参 说明
genApiType 生成的接口类型
1、all 生成当前服务下全量的接口数据
2、 increment 生成当前服务下增量的接口数据
3、 protocolType 生成当前服务下指定协议类型的接口数据
4、methods 生成当前服务下指定方法(支持多个)的接口数据
genApiTypeValue 生成的具体接口数据
1、 当 genApiType=all、或increment,genApiTypeValue 无需填写、或直接填写""即可;
2、 当 genApiType=protocolType,genApiTypeValue 填如下的协议类型:soa、restful、easyopen
3、 当 genApiType=methods,genApiTypeValue 填写具体的方法,需参考接口文档提供的方法 path,例如:
  "/?name=act_strategy&version=get_settings,subsidy/strategy/getOrderSubsidy@"
具体规则:
1、可填写 1 个、或多个方法
2、多个方法之间用英文逗号分隔
jsonPath json 文件路径,例如:
jsonPath: "../mutation-core/src/main/resources/json/intelligentoperation/ic"
classPath j son 文件路径,例如:
jsonPath: "../mutation-core/src/main/resources/json/intelligentoperation/ic"
className 组件接口名,例如
className: "IcService"

配置举例:

ops-user-discount-svc:
  genApiType: "methods"
  genApiTypeValue: "/?name=act_strategy&version=get_settings,subsidy/strategy/getOrderSubsidy@"
  jsonPath: "../mutation-core/src/main/resources/json/intelligentoperation/subsidy"
  classPath: "../mutation-core/src/main/java/cn/huolala/mutation/component/intelligentoperation/subsidy"
  className: "TestLu"
  1. 通过执行 shell 脚本下发被执行命令 进入 mutation 项目终端,执行如下的命令:
sh run.sh ops-user-discount-svc 

执行完成后检查生成的组件 json 文件、组件接口、组件实现类是否生成成功。
以 ops-user-discount-svc 服务为例,按照如上的两步执行后,生成的 3 个文件如下:
【demo-json 文件】

【demo-组件 - 接口定义】

【demo-组件 - 接口实现类】

2.4.4 组件能力下沉

废弃 flow 层硬编码组装场景的模式,在 case 层直接串联组件,每个组件包含前置处理器、校验器、原子请求以及后置处理器,case 层只需要按照自己需要验证的场景,灵活去组装组件.
组件:指的是最底层的原子请求,比如登陆事件、访问活动、完单等
整体设计如下:

最终呈现模式:case 层只需要串联各个场景模块,代码可读性高,清晰明了

链式处理器
组件层:

部分核心代码:

核心处理器

2.5 收益

  1. 成本收益:自动化编写效率提升 300%,平均每个需求新增自动化编写成本节省 0.75d;维护成本由小时级别降低至分钟级别

  2. 稳定性收益:自动化稳定性提升 30%:经过数据保鲜后,进一步提高用例所依赖测试数据的可靠性和完整性,降低数据生命周期管理时间与降低造数决策的成本

三:V3.0 稳定时代:“扩展框架,覆盖应急&资损”

3.1 业务特点与挑战

随着公司业务的快速增长和迭代频率的增加,业务场景变得愈加复杂,出现资金损失风险的概率也随之上升。因此,及时发现问题并迅速采取措施变得尤为重要。过去,由于系统原因或人为因素,平台时常发生资金损失和财务数据不一致的问题。如果无法在第一时间发现并解决这些问题,将会带来巨大的经济损失。因此,保障资金安全的工作已经刻不容缓。
特别是在处理资金损失问题&系统出现异常时,测试团队能否第一时间验证结果,并从业务视角评估系统的健康状况,显得尤为关键。通过快速、准确的测试反馈,确保系统的稳定性和资金的安全性,是当前工作的重中之重。

3.2 框架需要解决的问题

  1. 自动化的安全性
    在生产环境中执行自动化测试时,涉及到大量的写操作,如活动、策略和用户画像等。如何确保这些自动化操作不会影响生产数据的完整性和安全性,是一个重要的挑战。需要制定严格的策略和措施,确保自动化测试在生产环境中的安全性。

  2. 快速定位问题
    线上 oncall 的反馈需要高度的实时性。如果能够在 1 分钟内完成链路级别的验证,并将结果快速返回给操作人员,将大大提升问题定位和追溯的效率。这种快速响应能力对于保障系统的稳定性和用户体验至关重要。

  3. 稳定性问题
    在进行线上验证时,如果频繁出现类似测试环境中的数据失效、token 过期、代码变更或脚本错误等问题,将严重影响 oncall 的公信力。因此,确保测试环境的稳定性和一致性,及时更新和维护测试脚本,是提升自动化测试可靠性的关键。

3.3 自动化设计特点

断言追溯组件

  • 失败的断言自动解析,对失败原因自动聚合分析

  • 脚本异常时提供 trace 进行链路级追踪

  断言追述实现

1、通过自定义注解收集用例信息

  @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation {

    String actId() default "";

    String taskId() default "";

    String descption() default "";

    String remark() default "";

    String author() default "";
}

2、通过自定义断言收集断言信息

  • 断言 actual 大于 expected、断言 object 为空、断言 object 非空、断言 content 包含 expected 内容、断言 content 不包含 expected 内容、断言 actual 和 excepted 值相等、断言 actual 和 excepted 不相等
  @Test
@CustomAnnotation(author = "charles.kang",descption = "优惠券:小B订单获取最优券&可用券列表")
public void testFreightQueryCouponP() {
    userContext.getInput().put("orderVehicleId", 3202);
    useCouponFlow.xbGetBestCouponFlow(userContext);
    useCouponFlow.xbGetOrderCouponListFlow(userContext);     
    CustomAssert.assertEqual(userContext.getInput().get("ret"),"0",userContext,"验证请求200");
}

/**
 * 断言actual和excepted值相等
 *
 * @param actual   实际值
 * @param expected 预期值
 * @param context  LLContext
 * @param desc     断言描述
 * @Throw AssertionError
 */
public static void assertEqual(Object actual, Object expected, LLContext context, String... desc) {
    putAuthor(context);
    AssertEquals.assertEqualsImpl(actual, expected, getSendMessage(context), context, desc);
}

3、断言数据收集模块

  • 获取测试类、方法、作者、入参、trace 基础信息作用
     private static String getSendMessage(LLContext context) {
        String messageInfo = "";
        try {
            String parentMethodName;
            //这里不知道断言会加在哪里,可能在component,也可能在flow层,向外循环,直到发现有命名是*test*的方法
            int count = 0;
            do {
                ++count;
                parentMethodName = Thread.currentThread().getStackTrace()[2 + count].getMethodName();
                //忽略大小写
            } while (!parentMethodName.toLowerCase().contains("test"));
            //可能是在lamb表达式里断言
            if (parentMethodName.contains("$")) {
                parentMethodName = parentMethodName.replace("lambda$", "");
                parentMethodName = parentMethodName.substring(0, parentMethodName.length() - 2);
            }

            String parentClassName = Thread.currentThread().getStackTrace()[2 + count].getClassName();

            Method method = null;
            try {
                method = Class.forName(parentClassName).getDeclaredMethod(parentMethodName, null);
            } catch (NoSuchMethodException noSuchMethodException) {
                //有些人使用dataProvider的写法,test方法是有参数的
                Method[] methods = Class.forName(parentClassName).getDeclaredMethods();
                if (Objects.nonNull(methods) && methods.length > 0) {
                    for (Method withParaMethod : methods) {
                        if (withParaMethod.getName().equalsIgnoreCase(parentMethodName)) {
                            method = withParaMethod;
                        }
                    }
                }
            }

            method.setAccessible(true);

            String desc = "";
            String reMark = "";
            String testCaseAuthor = "";

            String currentErrorLine = getCurrentLine(3);
            if (method.isAnnotationPresent(CustomAnnotation.class)) {
                desc = method.getAnnotation(CustomAnnotation.class).descption();
                reMark = method.getDeclaredAnnotation(CustomAnnotation.class).remark();
                testCaseAuthor = method.getDeclaredAnnotation(CustomAnnotation.class).author();
            }
            String testMethodName = method.getName();
            String currentParameter = "";
            String monitorTraceId = "";

            if (Objects.nonNull(context)) {
                currentParameter = (String) context.getOutput().get("currentParameter");
                monitorTraceId = (String) Optional.ofNullable(context.getOutput().get("x-hll-trace")).orElse("");
            }

            String groupName = systemConfiguration.getGroupName();
            if (StringUtils.isBlank(groupName)) {
                groupName = DEFAULT_GROUP_NAME;
            }

            //其他字段从yml文件读取,根据不同的群组展示发送不同的字段
            Map<String, Map<String, String>> ymlGroupAndFields = (Map) getValueForKey("fields", NEED_TOAST_FIELD);
            //key-当前组名,values-当前组名需要展示的字段  {自动化告警={traceId=traceId, userId=userId, inviterId=邀请者id, inviteeId=被邀者id, actId=活动id, taskId=任务id}}
            Map<String, Map<String, String>> currentGroupAndFields = Maps.newHashMap();
            if (CollectionUtil.isEmpty(ymlGroupAndFields) || StringUtils.isEmpty(groupName)) {
                log.warn("ymlGroupAndFields or groupName is null, please check toast_filed.yml and qa-ci config!");
                return messageInfo;
            }

            String finalGroupName = groupName;
            ymlGroupAndFields.forEach(
                    (ymlGroupName, ymlField) -> {
                        if (ymlGroupName.equals(finalGroupName)) {
                            currentGroupAndFields.put(ymlGroupName, ymlField);
                        }
                    });

            //存的是需要展示的字段-展示字段的值
            Map<String, String> fieldAndValue2 = Maps.newHashMap();

            //@是作为字段的分隔符
            String preStr = String.format("%s@%s@%s@%s@%s@%s@%s@%s@", parentClassName, testMethodName, desc, reMark, currentParameter, testCaseAuthor, currentErrorLine, monitorTraceId);
            StringBuilder stringBuilder = new StringBuilder();
            if (CollectionUtil.isNotEmpty(fieldAndValue2)) {
                fieldAndValue2.forEach(
                        (key, value) -> stringBuilder.append(value).append("@"));
            }
            messageInfo = preStr.concat(stringBuilder.toString());
            if (Objects.nonNull(context)) {
                AssertCommonMethod.currentContextMap.put(parentClassName.concat(".").concat(testMethodName), context);
                AssertCommonMethod.currentContextMap.put(groupName.concat("_filed_value"), fieldAndValue2);
            }
        } catch (ClassNotFoundException | ArrayIndexOutOfBoundsException | NullPointerException e) {
//            messageInfo = String.format("断言异常:%s", e.getMessage());//这里不能抛异常
            messageInfo = String.format("未获取到测试方法,请检查测试方法是否包含test字样@@@@@@@@@@");
        }
        return messageInfo;
    }     

4、断言信息推送消息

      自定义 IReporter 报告模板,把通过currentContextMap收集信息推送给用户

  @Override
public void onFinish(ISuite suite) {
    //这里统一发送运行失败的case
    String groupName = systemConfiguration.getGroupName();
    if (StringUtils.isBlank(groupName)) {
        groupName = DEFAULT_GROUP_NAME;
    }
    ConcurrentHashMap concurrentHashMap = AssertCommonMethod.currentContextMap;
    String failCaseInfos = "";
    //获取失败,数据组装
    if (ObjectUtils.isNotEmpty(concurrentHashMap.get(groupName))) {
        failCaseInfos  = ((StringBuffer) concurrentHashMap.get(groupName)).toString();
    }
    try {
        log.info("=====onTestSuit finish,begin to send message, content:{}", failCaseInfos);
        feishuNoticeFlow.sendCaseFailInfo(failCaseInfos, groupName);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        concurrentHashMap.put(groupName, "");
    }
}

断言追溯效果

  • 断言失败的用例可视化展示

  • 调用过程接入日志追溯 Trace

  • 用例 one by one,做到所有用例有迹可循、有人可追

生产自动化执行检验方案

  • 通过对于代码规范、审核、强拦截(请求过滤与限制、身份验证、SQL 注入攻击防范、高并发处理、仅支持指定的请求类型)、白名单策略等方式保障生产环境下运行自动化的安全性和稳定性

  • 对接飞书机器人实现:通过聊天框进行可交互式执行接口自动化

  校验实现
    通过 apollo 配置中心管理生产允许访问的 userId,对配置中心进行权限管控

/**
 * 通用字段校验
 */
private static Boolean defaultJudge(String body, String operation) {
    if (isInWhiteString(OPERATION_WHITE,operation)){
        return (whiteJudge(body, USER_WHITE,"userId") ||
                whiteJudge(body, DRIVER_WHITE,"driverId") ||
                whiteJudge(body,EP_WHITE,"epId"));
    }
    return false;
}
/**
 * 判断是否在白名单中
 */
public static boolean isInWhiteString(String whiteString, String value) {
    return whiteString.contains(value);
}

/**
 * 校验是否白名单
 */
public static Boolean whiteJudge(String body, String whiteString, String words){
    JSONObject jsonObj = dataParser(body);
    Iterator<String> keys = jsonObj.keys();
    while (keys.hasNext()) {
        String key = keys.next();
        String value = String.valueOf(jsonObj.get(key));
        key = convertToCamelCase(key);
        if(key.contains(words)) {
            if (isInWhiteString(whiteString, value)) {
                return true;
            }
        }
    }
    return false;
}

编码规范化
生产自动化编码规范表:主要按照以下四个方面进行:规范要求、数据要求、分支管理要求、运行要求
1. 规范要求:

2. 数据要求:

3. 分支管理要求:

4. 运行要求:

报告可视化

整体回归效果/单一领域回归效果:

  • 资损
    • 通过 groups 建立标签管理体系,分类自动化测试用例。根据营销特色,编写资损自动化。
    • 确定业务资损基线,并实现相关自动化代码。
    • 构建资损自动化组,以有效管理资损自动化测试。

  • 通过资损测试标签过滤、搜索资损测试用例,创建具有资损标识的自动化任务。 qa-ci 资损任务要打标操作如下图:
    • 触发类型: 一次性任务
    • 任务类型: 接口自动化
    • 标签:资损

3.4 收益

  1. 排障成本:使用断言追溯单测试用例失败分析需要 5min->30s 定位问题,大大提升了自动化排障效率

  2. 生产应急验证效率:验证效率提高 30 倍,从原来人工线上回归所有核心业务从 1h->2min;

  3. 资损验证效率:智能识别资损需求,在测试流水线的准出节点中设置资损自动化卡点,自动执行相关业务的资损自动化任务。将资损测试从 30min,到现在的 2min,验证效率提供 15 倍;

四:总结与思考

接口自动化测试在验证 API 的正确性、稳定性和一致性方面发挥了至关重要的作用。它能够快速执行大量测试用例,显著节省时间和人力成本;同时,自动化测试可以多次运行,确保每次测试结果的一致性。此外,接口自动化测试能够覆盖各种边界条件、异常情况和不同的输入组合,提供全面的测试覆盖。然而,接口自动化测试也存在一些短板,例如测试数据的丰富性、脚本撰写的成本以及场景的真实性等问题,仍然需要进一步突破。

4.1 流量回放与自动化融合方向

相比之下,流量回放技术提供了一种更为高效的测试方法。通过对线上流量的捕获、放大或缩小,流量回放可以在测试环境中重现这些流量,从而实现对复杂业务场景的仿真测试。线上真实的大量场景数据能够弥补接口自动化测试的短板,提供更为全面和真实的测试覆盖。
接口自动化测试与流量回放技术是相互补充的。接口自动化测试提供了高效、可重复的功能验证,而流量回放则通过真实场景的仿真,提升了测试的真实性和覆盖面。两者结合使用,可以显著提升软件测试的效率和准确性,确保系统在各种使用场景下的稳定性和可靠性。

4.2 AI机器学习的应用方向

  1. 客服语音和视图类型的测试
    AI 技术在客服语音和视图类型的测试中展现了强大的能力。这包括语音识别、图像识别等技术,能够自动化地进行测试,提升测试效率和准确性。通过 AI 驱动的自动化测试,客服系统可以更快速地处理和响应用户请求,确保高质量的用户体验。

  2. 契约测试的优化
    利用 AI 技术进行契约测试,可以显著减少前期接口测试的投入成本。契约测试确保服务之间的接口契约(协议)保持一致,避免因接口变更导致的系统故障。通过 AI 的自动化分析和验证,契约测试能够更高效地发现和解决潜在问题,提升系统的稳定性和可靠性。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册