查看原文
其他

基于Java代码模型生成质量平台自动化用例方案与实践 | 得物技术

Evan.hu 赵军 得物技术
2024-12-05

目录

一、背景&目的

二、聊聊自动化那些事

    1. “低代码” 银弹 v.s 毒瘤

    2. 浅谈自动化用例生成的几种方案

三、基于Java代码模型生成自动化用例实践&方案

    1. 方案&设计

    2. 注解生命周期

    3. 平台核心组件

    4. 基于Java注解 & “重设计,轻实现”设计理念驱动

    5. 一站式生成测试用例服务

    6. 可扩展&维护性

    7. Auto Gen Auto 所有测试工作即代码

    8. 自动化ROI

四、价值&收益

    1. 一个订单税费自动化测试用例设计演示

    2. 价值&收益

背景&目的

得物自动化从最开始由各域自建到质量平台统一搭建自动化测试基础服务中台发展过程,目前在质量平台的支撑下实现了全域自动化的统一管理和维护,进一步降低了自动化测试的成本同时也提高了自动化测试效率。在质量平台的支撑下如何进一步快速、高效实现自动化用例开发和维护提高ROI也是值得探索和创新的。我们都知道自动化用例的开发和维护成本一直是自动化测试领域老生常谈的话题,本次分享结合了低代码思想和Java代码模型快速的生成质量平台自动化测试用例方案与实践,主要是为了解决:

  1. 提升自动化用例开发效率

  2. 降低自动化用例维护成本

  3. “重设计,轻实现”设计驱动

聊聊自动化那些事

“低代码”银弹 v.s 毒瘤

自动化生成测试用例业界一直有讨论,单元、接口等都有,说到自动化生成测试用例难免避不开低代码。说到低代码,也许有人说它是“毒瘤”,也有人说它是“银弹”,那到底应该怎么看呢?我们暂时不介入这两个言论的细节,而是先把关注点移到低代码本身,先回答这个问题:低代码到底是要革程序员的命?还是成为程序员工具箱里的另一个工具?
如果你觉得低代码的目的是为了革程序员的命,是要把程序员的手脚给捆住,是要束缚程序员的创造性等那么大概率你会接受毒瘤论。
如果你觉得低代码是帮助程序员提升工作效率,大概率你会勇于探索、期望尝试新方法和技术提升团队效率和个人创新,你可能认为它是一个不错的工具;其实换个角度:条条大路通罗马,只要能按时到达罗马,我相信没人会太关注过程,所以在去罗马的旅途只要你开心就好。

浅谈自动化用例生成的几种方案

自动化用例生成一直是业界探索和希望突破的瓶颈,目前自动化生成测试用例的面临的问题是无法理解业务以及生成用例过程中测试数据准备、接口断言这三大方面;其实测试设计正好解决了这三个问题,现阶段我们的解决方案是将QA人员的设计思路准确的形成配置文件再利用AIGC技术生成测试用例代码和测试用例,避免QA人员过多的投入到重复代码copy和编写的机械工作,下面我们来聊聊自动化生成技术的几种方案:
  1. 流量录制&字节码

    1. 基于流量录制 (UI & http & dubbo)

    2. 基于字节码技术

  2. 平台化&智能化

    1. 设计驱动

    2. 统一平台载体

    3. 低代码&自动化生成技术

  3. 基于大模型和AI技术

    1. L0,原始级

      1. 测试工程师还是在做测试用例设计、执行、回归、修复后再回归。没有专职的人写自动化的脚本。测试人员按需撰写脚本,遇到较大变更的时候还要检查脚本是否有效。

    2. L1,辅助级

      1. 测试框架能帮助测试工程师完成一些枯燥乏味的工作,通过一些算法完善测试脚本并将测试结果发送给对应的工程师,由工程师来决策测试结果。

    3. L2,部分自动化级

      1. 自动化测试的算法可以自我容错,不需要大量的维护工作,会按照测试用例去执行与识别,不会影响执行流程。然后它会把测试结果发送给测试工程师,由工程师决策测试。

    4. L3,有条件的自动化级

      1. 测试工程师能建立自己的测试基线与准则,测试框架可以通过机器学习完成基线的建立,可以在无人干预的情况下完成测试,测试工程师只需要了解被测系统和数据规则即可,并自动的确定Bug。并且还可以收集并分析全部的测试用例,通过机器学习等相关技术,人工智能系统可以检测到变化中的异常, 并只将异常提交给人工进行验证。

    5. L4,高度自动化级

      1. 系统能模拟类人的行为,进行并执行一些逻辑脚本或者业务脚本的撰写,达到一种完美的人机交互。它可以将测试结果定优先级,会根据严重程度发到测试管理系统,但不会对没有样本的做定义,还需要人类决策。

    6. L5,全量自动化级

      1. 系统能够完成过程化的测试,了解产品的变更,知道产品的黄金流程,同时还会将问题完全反馈。测试工程师在这里仅仅做的是算法逻辑的维护、规则的维护。

