返回文章
测试与验证·约 14 分钟·Kain

AI下借助SDD激活研发最后一公里——E2E测试

用规格驱动补齐 AI 编程的最后一公里,让端到端测试进入研发闭环。

AI下借助SDD激活研发最后一公里——E2E测试的封面
文章目录

当单元测试覆盖率逐年提升,CI流水线日益完善,为什么交付前的最后一轮手工验证依然无法消除?本文从一次真实的Legacy系统E2E测试实践出发,分享如何借助Spec-Driven Development方法论与AI能力,打通"写代码的同时产出E2E测试用例"的闭环。

一、最后一公里的困境

1.1 被忽视的测试断层

过去几年,团队在测试左移上投入了大量精力:

  • 单元测试:核心Manager层覆盖率从20%提升至60%+,SonarGate卡住PR合入。
  • 接口测试:Postman集合覆盖了80%的API端点,CI阶段自动运行。
  • 代码审查:CR流程强制关联Spec编号,变更可追溯。

但每次发版前,测试团队仍需花费2-3天执行全量回归。问题出在哪里?

答案在E2E(End-to-End)测试这一层。

我们的系统是一个零售商品管理系统,基于Spring MVC + jQuery + EasyUI构建,核心业务全部以单据页面的形式呈现——到货单、配货单、退货单、调拨单、盘点单……共118个bill模块,每个模块遵循相同的继承结构:

BillController → BillHeader → BillDetail → BillList

这些页面承载了从录入、审核、确认到出库的核心业务流程。单元测试覆盖了Manager层的计算逻辑,接口测试验证了Controller的请求响应,但页面上的联动、校验、状态流转、字段默认值——这些真正影响用户体验的规则,始终是自动化测试的盲区。

1.2 Legacy前端的E2E测试为何这么难

尝试过Selenium、Playwright、Cypress等主流方案后,我们总结了三个核心困境:

困境一:EasyUI组件是黑盒

EasyUI通过JavaScript动态渲染DOM结构,同一个combobox在不同版本、不同数据状态下可能生成完全不同的DOM树。传统的CSS选择器或XPath定位方式极其脆弱:

<!-- EasyUI combobox 生成的DOM -->
<span class="combo">
  <input class="combo-text" ...>
  <span><input type="hidden" class="combo-value" ...></span>
</span>
<!-- 下拉面板是动态挂载到body上的,与input不在同一DOM子树 -->

一次EasyUI版本升级可能导致全部E2E用例失效,维护成本远超收益。

困境二:118个同类模块,手工编写不可行

每个bill模块平均1000-1200行JavaScript,包含: - 8类可提取的业务规则(查询默认值、字段联动、操作拦截、保存校验……) - 3-6个标准测试场景(查询组件初始化、查询参数传递、新增工具栏流程……)

手工编写一个模块的E2E测试套件需要1-2人天,118个模块意味着120+人天的纯手工投入——这在任何团队都是不可接受的。

困境三:测试用例跟不上代码变更

业务需求迭代频繁,每次变更可能影响字段联动逻辑、校验规则、操作权限。手工维护的E2E用例往往在2-3个迭代后就开始大面积失败,最终沦为"红色测试"——没人敢跑,也没人愿意修。

二、破局思路:SDD + AI + CDP

2.1 SDD的测试延伸

Spec-Driven Development(规格驱动开发)在我们的实践中已经形成了明确的工作流:

需求 → Spec(R-xxx需求 + D-xxx设计 + T-xxx任务)→ 代码实现 → 验证

每个变更都关联Spec编号,代码提交必须包含@spec标注。这保证了每一个代码变更都有对应的业务意图描述。

但我们发现,Spec的价值远不止于追溯。它实际上定义了一套结构化的业务契约:

Spec内容 对应的E2E验证点
R-001: 新增时默认有效期为7天 add-toolbar-flow: 验证validDay字段默认值
R-002: 送货日期根据单据类型启用/禁用 field-conditional-logic: 验证sendDate的enable/disable
R-003: 来源为补货建议单时禁止删除明细 operation-guard: 验证delete按钮被拦截

Spec不仅是开发者的指南,更是E2E测试用例的天然需求来源。如果能从源码中自动提取这些规则并生成测试用例,就能实现SDD流程的自然延伸——从"规格驱动开发"到"规格驱动测试"。

