自然语言驱动E2E测试:用例设计、转换引擎与稳定性治理实践 最近在维护一套老项目的 e2e 自动化用例时我越来越觉得别扭用例脚本里全是>用例: 登录后把商品加入购物车角标显示数量 前置: 使用账号 demo_user 登录系统 操作: - 打开首页 - 点击第一张商品卡片 - 点击加入购物车按钮 期望: - 购物车角标显示数字 1注意看这里面的表述不算“完全自由”每个句子都符合“动词 对象 可选参数”的形态。“打开”“点击”“输入”“断言”这类词来自动作词表。“登录系统”“商品卡片”“加入购物车按钮”来自页面对象词表。解析器要做的是把这些句子拆成(动作, 目标, 参数)的机器结构化数据然后翻译成可执行脚本。2.2 动作词表与页面对象的映射约定动作词表是整个系统的“菜单首页”所有能出现在操作里的动词都要提前登记。我初始版本只保留了十几个动词少了不够用多了容易互相混淆动作可接受的同义说法作用打开访问、进入、跳转到导航到指定页面或 URL点击按下、点一下、选择点击页面元素输入填写、键入、填入向输入框写入文本登录使用账号登录执行登录组合动作等待暂停、稍等显式等待元素或时长断言显示、出现、可见、包含断言页面状态或文本上传提交文件文件上传动作切换跳到新标签页切换页面或 iframe页面对象词表同样重要。每个业务概念比如“登录按钮”“商品卡片”“购物车角标”都要映射到实际的可定位方式。我建议直接在词表里维护三层信息业务名称、定位方式、动作权限。这样解析器一旦识别出“商品卡片”就知道去哪里找它。页面对象: - 业务名称: 商品卡片 定位符: css.product-card 允许动作: [点击] - 业务名称: 加入购物车按钮 定位符: xpath//button[contains(text(),加入购物车)] 允许动作: [点击] - 业务名称: 购物车角标 定位符: css.cart-badge 允许动作: [断言文本]这套映射的价值在于页面结构变了只改词表不生成新用例维护成本被压到最小。2.3 参数、数据与断言的表达方式自然语言里最难处理的就是参数和断言。光说“输入用户名”用户名是什么光说“购物车角标显示数量”数量是几所以我的契约里做了几条约定参数用等号或冒号内联比如“输入用户名 demo_user”“输入密码 123456”。数据表驱动需要跑多组数据时直接在用例里挂一张数据表操作里的动作会按行自动重复执行。断言统一用“显示、包含、等于”不要写“购物车角标应该是我刚才加的那个数”而要写成“购物车角标显示数字 1”。数据表示例数据表: 登录账号: demo_user 登录密码: 123456 商品序号: 1 期望角标数: 1这样解析器拿到数据表就能把用例里的占位符{{登录账号}}自动替换成demo_user。整个流程里人写的仍然是自然语言只是遵循了一个很小的约束集。2.4 一个完整的用例示例我把上面这些约定拼起来一个完整的用例长这样用例: 登录后添加商品购物车角标更新 前置: 使用账号 demo_user 登录系统 操作: - 打开首页 - 点击 {{商品序号}} 号商品卡片 - 点击加入购物车按钮 期望: - 购物车角标显示 {{期望角标数}} 数据表: 登录账号: demo_user 登录密码: 123456 商品序号: 1 期望角标数: 1这份文件放在仓库里既可以直接当测试文档给业务看也能被转换引擎识别并生成可执行的 e2e 脚本。我实测下来业务同事阅读这份用例几乎不需要额外解释这就是自然语言表达带来的最大收益。3. 转换引擎意图识别、槽位抽取与代码生成3.1 整体架构解析、映射、生成三层契约定好了接下来就是转换引擎怎么实现。我把它拆成三层解析层、语义映射层、代码生成层。解析层负责把 YAML 文本拆条按“前置、操作、期望”分类语义映射层负责把每条自然语言映射到动作词表和页面对象词表产出中间结构化数据代码生成层再把中间数据翻译成可执行的测试代码。这三层之间用中间 JSON 传递数据方便调试也方便将来替换任意一层的实现。我的目录结构大概是这样的nlp_e2e/ parser/ step_parser.py # 解析自然语言步骤 table_parser.py # 解析参数表 semantic/ intent_matcher.py # 动作意图识别 slot_extractor.py # 槽位抽取 object_resolver.py # 页面对象解析 generator/ code_generator.py # 生成测试脚本 static_checker.py # 生成脚本的静态校验 models/ test_definition.py # 中间结构化数据模型3.2 意图识别与槽位抽取怎么做意图识别是整个引擎的翻译官。我采用的是“规则优先模型兜底”的策略而不是一上来就全交给大模型。规则层先做几件事句子拆词找到动作词表中匹配的动词比如“点击”“输入”“打开”。去掉语气词和连接词比如“然后”“接着”“请”。从剩余文本里抽取目标对象和页面对象词表的业务名称做最长匹配。抽取参数匹配账号 xxx、密码 xxx、数字 N这类模式。举个例子句子“点击第一张商品卡片”经过规则层会被拆成{ action: click, target: 商品卡片, params: {index: 1} }如果规则层没有命中比如用户写了一句比较口语化的“把第一个加到购物车里”这时候我再调用大模型接口做意图补全把句子映射到标准动作和对象上。为了防止模型乱发挥我强制它只输出 JSON且必须从已有的动作词表里选动作、从页面对象词表里选目标。这里有个经验规则的覆盖面决定系统下限模型的兜底决定系统上限。纯规则系统维护久了会变成正则地狱纯模型系统又容易失控两边结合才平衡。3.3 代码生成从意图到可执行脚本语义映射产出中间 JSON 后代码生成就变得很机械。我的生成器是模板式的每个动作对应一个代码片段模板def generate_step(step: StepDefinition, root_template: str) - str: # click action if step.action click: target object_resolver.resolve(step.target) return fpage.locator({target}).nth({step.params.get(index, 0)}).click()生成的完整脚本示例def test_add_to_cart_after_login(): page runner.new_page() login(page, account{{登录账号}}, password{{登录密码}}) page.locator(.product-card).nth(0).click() page.locator(//button[contains(text(),加入购物车)]).click() expect(page.locator(.cart-badge)).to_have_text(1)所有生成的脚本最后会汇总成一个测试文件并自动注册到测试运行器里。每个用例都会生成一个独立函数函数名由用例名做 slug 化得到保证可读且唯一。3.4 为什么不用“纯LLM直接生成代码”我最早试过让大模型直接把 YAML 用例翻译成完整测试代码效果喜忧参半。喜的是简单场景生成得很快忧的是复杂场景会出现“幻觉代码”——调用了一个根本不存在的 API、把页面对象名称拼错、断言写错条件排查起来比手写代码还累。后来我改成“LLM 只生成中间结构化 JSON代码由模板引擎生成”。模型的任务缩小成“从自然语言里抽取意图和槽位”这个任务容错率更高即使某个槽位抽错了也能在接下来的人工审查里一眼发现。而模板生成的代码天然是可控的不会突然冒出不存在的 API。这个取舍我认为是整套方案里最重要的一步让模型做它擅长的事理解语义用模板做机器擅长的事生成代码两边都不越界。4. 不可信输出LLM生成用例的稳定性治理4.1 LLM生成代码的常见“翻车”现场只要系统里引入了大模型就得面对“不可信输出”这个现实。我跑了两个月翻车主要集中在这几类动作词乱搭模型把“点击”和“输入”搞混生成的动作虽然语法正确但语义不对。对象名称漂移抽出来的目标对象和页面对象词表不一致生成后找不到元素。多余创意模型自作主张加了一些页面里不存在的步骤比如“先清空购物车”“先关闭弹窗”导致用例误报。断言过度把一个简单文本断言升级成复杂的条件判断与原意偏离。这些问题有个共同点它们不容易被语法检查发现但很容易被语义审查发现。所以稳定性治理要从两个方向同时下手提高机器侧的自动校验能力同时在流程上保留人审环节。4.2 结构化输出约束与静态校验我采取的第一道防线是给模型输出做硬约束。调用接口时我明确要求返回一个固定 schema 的 JSON并且在后端做一个 schema 校验器不合格直接拒绝重试。schema 长这样{ intent: {$ref: /动作词表中存在的动作}, target: {$ref: /页面对象词表中存在的对象}, params: {type: object}, confidence: {type: number} }校验器会做两件事检查字段是否齐全检查枚举值是否在词表里。这一步挡住了大部分“格式合规但内容非法”的输出。第二道防线是静态校验。生成的代码不直接入库执行而是先通过语法解析器检查再做一次“干跑”。干跑时启动一个无头浏览器进入录制模式只访问页面不执行断言把所有定位器在真实 DOM 上试一遍。跑不通的用例直接标红返回给生成器重新生成。这道防线特别管用很多选择器写错、元素不存在的问题在干跑阶段就暴露了。4.3 自愈机制与人工审批兜底即使有校验e2e 用例跑起来还是会因为环境、时序问题而失败。我加了一个有限的自愈机制失败重试同一用例最多重试 2 次每次间隔 3 秒排除瞬时网络抖动。选择器降级主定位器失败时尝试用备选定位器从页面对象词表里读第二个定位符字段。快照对比失败时自动截图 导出当前 DOM方便事后定位是脚本问题还是页面问题。自愈机制只能解决“跑挂”的问题解决不了“用例写错”的问题。所以新用例默认进入草稿状态必须经过一次人工审查加一次真实环境回归才能进入正式用例子集。这个流程看着笨但能防止不可信输出污染整个回归结果。我宁可人工多看一分钟也不想让测试报告里全是“假阳性”。4.4 一个真实排查过程的复盘有段时间我们某个线上环境的购物车用例总在夜间回归时挂掉。初步看日志是“点击加入购物车按钮”这一步找不到元素但日间手动跑又是好的。后来我用快照比对了 DOM发现夜间运行时页面多了一个“新人优惠弹窗”把按钮整个遮住了。真实环境里这种“弹窗遮罩”和用例脚本没关系是页面本身的变化。但我这套系统在生成用例时压根不知道这台环境会有弹窗。后来我在动作词表里加了一个“先关闭弹窗”的动作模板并在特殊环境配置里标记为强制前置步骤问题才稳定解决。这个案例给我一个教训自然语言用例再智能本质还是封装了测试逻辑环境相关的隐性依赖必须要有人工沉淀到用例库里。纯靠自动生成很多环境上下文是感知不到的。5. 从Demo到工程化落地时绕不开的坑与收益盘点5.1 三个容易翻车的工程化细节第一个坑是中文分词和同义词。不同人写“点击”“点一下”“按下”是同一件事但在词表里如果只登记“点击”解析器就会漏掉。我建议维护一个同义词扩展表定期从历史未命中句子里捞新词人工确认后补充进去。这个过程要持续做系统才会越用越顺。第二个坑是对象漂移。页面对象词表如果和实际页面脱节生成再漂亮的用例也白搭。所以我把词表纳入版本管理每次前端改动时先更新词表再跑回归。词表里不仅要放定位符还要有“最后验证日期”字段超过一定时间强制重新干跑确认。第三个坑是异步等待粒度。自然语言里说“点击商品卡片”人知道要等页面加载完代码可不知道。如果模板生成器不给标准化的等待策略就会出现大量偶发抖动。我统一在生成器里为每个动作注入“前置等待 动作后等待”的默认值并把超时做成可配置参数。这样既保证了稳定性也保留了灵活性。5.2 在一段时间试跑后我看到的实际收益跑了一段时间后我统计了一下几个数字虽然不是严格的对照实验但趋势很明显用例编写时间下降新业务场景的用例从写描述到生成脚本整体耗时比手写代码减少约四成。用例评审参与度提升业务同事能直接看懂用例文本评审会上不再对着代码沉默而是真正提出覆盖场景的意见。定位器变更的修复成本下降页面改版时只更新词表里的定位符不用逐条改用例大部分用例不用碰。用例和需求文档的偏差变小因为用例本身就是文档需求变更时直接在用例文本上修改代码自动重新生成文档和脚本不会长期脱节。这些收益不是我拍脑袋预想的是真跑出来的。当然前提是团队愿意花时间维护词表和审查用例这本身是一种投入。5.3 什么样的团队适合引入这套方案说实话并不是所有团队都适合立刻上这套东西。我觉得有几个前置条件团队的 e2e 用例数量已经多到维护吃力且重复套路占比高。业务方有参与测试用例评审的意愿而不是只把测试当“验收工具”。团队至少有一两个人愿意投入建设词表和审查流程这决定了工具的“体质”。自动化框架本身有一定稳定性如果手工用例都频繁失败加自然语言层只会放大问题不会解决问题。如果团队规模很小、用例就几十条或者纯技术验证型项目不需要业务参与那这套方案的收益可能不明显。我一个朋友的团队试了一周就撤了原因很简单他们业务需求变化极快页面一天一改词表维护成本反而比改测试代码还高。任何工具都不是银弹选型还是要看自己的场景。我自己现在还在持续迭代这套系统下一步计划把页面对象词表的维护做成半自动化前端改动后用扫描工具对比 DOM 变化自动给出定位符的更新建议。这样词表维护成本还能再降一截。如果你也在折腾自然语言驱动的 e2e 测试建议从一个小模块、一条核心业务链路开始试点先把词表和审查流程跑顺再逐步扩大覆盖面。过程中遇到最多的坑永远不是技术而是“人写的自然语言”和“机器想要的结构化”之间那层默契——一旦这层默契建起来整个体系会越用越顺手。