基于Java代码模型生成自动化用例实践&方案

方案&设计

注解生命周期

  • @TestConfig:描述测试用例的基本信息
  • @TestData:测试数据准备,主要是编写对应sql来获取数据,也支持自定义方法
  • @BeforeAlls:测试用例固定参数,例如用户名、密码等固定参数
  • @Component:顾名思义组件的意思,通过Component进行组件编排
  • @UrlParameter:接口URL请求参数
  • @HeaderParameter:接口header参数
  • @ApiSchedule:http、https、dubbo接口信息
  • @Asserts:接口断言,支持接口返回、数据、MQ等断言

@TestConfig

@TestConfigprivate static TestCaseParameter bases() { TestCaseParameter testCaseParameter = new TestCaseParameter(); testCaseParameter.setDescribe("xxxxxxxxx"); // 测试用例描述 testCaseParameter.setPriority(PriorityConst.P1); // 测试用例级别 testCaseParameter.setLabel(LabelConst.BVT); // 测试用例类型 testCaseParameter.setModule(ModelConst.TEST); // 测试用例模块 testCaseParameter.setRemove(true); return testCaseParameter;}
TestConfig生成质量平台自动化用例基本信息和骨架。

@Parameters

@BeforeAllspublic static Map beforeAlls() { Map<String, Object> beforeAlls = new LinkedHashMap<>(); beforeAlls.put("service", "xxxxxx"); beforeAlls.put("userName", "xxxxxx"); beforeAlls.put("scene", "USER_CENTER"); return beforeAlls;}

@TestData

@TestData( type = DataType.AFTER, order = 0)public static Database testData01() { Database database = new Database(); database.setDb(AppDbEnum.HUPU_DU.getName()); database.setSql("select userId from users where userName = '${userName}' and region = 'US';"); return database;}
// 1.DataType.AFTER测试用例执行之后从数据库提取数据// 2.DataType.BEFORE测试用例执行之后从数据库提取数据// 3.order参数值和API绑定
Parameters和TestData对应平台脚本前置测试数据准备阶段。

@Component

@Componentprivate static List<String> component() { List<String> componentList = new ArrayList<>(); componentList.add(ComponentEnum.USER_COOKIES.getName()); return componentList;}// 自动化平台开发好的组件
Component用例对应平台测试用例组件。

@UrlParameter

@UrlParameter( order = 1)public static Map<String, Object> urlParameter01() { Map<String, Object> urlParameter = new LinkedHashMap<>(); urlParameter.add("userId","xxxxx"); return urlParameter;}// 接口URL参数,order和@ApiSchedule API绑定
UrlParameter用来对应API接口URL参数。

@HeaderParameter

@HeaderParameter( order = 0)public static Map headerParameter01() { Map<String, Object> headerParameter = new LinkedHashMap<>(); headerParameter.put("userId", "xxxxx"); headerParameter.put("Content-Type", "application/json"); return headerParameter;}// 接口header参数,order和@ApiSchedule API绑定
HeaderParameter用来对应API接口header参数。

@ApiSchedule

@ApiSchedule( order = 0)public static TestApiInfo apiSchedule00() { TestApiInfo testApiInfo = new TestApiInfo(); testApiInfo.setDescribe("xxxxxxxxxx"); // api接口信息描述 testApiInfo.setApp("intl-bigger"); // api对应服务部署服务名 testApiInfo.setHost("xxxxxxxx"); testApiInfo.setUrl("/seller/confirm"); testApiInfo.setMothod(RequestMethodConst.POST); testApiInfo.setBody("{\n" + " \"list\": [\n" + " {\n" + " \"skuId\": \"6040346982\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " },\n" + " {\n" + " \"skuId\": \"6040346983\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " }\n" + " ]\n" + "}"); List<String> component = new ArrayList<>(); component.add(ComponentEnum.USER_COOKIES.getName() + "_true"); component.add(ComponentEnum.AUTO_GLOBAL_LIST_ASSERTION.getName() + "_false"); Map<String, Object> bParameter = new LinkedHashMap<>(); bParameter.put("userName", "huxiaotian@shizhuang-inc.com"); bParameter.put("passWord", "du123456"); bParameter.put("time_pre", "uuid.uuid()_vbs"); testApiInfo.setBParameter(bParameter); Map<String, Object> fParameter = new LinkedHashMap<>(); fParameter.put("userName", "huxiaotian@shizhuang-inc.com"); fParameter.put("passWord", "du123456"); fParameter.put("time_pre", "uuid.uuid()_vbs"); testApiInfo.setFParameter(fParameter); testApiInfo.setComponent(component); testApiInfo.setIsFront(true); return testApiInfo;}
@ApiSchedule( order = 1)public static TestApiInfo apiSchedule01() { TestApiInfo testApiInfo = new TestApiInfo(); testApiInfo.setDescribe("xxxxxxxxxx"); testApiInfo.setApp("intl-bigger"); testApiInfo.setHost("xxxxxxxx"); testApiInfo.setUrl("/seller/cancel"); testApiInfo.setMothod(RequestMethodConst.POST); testApiInfo.setBody("{\n" + " \"list\": [\n" + " {\n" + " \"skuId\": \"6040346982\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " },\n" + " {\n" + " \"skuId\": \"6040346983\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " }\n" + " ]\n" + "}"); Map<String, Object> bParameter = new LinkedHashMap<>(); bParameter.put("time_sleep", "time.sleep(6)_vbs"); testApiInfo.setBParameter(bParameter); return testApiInfo;}
// 用例执行按照order值从小到大
ApiSchedule用来对应API接口请求信息。

@Extract

@Extract(order = 0)private static List<String> extract() { List<String> extract = new ArrayList<>(); extract.add("staus=$..staus"); return extract;}// 提取接口返回报文中的字段信息,编写规则完全遵循jsonpath语法规则:$..date..list[0]..order
Extract作用是提取对应API接口返回报文中指定值保存到环境变量中后续自动化脚本使用。

@Asserts

@Asserts( type = AssertType.RESPONSE, order = 0)public static List<String> response00() { List<String> rule = new ArrayList<>(); rule.add("$..amountInfo!=null"); rule.add("$..date..list[0]..freightAmount=20"); rule.add("$..saleTaxAmount>100"); return rule;}// AssertType.RESPONSE:接口返回报文断言,$..date..list[0]..freightAmount是根据jsonpath规则提取接口返回报文的字段信息来断言;// 支持断言操作符:=、!=、>=、<=、>、<、<>
@Asserts( type = AssertType.SQL, order = 0)private static DbAssert asserts() { DbAssert dbAssert = new DbAssert(); String db = OverseaDatabaseConst.DW_INTL_ORDER; String table = "intl_trade_order_item"; String sql = "SELECT order_item_no,saleTaxAmount,freightAmount FROM `intl_trade_order_item` where order_item_no = '${order_item_no}';"; Map<String, String> rules = new LinkedHashMap<>(); rules.put("sql.$..order_item_no=${order_item_no}"); rules.put("sql.$..saleTaxAmount>=200"); rules.put("sql.$..order_item_no=500"); dbAssert.setDb(db); dbAssert.setTable(table); dbAssert.setSql(sql); dbAssert.setRules(rules); return dbAssert;}// AssertType.SQL:是针对接口实现SQL断言// 支持断言操作符:=、!=、>=、<=、>、<、<>
以上注解的代码并非QA人员手动编写,是通过配置文件将QA设计思路沉淀下来通过自动化代码生成的方式实现的,这大大节省了QA人员编写代码的成本,再借助得物内部自动化平台为载体,从而实现了自动化用例从设计到自动化用例生成、执行、报告汇总等一站式服务。

平台核心组件

用于通用or复杂功能的复用(可以把通用的、复杂的功能逻辑封装成公共组件)实现复杂处理逻辑。下面列举几个重要的组件:

接口返回断言

$..data[0]..inboundApplyNo!=null$..data[0]..status=1$..data[0]..status<>1,3,4,5$..status=request.$..status$..name[*]=4// 支持断言操作符:=、!=、>=、<=、>、<、<>

接口数据库断言

sql.$..biz_no=nullsql.$..biz_no!=nullsql.$..status=request.$..statussql.$..biz_no=response.$..biz_nosql.$..feature..status=5// 支持断言操作符:=、!=、>=、<=、>、<、<>

基于Java注解&“重设计,轻实现”设计理念驱动

@RunWith(GenerateCode.class)@Property( source = RegionEnum.GUS_APP, packages = "com.platform.autotestcase.testcases.sg.bvt.order", describe = "xxxxxxxxxxx", module = ModuleEnum.TEST, beforeAll = { @Args(key = "reverse_no", value = "xxxxxxx", clazz = Demo.class, method = "parme"), @Args(key = "time_pre", vbs = "uuid.uuid()"), }, mysql = { @Database(db = AppDbEnum.DW_INTL_DEPOSIT, sql = "update outbound_apply set status = 5,signed_time=null where reverse_no = '${userName}';"), @Database(db = AppDbEnum.DW_INTL_DEPOSIT, sql = "select order_no outbound_apply where status = 5 and reverse_no = '${userName}';"), }, component = { ComponentEnum.USER_COOKIES, }, testData = { @Source(type = DataType.AFTER, order = 0, db = AppDbEnum.HUPU_DU, sql = "select userId from users where userName = '${userName}' and region = 'US';"),
@Source(type = DataType.BEFORE, order = 0, db = AppDbEnum.DW_INTL_ORDER, sql = "select order_item_no from intl_trade_order_item where buyer_id = '${userId}' and order_item_status >= 2000 and order_item_status <= 4000 and biz_id = 'DEWU_NORMAL_EXPORT_2C' and package_info like '%SFGJIECS%' order by modify_time desc limit 1;"), }, extract = { @Filter(order = 0, key = "amountInfo", path = "$..amountInfo..minUnitVal"), @Filter(order = 0, key = "freightAmount", path = "$..freightAmount..minUnitVal"), @Filter(order = 0, key = "saleTaxAmount", path = "$..saleTaxAmount..minUnitVal"), @Filter(order = 0, key = "paymentAmount", path = "$..paymentAmount..minUnitVal"), }, rspAssert = { @Regular(order = 0, path = "$..amountInfo!=null"), @Regular(order = 0, path = "$..freightAmount!=null"), @Regular(order = 0, path = "$..saleTaxAmount!=null"), }, dbAssert = { @Source(db = AppDbEnum.DW_INTL_ORDER, order = 0, sql = "select reverse_type,reverse_status,reason_code from intl_reverse_order where order_item_no = '${orderItem}' and is_del = 0;", verify = { @Args(path = "sql.$..reverse_status=200") } ), @Source(db = AppDbEnum.DW_INTL_ORDER, order = 1, sql = "select reverse_status,reason_code from intl_reverse_order where order_item_no = '${orderItem}' and is_del = 0;", verify = { @Args(path = "sql.$..reason_code=${reasonCode}"), @Args(path = "sql.$..reverse_status!=200") } ) })public class DemoTemplateTest {
@Http(order = 0, app = AppEnum.INTL_BIGGER, describe = "api描述") @Assertion( asserts = { @Path(rule = "${orderItemNo}=34"), @Path(rule = "${orderItemNo}=34"), }) @Addition( bParameter = { @Args(key = "userName", value = "xxxxxx"), @Args(key = "passWord", value = "xxxxxx"), @Args(key = "time_pre", vbs = "uuid.uuid()"), }, fParameter = { @Args(key = "userName", value = "xxxxxxx"), @Args(key = "passWord", value = "xxxxxxx"), @Args(key = "time_pre", vbs = "uuid.uuid()"), }, components = { @Storage(component = ComponentEnum.USER_COOKIES, isFront = true), @Storage(component = ComponentEnum.AUTO_GLOBAL_LIST_ASSERTION), }) public static String curl = "curl --location --globoff '{{myVariable}}:8888/deposit/enterprise-stock-apply/add' \\\n" + "--header 'x-infr-flowtype: {{flowtype}}' \\\n" + "--header 'Content-Type: application/json' \\\n" + "--data '{\n" + " \"globalSkuId\": 12000002083,\n" + " \"applyQty\": 1,\n" + " \"sellerId\": 10002366,\n" + " \"timeZone\": \"Asia/Shanghai\",\n" + " \"language\": \"en\"\n" + "}'";

@Http(order = 1, app = AppEnum.INTL_BIGGER, describe = "api描述") @Addition( bParameter = { @Args(key = "time_sleep", vbs = "time.sleep(6)"), } ) public static String cur1 = "curl --location 'http://127.0.0.1:8888/tempCtrl/seller/cancel' \\\n" + "--header 'userId: 45817899' \\\n" + "--header 'Content-Type: application/json' \\\n" + "--data '{\n" + " \"list\": [\n" + " {\n" + " \"skuId\": \"6040346982\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " },\n" + " {\n" + " \"skuId\": \"6040346983\",\n" + " \"codeInfo\": \"4057283582767\",\n" + " \"origin\": \"COMPETITION\",\n" + " \"codeOrigin\": \"nice\"\n" + " }\n" + " ]\n" + "}'";}

一站式生成测试用例服务

通过Java注解将测试用例设计思路形成配置,再通过解析注解统一生成Java可执行的测试用例文件,最后生成质量平台自动化测试用例,通过这种方式解决了测试用例开发效率低下和代码维护等问题。

可扩展&维护性

随着自动化测试不断发展以及越来越重要,传统的方式对自动化的理解重在编写代码,自动化测试用例的设计逐渐成长为一个非常重要的环节。所以,在自动化实践过程中我们应该要走向“重设计,轻实现”的自动化的理念&方法。
所谓重设计,QA人员更多的把时间花在设计自动化测试用例上,然后通过配置以及AI技术快速把设计思路生成平台可执行的代码,而不是全部精力在编写代码&维护成本上。通过设计驱动的思维和理念设计极大部分解决了自动化测试用例开发成本以及维护成本。

Auto Gen Auto 所有测试工作即代码

Automation Generate Automation:叫自动化产生自动化测试代码。我们暂且叫Auto Gen Auto,常规的自动化测试,是指用代码实现设计好的TestCase,而Auto Gen Auto的目的是让设计驱动自动化生成Test Case;
从测试需求到自动化测试用例是完全自动化的,每次需求改变的时候,只需运行一次 Auto Gen Auto 即可生成新的自动化案例,维护成本几乎为零。所以 Auto Gen Auto 技术如果能落地,ROI 就会大大提高。

自动化ROI

t:单次运行时间

n:自动化测试运行次数

d:开发成本

m:维护成本

价值&收益

一个订单税费自动化测试用例设计演示

URL:/api/v1/h5/bigger/intl/bigger/buyer-order/confirm?noSign=true

Method: POST

Header:

  Content-Type:application/jsonCurrencycode:USDDeviceid:5df7fa6f-e075-4f6f-96ea-c9b7d7514859Devicetype:h5Region:US

Body:

  {

  sizeKey:""

  inventoryNo:"SN1021464791"

  spuId:"30740440"

  skuId:"602916395"

}

接口断言:

1.接口返回字段:minUnitVal字段值为不为0;

2.数据库断言:查询条件是接口返回requestId;

    表1: oversea_sales_tax:新增1条税费记录;

   表2: intl_trade_order_item:sale_tax_amount字段值大于“0”,status值为1000。

一般情况下一个单接口自动化的代码约500~800行,通过设计驱动和自动化用例生成的方式代码编写几乎为0,生成的自动化完全符合预期,另外如果后续接口有变动我们只需要在设计上修改即可更新和维护自动化case。

价值&收益

  • 提升自动化用例开发的效率(80%+)
  • 降低自动化维护成本&代码复用(80%+)
  • 自动化脚本调试成本几乎是零
  • 团队一致性:代码的可读性&团队共享

往期回顾


1. 粉丝福利QCon赠票|与巴布、郭东白、章文嵩等大咖对话,1v1 约见 100+ 行业顶级技术专家
2. 得物千人规模敏捷迭代实践分享
3. “不知今夕是何年”的周基年解法 | 得物技术
4. Monkey自动化工具结合B端组件可行性探索 | 得物技术


文 / Evan.hu 


关注得物技术,每周一、三、五更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:

修改于
继续滑动看下一个
得物技术
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存