☰
低代码测试平台实践:从API编排到性能测试的统一用例模型
2026/9/26 6:46:47 网站建设 项目流程

这两年团队在接口自动化上的最大变化,不是用例数量涨了多少,而是大家越来越受不了一锅端的脚本式维护。开发随手改个字段名,测试就得花半天改代码,改完还得在联调环境反复验证;一条门店下单链路横跨五个系统,光构造请求和断言就写了三百行。我们索性自己动手,用拖拽编排的方式搭了一个低代码测试平台,把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从响应中提取数据写入上下文,比如token
script运行一段沙箱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节点开始,对每个节点执行"执行节点自身逻辑,然后找下一条边"的循环。找下一条边的规则是:

  1. 取当前节点的所有出边;
  2. 如果只有一条默认边,直接走;
  3. 如果有条件边,依次评估条件表达式,选第一个为true的;
  4. 如果一条都不满足,且存在默认边,走默认边;否则用例终止(相当于走了一个隐式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}} == 0,true走继续下单,false走重新登录。循环用loop节点实现,loop节点的出边有一条指向循环体的首节点,循环体末节点需要有一条特殊连线指回loop节点,同时loop节点维护当前迭代次数,达到条件后走循环结束边。

失败重试是另外一套机制。我们没有把重试做成节点类型,而是在http-request节点上做配置:定义"失败后跳到哪个节点",相当于给节点加了一条隐式的失败连线。长链路里最常见的用法是,下单请求因为Token过期返回401,失败后不是直接终止用例,而是跳到登录节点重新认证,再走回下单节点。这种"节点级失败路由"比整条用例从头重跑高效得多。

4. 编排里的API测试能力:请求、断言、数据驱动是怎么落地的

画布和执行引擎撑起了骨架,但真正让测试人员愿意天天用的,还是那些API测试里必不可少的细节能力。这一节讲我们在节点能力上的打磨。

4.1 请求节点:从基础配置到动态参数

http-request节点的属性面板支持的方法、URL、Query参数、Headers、Body,Body支持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或201;
  • JSONPath断言,例如"$.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:>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询