ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

低代码测试平台实践:从API编排到性能测试的统一用例模型

低代码测试平台实践:从API编排到性能测试的统一用例模型 这两年团队在接口自动化上的最大变化不是用例数量涨了多少而是大家越来越受不了一锅端的脚本式维护。开发随手改个字段名测试就得花半天改代码改完还得在联调环境反复验证一条门店下单链路横跨五个系统光构造请求和断言就写了三百行。我们索性自己动手用拖拽编排的方式搭了一个低代码测试平台把API测试和性能测试都收到同一套可视化用例模型里。画布上拖几个节点、连几条线一条登录-下单-支付-查单的深链路就能跑起来同一个编排还能一键转成压测场景。这篇文章把我们在平台架构、编排引擎、性能场景落地这几块的工程实践和踩过的坑都捋一遍给同样在自建测试平台或正在选型的团队做个参照。1. 为什么我们最后选择自建低代码测试平台先说实话最开始我们并不是非自研不可。团队里Postman用得很熟JMeter也有专门的性能测试小组在维护开源测试平台也调研过两三套。但把所有工具摆在一起盘了一圈之后发现每条路都有没法绕过去的坎。1.1 现成工具的能力边界Postman和Apifox这类工具做单接口调试和简单的Collection Runner很顺手可一旦用例开始形成链路多个接口之间需要传递变量、有分支判断、有循环回放它们的编排能力就变得很别扭。Collection Runner本质上是顺序执行数组你想表达如果登录失败就重新认证再走一遍在脚本里写一堆pm.test和条件判断最后还是要落入代码维护的坑。JMeter是性能测试的事实标准它的线程组取样器逻辑控制器能覆盖绝大多数场景但jmx这套XML的工程化程度很低——没法做细粒度的代码评审不好跟业务字段绑定团队协作全靠谁来动这个jmx文件谁负责。开源测试平台能力覆盖广可一旦要对接我们内部的网关、注册中心、配置中心以及把测试数据源绑定到已有业务库改动工作量反而比重写核心引擎更大。1.2 我们真正缺的不是工具而是模型看了一圈之后结论变得很清晰我们要的不是又一个发请求的工具而是一个能把业务链路的执行语义表达清楚、并且能和团队现有工程体系无缝接轨的模型。具体拆解下来有几条硬需求用例必须可视化表达非技术人员也能看懂一条链路在做什么同一个用例模型要同时服务API测试和性能测试不能是两套脚本各写各的变量流转、断言、分支、循环这些逻辑要能从画布上直接完成而不是藏在后置脚本里执行记录要细到节点级能清楚看到哪一步慢、哪一步出错。这几条需求里前三条是我决定自研的关键。测试平台本质上是一个执行编排系统谁的用例模型更贴近业务谁就能让更多人参与到接口测试里来。现成工具把用例定义成脚本集合我们要的用例其实是业务流程图。1.3 自研的整体取舍自研当然有代价。低代码平台的坑在于编辑器好做执行引擎难做画布上拖拽出来的图能不能稳定地被解释执行决定了这个平台是真工具还是玩具。我们在设计之初就定了一个原则画布只是交互层真正的用例是一个结构化JSON执行引擎只认JSON。前端把图形翻译成JSON后端把JSON翻译成执行计划中间不许有模糊地带。这条原则让后续很多问题都变得简单——比如用例版本比较、权限控制、批量编辑本质上都是对这个JSON做操作。2. 平台总览一个用例从画布到执行的完整链路我们的平台整体分成四层前端画布层、服务管理层、调度执行层、存储与报告层。这四层各自职责边界很清晰前端的画布和后端的执行引擎完全解耦中间只通过用例DSL通信。2.1 前端画布层画布层是一个单独的Web应用技术选型上我花了点时间对比React Flow和AntV X6最后选了基于X6定制。原因是X6在流程图、连线、节点交互这些场景上内置能力比React Flow更贴近编排场景支持自定义连线桩、锚点、边样式对节点间数据的展示也更方便。页面布局是经典三栏左侧是节点库中间是画布右侧是属性面板。节点库里每个节点都预置了默认配置拖到画布上之后在右侧面板里填请求地址、规则、断言表达式。画布上除了保存、执行、调试这些常规操作还有一个一键转压测的按钮点了之后会跳到性能场景配置页直接复用当前这条编排用例。2.2 服务管理层服务管理层由几个Spring Boot服务组成核心是用例管理服务和执行调度服务。用例管理服务负责用例CRUD、版本管理、权限管理同时负责做DSL的合法性校验——节点类型是否合法、连线是否指向存在的节点、变量引用是否能被解析、是否存在孤立节点。校验通过才允许保存这一步非常关键不然执行引擎拿到一张不完整的图只能在运行时报错。执行调度服务更单纯接收执行请求构造执行任务放到消息队列等待执行器上报结果再把结果写入报告中心。2.3 存储与执行层存储上我们没有用太复杂的东西。MySQL存用例JSON、执行历史、用户权限、测试数据源元数据Redis有两个用途一是任务队列执行调度和结果汇报之间通过它解耦二是存放执行过程中的实时指标方便前端拉取画布上每个节点的实时状态。MinIO用来存jmx文件、压测报告附件、大响应体快照。执行层区分两类执行器API执行器是自研的负责解释执行编排用例性能执行器一开始包了一层JMeter后来拆出自研并发引擎这个演进后面专门讲。2.4 一条用例的执行链路完整走一遍会更容易理解测试人员在画布上拖好节点、保存平台把用例JSON写入MySQL同时生成一个版本号。点击执行后执行调度服务从MySQL取出用例JSON发送到Redis任务队列。API执行器的一个Worker消费到任务解析JSON构建有向图开始逐节点执行。每执行完一个节点Worker会往Redis写一条节点状态记录前端通过WebSocket或短轮询从报告中心拉这些状态实时渲染到画布上比如节点变绿、变红、耗时多少。整条链路跑完后报告中心汇总所有节点数据生成一份分步骤的执行报告。3. 拖拽编排的核心有向图用例模型与执行引擎这一块是整个平台的心脏。很多人以为拖拽编排就是画个流程实际上关键问题在于你画的这张图计算机会不会正确、稳定、可预期地把它跑完。3.1 节点、连线和一个最小的用例DSL我们把用例定义成一个有向图图的顶点是节点边是连线。节点的类型一开始定了十来种经过实际使用后收敛为最常用的几个节点类型作用start / end用例的入口和出口一个用例只允许一个start节点end节点可以多个http-request发一次HTTP请求支持配置超时、认证、请求体assert对请求结果做断言支持状态码、JSONPath、响应时间extract从响应中提取数据写入上下文比如tokenscript运行一段沙箱JS脚本用于生成签名、时间戳等动态数据loop进入循环体支持按次数、按集合、按条件condition分支节点按条件选择后续的某一条连线wait等待固定时长用于模拟思考时间或等待异步结果连线不是简单的上一个执行完就执行下一个每条线都可以携带条件。普通默认连线表示无条件通过条件连线则带上一个表达式只有表达式为真时才会沿着这条线走。这个设计是后面做失败重试、分支循环的根基。一个最简登录用例的DSL大概长这样{ nodes: [ { id: n_start, type: start }, { id: n_login, type: http-request, name: 登录获取Token, config: { method: POST, url: {{env.baseUrl}}/api/v1/auth/login, headers: { Content-Type: application/json }, body: { mode: json, content: {\username\:\{{var.username}}\,\password\:\{{var.password}}\} } } }, { id: n_extract_token, type: extract, name: 提取Token, config: { source: response.body, expression: $.data.token, target: var.token } }, { id: n_assert, type: assert, name: 校验登录成功, config: { target: response.body, expression: $.code, operator: equals, expected: 0 } }, { id: n_end, type: end } ], edges: [ {id: e1, source: n_start, target: n_login, type: default}, {id: e2, source: n_login, target: n_extract_token, type: default}, {id: e3, source: n_extract_token, target: n_assert, type: default}, {id: e4, source: n_assert, target: n_end, type: default} ] }这个JSON看起来简单但它背后有一个重要约定节点本身不维护指向下一个节点的信息指向关系全部由edges决定。这意味着画布上任何一条连线都可以独立增删修改而不需要改动节点内部的数据结构。前端拖拽改动连线之后重新序列化edges即可非常干净。3.2 执行引擎的图遍历逻辑执行引擎拿到用例JSON之后第一件事是把所有节点按id建索引把所有出边按source节点归组。然后从start节点开始对每个节点执行执行节点自身逻辑然后找下一条边的循环。找下一条边的规则是取当前节点的所有出边如果只有一条默认边直接走如果有条件边依次评估条件表达式选第一个为true的如果一条都不满足且存在默认边走默认边否则用例终止相当于走了一个隐式end。这个规则简单直接但有个问题如果用户拖出了一张带环的图比如A到B、B到A执行引擎会死循环。我们做了两层防护一是画布保存时做连通性检查检测环路并提示用户二是执行引擎保留一个全局步数计数器默认上限1000步超过即判定用例异常退出。第二层是兜底防止数据库里存了某些历史脏数据导致运行时失控。3.3 变量上下文与作用域编排用例的实用性很大程度上取决于变量机制好不好用。我们在执行实例级别维护了一个上下文对象节点里所有{{xxx}}占位符都会在真正执行前被上下文解析替换。上下文分成几个作用域env作用域环境级别的配置比如baseUrl、appId在执行任务创建时从环境配置读取var作用域用例级别的变量由extract节点、script节点或测试数据源写入vu作用域虚拟用户级变量性能测试时每个并发用户有独立的一份。设计上http-request节点引用变量时写{{var.username}}只是为了避免不同作用域变量撞名。这个细化看起来多余但在后面跑性能测试时救了大命。并发100个虚拟用户如果共享一个var作用域一个用户写完token另一个用户能读到别人的token数据串得一塌糊涂。所以我们从第一天就坚持上下文按执行单元隔离单个用例的API测试用一个上下文性能压测里每个VU单独一个上下文。3.4 分支、循环与失败重试分支用condition节点实现。condition节点本身不做任何请求它只负责评估一个表达式然后根据结果为true或false选择不同出边。比如登录是否成功这个分支表达式可以写成{{var.code}} 0true走继续下单false走重新登录。循环用loop节点实现loop节点的出边有一条指向循环体的首节点循环体末节点需要有一条特殊连线指回loop节点同时loop节点维护当前迭代次数达到条件后走循环结束边。失败重试是另外一套机制。我们没有把重试做成节点类型而是在http-request节点上做配置定义失败后跳到哪个节点相当于给节点加了一条隐式的失败连线。长链路里最常见的用法是下单请求因为Token过期返回401失败后不是直接终止用例而是跳到登录节点重新认证再走回下单节点。这种节点级失败路由比整条用例从头重跑高效得多。4. 编排里的API测试能力请求、断言、数据驱动是怎么落地的画布和执行引擎撑起了骨架但真正让测试人员愿意天天用的还是那些API测试里必不可少的细节能力。这一节讲我们在节点能力上的打磨。4.1 请求节点从基础配置到动态参数http-request节点的属性面板支持的方法、URL、Query参数、Headers、BodyBody支持JSON、form-data、x-www-form-urlencoded和raw文本。raw文本可以塞XML、纯文本、二进制协议体。这里有一个容易被忽略的设计请求体的内容默认不会做JSON格式化校验我们以字符串形式保存执行时才替换变量。这种做法让用户在编排阶段不用纠结我的JSON是不是合法执行时如果请求体解析失败错误日志会直接指出哪个节点、哪个变量没有被替换。这比在保存时就强行校验JSON要友好很多。动态参数是这个节点最高频的使用场景。时间戳、随机数、签名这类数据如果每次执行都一模一样会导致接口幂等校验失败。我们用script节点解决这个问题提供一个JS沙箱// script节点生成当前时间戳和签名 const timestamp String(Date.now()); const sign md5(order_ timestamp key secret); context.set(var.timestamp, timestamp); context.set(var.sign, sign);沙箱里暴露了context、md5、logger这几个内置对象基本能覆盖签名、加密、随机数据这些常规需求。不用完整Node.js环境一是安全考虑二是沙箱的执行时间可控避免有人写一段死循环脚本把执行器拖垮。4.2 断言节点把接口对不对变成一条规则断言节点支持常见的几类状态码断言比如期望200或201JSONPath断言例如$.data.orderStatus 等于 CREATE_SUCCESS响应头断言比如Content-Type包含application/json响应时间断言比如响应时间小于800ms脚本断言用JS写复杂逻辑。实际使用中JSONPath断言使用率最高。属性面板上会有一个试运行按钮可以直接拿上一次执行的实际响应体来测试当前表达式秒级反馈表达式是否写错。这个功能虽然小但极大降低了JSONPath的学习成本尤其是对不常写代码的测试同学。4.3 变量提取长链路的数据接力extract节点是我们用得最多的节点类型之一。它支持JSONPath和正则两种提取器。JSONPath用于从响应体里取结构化的业务数据正则用于取Token、Cookie这类不需要解析JSON的文本数据。提取结果写到上下文的var作用域里后续节点通过{{var.xxx}}引用。比较关键的是提取失败的处理。默认情况下提取失败会导致用例失败因为后续节点大概率会因为拿不到数据而报错。但我们允许在提取节点上配置一个失败默认值比如某个字段在部分场景下确实会缺失你可以直接填一个兜底值让链路继续跑下去。这个设计让流程编排不必为了一个可空字段去写一大段分支。4.4 数据源参数化让用例从一条数据变成一批数据测试用例要跑出价值数据就不能只写死一组。我们的数据源设计是独立的测试数据模块一个用例可以绑定一个或多个数据表。数据表来源有三种Excel导入、CSV文件、数据库查询SQL。执行时执行器按迭代顺序数据行进行映射——第n次迭代取第n行数据字段名与用例变量名做映射绑定。API测试里可以选择每次迭代换一行数据性能测试里可以选择按虚拟用户分配数据行或按迭代分配数据行这个细节在后面压测章节展开。5. 从编排到性能测试一个用例如何变成压测场景一开始我们的平台只有API测试能力性能测试还是团队里几个老人用JMeter单独做。后来我们意识到一个尴尬的事实API测试编排好的链路在压测时又要用JMeter重新写一遍两个地方维护两份脚本链路稍微复杂一点就经常出现API测试跑通了压测脚本还报路径参数错误这类低级问题。于是我们决定把性能测试拉进编排体系。5.1 方案选择JMeter还是自研执行器这个选择我们纠结了很久。JMeter生态成熟分布式压测有现成方案报告模板丰富但它和编排DSL是两套模型我们需要写一套DSL转jmx的转换器而且转出来的jmx脚本在维护上是黑盒出了问题很难定位是转换器的问题还是JMeter本身的问题。权衡之后我们走了一条折中路线第一阶段做DSL转jmx让性能测试能用起来第二阶段自研轻量级并发执行器把编排用例直接原生化执行。自研执行器的核心逻辑并不复杂——它内部就是N个goroutine/线程每个线程模拟一个虚拟用户每个虚拟用户独立跑一遍同一张用例图。难的不是让一个用户跑起来而是并发调度、资源隔离、结果采样这些工程细节但这些都是可控的。我们最终自研执行器上线后压测场景从创建到出结果的时间缩短了大半维护成本也明显降低。5.2 场景配置一张图定义怎么压性能场景的配置挂在编排用例的外层通过YAML描述testPlan: name: 下单链路压测 description: 每日核心链路回归压测 engine: builtin strategy: mode: step steps: - vu: 10 duration: 30s - vu: 50 duration: 60s - vu: 100 duration: 120s thinkTime: 2 variables: - name: username source:>
返回列表