2.2 为什么选择CDP而非传统UI自动化

面对EasyUI的黑盒问题,我们选择了Chrome DevTools Protocol(CDP)作为技术路线。

CDP是Chrome浏览器原生的调试协议,它允许我们直接在浏览器上下文中执行JavaScript,绕过DOM结构的不稳定性:

传统Selenium/Playwright:  选择器 → DOM操作 → 断言
CDP方案:                  注入Helper → 调用EasyUI API → 读取组件状态 → 断言

关键区别在于:我们不操作DOM,而是调用EasyUI组件的JavaScript API。

举个例子,验证一个combobox的选中值:

// 传统方式(脆弱):依赖DOM结构
const text = await page.locator('.combo-text').inputValue();

// CDP方式(稳定):直接调用EasyUI API
const result = EUI.getComboValue('#storeNo');
// result → { value: 'S001', text: '旗舰店' }

为此,我们构建了一个浏览器端Helper——easyui-test-helper.js(约1200行),封装了: - EUI.getComboValue() / EUI.setComboValue() — combobox读写 - EUI.getDateboxValue() / EUI.setDateboxValue() — datebox读写 - EUI.getGridData() / EUI.interceptGridLoad() — datagrid数据读取与拦截 - EUI.clickBillToolbar(action, { zone }) — 工具栏按钮按zone点击 - EUI.getFormValues() — 表单全量读取

这些Helper屏蔽了EasyUI的DOM实现细节,让测试用例专注于业务意图而非控件操作。

2.3 双引擎架构

在实际验证中,我们发现纯CDP方案也有局限:文件上传下载、复杂弹窗、拖拽等交互仍需要真实的DOM操作。因此我们设计了双引擎架构:

05.Test/04.auto-test/
├── lib/                          # 框架核心
│   ├── cdp-session.js            # CDP通信层
│   ├── chrome-launcher.js        # Chrome启动管理
│   ├── pilot-runtime.js          # 测试执行引擎
│   ├── suite-runner.js           # CDP用例编排
│   ├── playwright-suite-runner.js # Playwright hybrid编排
│   └── ...
├── easyui-test-helper.js         # 浏览器端Helper
├── run-suite.js                  # CDP入口
├── run-playwright-suite.js       # Playwright hybrid入口
└── cases/                        # 各模块测试套件
  • CDP引擎(run-suite.js):适合验证EasyUI组件状态、表单数据、datagrid查询参数、controller合约。轻量、快速、稳定。
  • Playwright hybrid引擎(run-playwright-suite.js):适合真实点击交互、文件上传下载、可见元素验证。复用同一套登录/菜单/iframe链路。

两个引擎共享同一套测试数据(datasets/smoke.yaml)和业务规格定义(oracle.yaml),开发团队可以根据测试目的灵活选择。

三、AI驱动的用例生成

解决了技术架构问题,118个模块的用例编写成本依然存在。这是我们引入AI能力的核心动机。

3.1 从源码提取业务规则

每个bill模块的JavaScript源码中,业务规则遵循固定的代码模式。我们定义了8类可提取的业务规则:

规则类型 代码模式 示例
addBeforeHandle默认值 addBeforeHandle: function() { ... } $('#bizType').combobox('setValue', 'NORMAL')
onInited查询默认值 onInited: function() { DateUtils... } createTimeStart = 7天前
refresh条件联动 refresh: function() { if(...) ... } status=='CONFIRMED' → 禁用编辑
returnTabs新增入口 returnTabs: function() { ... } 间接新增:先切换tab再调用add
Detail操作拦截 if(source == '补货建议单') return 阻止删除操作
preSave校验 preSave: function() { if(...) return false } 日期范围校验
静态数据枚举 var xxxStr = [...] 下拉选项列表
FTL页面结构 searchForm、datagrid定义 查询表单字段清单

以billDeliveryNt.js(配货单,1065行)为例,AI从中提取出了12条可验证的业务规则,覆盖了新增默认值、查询日期范围、字段联动、操作拦截等关键逻辑。

3.2 标准用例模板

基于规则提取结果,我们定义了6种标准用例模板,每个模块按条件生成:

