
当单元测试覆盖率逐年提升,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%覆盖。我们的策略是:
- 基线覆盖:所有模块生成3个基线用例(查询、提交、新增),覆盖最核心的用户路径
- 按需增强:高风险模块(涉及金额、库存)补充字段联动、操作拦截、保存校验
- 变更驱动:每次代码变更自动触发增量更新,只补充受影响的测试
这种分层策略比"追求100%覆盖率"更务实,也更容易获得团队的认可和执行。
5.3 "写代码的同时产出测试"是可行的
传统认知是"先写代码,再写测试"。但在SDD + AI的辅助下,工作流变成了:
需求描述 → Spec → 代码实现 → AI从源码生成E2E用例 → 自动化验证
↑
无需人工介入
开发者提交代码后,AI自动分析变更内容,生成或更新对应的E2E测试用例,并运行验证。开发者无需编写任何测试代码,只需要审核AI生成的用例是否符合预期。
这才是"最后一公里"的真正打通——不是补齐测试覆盖率的数字,而是让E2E测试成为开发流程的自然延伸,而非额外负担。
5.4 对企业级Legacy系统改造的启示
许多企业面临类似的挑战:核心业务系统基于老旧技术栈,前端页面承载了复杂的业务逻辑,但E2E测试长期空白。我们的实践提供了一条可复制的路径:
- 识别共同模式:Legacy系统往往存在大量同构模块,识别它们的共同模式是规模化的前提。
- 构建领域专用Helper:不要试图用通用UI自动化工具适配Legacy组件,而是为特定技术栈(如EasyUI)构建专用Helper。
- 建立规则提取体系:将业务规则的代码模式结构化,为AI的介入建立确定性通道。
- 渐进式覆盖:从基线用例开始,按风险等级逐步增强,避免一步到位的阻力。
六、结语
E2E测试之所以成为"最后一公里",不是因为技术上做不到,而是因为ROI不够——投入产出比无法说服团队持续投入。
SDD方法论提供了结构化的需求来源,CDP提供了稳定的技术通道,AI提供了规模化的生成能力。三者结合,将E2E测试的成本从"每个模块1-2人天"降低到"每个模块不到1小时",从"手工编写维护"转变为"自动生成审核"。
当E2E测试不再是额外负担,而是开发流程的自然产出时,这最后一公里,也就不再是"最后一公里"了。
本文基于零售商品管理系统(GMS)的真实实践,项目包含118个bill模块、195个Controller、191个前端JS文件。测试框架代码约4400行,Skill/Agent包含29个文件,已为2个试点模块生成完整E2E测试套件。