模板 生成条件 验证内容
query-components 始终 查询组件初始化状态 + 默认日期
query-submit-contract 始终 表单值正确传递到datagrid查询参数
add-toolbar-flow 始终 新增按钮→切换tab→初始化默认值
field-conditional-logic refresh有条件分支 字段联动(启用/禁用/显隐)
operation-guard Detail有拦截条件 操作被阻止 + 提示信息
save-validation preSave有校验逻辑 校验拦截 + 错误提示

前三个是每个模块的基线用例,后三个根据源码中的实际逻辑按需生成。这种"先基线、再按需"的策略,确保了覆盖面的同时避免了过度测试。

3.3 Skill/Agent工作流

我们将上述能力封装为一个可复用的Skill + Agent:

.claude/
├── agents/auto-test-gen.md              # 独立Agent定义
└── skills/auto-test-gen/
    ├── SKILL.md                         # 工作流指令
    ├── references/                      # 规则参考
    │   ├── extraction-rules.md          # 源码规则提取规则
    │   ├── case-templates.md            # 6种用例模板
    │   ├── eui-api-reference.md         # Helper API速查
    │   ├── page-model.md               # 页面模型
    │   └── login-and-navigation.md     # 登录导航策略
    ├── templates/                       # 输出文件模板
    │   ├── case-config.js.md
    │   ├── focused-suite.js.md
    │   ├── smoke.yaml.md
    │   ├── manual-spec.yaml.md
    │   └── oracle.yaml.md
    └── assets/                          # 框架快照(bootstrap用)
        ├── lib/ (10 files, ~3200行)
        ├── easyui-test-helper.js
        └── ...

Agent支持四种工作模式:

Init模式(新模块全量生成):

指令:为 bill-return-nt 生成E2E测试用例
流程:解析模块ID → 定位源码 → 提取业务规则 → 生成测试套件
产出:6个文件(配置、用例、数据、规格、预期结果)

Update模式(增量更新):

流程:读取现有测试 + 最新源码 → 对比覆盖规则 → 仅补充缺失用例
约束:不修改已有focused-suite.js(保护手工补充内容)

From-Requirement模式(需求驱动):

流程:理解需求文本 → 映射源码区域 → 生成针对性用例
要求:每个需求点至少一个正向 + 一个反向用例

Bootstrap模式(新项目部署):

流程:检查目标目录 → 复制框架文件 → 创建配置 → 提示下一步

3.4 生成的测试用例长什么样

以配货单(bill-delivery-nt)为例,AI生成的focused-suite.js结构如下:

module.exports = {
  module: 'bill-delivery-nt',
  cases: [{
    id: 'bill-delivery-nt-query-components',
    layer: 'ui-structure',
    steps: [
      { op: 'navigate', url: '${menuUrl}' },
      { op: 'eval', code: 'return EUI.exists("#searchForm")' },
      { op: 'eval', code: 'return EUI.getDateboxValue("#createTimeStart")' },
      { op: 'eval', code: 'return EUI.getDateboxValue("#createTimeEnd")' },
      // 验证:createTimeStart=7天前, createTimeEnd=今天
    ]
  }, {
    id: 'bill-delivery-nt-add-toolbar-flow',
    layer: 'ui-flow',
    steps: [
      { op: 'eval', code: 'EUI.clickBillToolbar("add", {zone:"top"})' },
      { op: 'eval', code: 'return EUI.getComboValue("#bizType")' },
      // 验证:bizType=NORMAL_DELIVERY, validDay=7, sendDate已启用
    ]
  }]
};

配套的datasets/smoke.yaml提供测试数据,oracle.yaml定义预期结果,manual-spec.yaml提供人工可读的测试规格——四个维度共同定义一个完整的测试契约。

四、实战效果

4.1 闭环时间

以配货单(bill-delivery-nt)为第一个试点模块:

环节 传统方式 AI辅助方式
源码分析 & 规则提取 2-3小时人工阅读 Agent自动完成(< 1分钟)
测试用例编写 4-6小时 Agent生成 + 人工审核(~30分钟)
测试数据准备 1-2小时 从源码枚举值自动生成
环境搭建 & 调试 2-3小时 Bootstrap一键部署 + 首次调试
合计 1-2人天 < 1小时

从"源码提交"到"E2E测试可运行",时间缩短了一个数量级。

4.2 用例质量

AI从billDeliveryNt.js(1065行)中提取了12条业务规则,生成了3个基线用例。经过人工评审,识别出了9条尚未覆盖的高价值规则:

  • 字段联动:deliveryNtType值变化时sendDate的启用/禁用
  • 操作拦截:来源为"补货建议单"时禁止删除明细
  • 保存校验:preSave中的日期范围检查
  • 静态枚举:refBillTypeStr(10种单据类型)的完整性

这些规则在Update模式下可以增量补充,而不影响已有的手工测试内容。

4.3 可扩展性

框架的核心设计目标是一次投入、规模化复用:

  • 框架层(~4400行):CDP通信、Chrome管理、EasyUI Helper、用例编排、报告生成——对所有模块通用。
  • Skill/Agent(29个文件):规则提取逻辑、用例模板、输出格式——对所有模块通用。
  • 模块特有层(每个模块~200-400行):业务规则、测试数据、预期结果——由AI从源码自动生成。

这意味着,118个bill模块的E2E测试覆盖,不再是"118次重复劳动",而是"一次框架投入 + 118次自动生成"。

五、架构师的思考

5.1 方法论先行,技术跟进

这次实践最大的收获不是CDP或AI技术本身,而是SDD方法论为E2E测试提供了结构化的需求来源。

在传统的"先写代码再补测试"模式下,E2E测试用例的质量高度依赖测试人员的业务理解深度。而在SDD模式下:

  • 每个Spec对应一组明确的业务规则
  • 每条业务规则在源码中有固定的代码模式
  • 每个代码模式可以映射到标准用例模板

这三层映射关系一旦建立,AI的介入就水到渠成——它只需要按照规则提取模式识别、模板填充,而不需要"理解业务"。

5.2 Legacy系统的测试策略不必追求完美

118个模块不需要100%覆盖。我们的策略是:

  1. 基线覆盖:所有模块生成3个基线用例(查询、提交、新增),覆盖最核心的用户路径
  2. 按需增强:高风险模块(涉及金额、库存)补充字段联动、操作拦截、保存校验
  3. 变更驱动:每次代码变更自动触发增量更新,只补充受影响的测试

这种分层策略比"追求100%覆盖率"更务实,也更容易获得团队的认可和执行。

5.3 "写代码的同时产出测试"是可行的

传统认知是"先写代码,再写测试"。但在SDD + AI的辅助下,工作流变成了:

需求描述 → Spec → 代码实现 → AI从源码生成E2E用例 → 自动化验证
                                    ↑
                              无需人工介入

开发者提交代码后,AI自动分析变更内容,生成或更新对应的E2E测试用例,并运行验证。开发者无需编写任何测试代码,只需要审核AI生成的用例是否符合预期。

这才是"最后一公里"的真正打通——不是补齐测试覆盖率的数字,而是让E2E测试成为开发流程的自然延伸,而非额外负担。

5.4 对企业级Legacy系统改造的启示

许多企业面临类似的挑战:核心业务系统基于老旧技术栈,前端页面承载了复杂的业务逻辑,但E2E测试长期空白。我们的实践提供了一条可复制的路径:

  1. 识别共同模式:Legacy系统往往存在大量同构模块,识别它们的共同模式是规模化的前提。
  2. 构建领域专用Helper:不要试图用通用UI自动化工具适配Legacy组件,而是为特定技术栈(如EasyUI)构建专用Helper。
  3. 建立规则提取体系:将业务规则的代码模式结构化,为AI的介入建立确定性通道。
  4. 渐进式覆盖:从基线用例开始,按风险等级逐步增强,避免一步到位的阻力。

六、结语

E2E测试之所以成为"最后一公里",不是因为技术上做不到,而是因为ROI不够——投入产出比无法说服团队持续投入。

SDD方法论提供了结构化的需求来源,CDP提供了稳定的技术通道,AI提供了规模化的生成能力。三者结合,将E2E测试的成本从"每个模块1-2人天"降低到"每个模块不到1小时",从"手工编写维护"转变为"自动生成审核"。

当E2E测试不再是额外负担,而是开发流程的自然产出时,这最后一公里,也就不再是"最后一公里"了。


本文基于零售商品管理系统(GMS)的真实实践,项目包含118个bill模块、195个Controller、191个前端JS文件。测试框架代码约4400行,Skill/Agent包含29个文件,已为2个试点模块生成完整E2E测试套件。

继续阅读同主题文章测试与验证