最近在维护一套老项目的 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 f"page.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 测试,建议从一个小模块、一条核心业务链路开始试点,先把词表和审查流程跑顺,再逐步扩大覆盖面。过程中遇到最多的坑,永远不是技术,而是“人写的自然语言”和“机器想要的结构化”之间那层默契——一旦这层默契建起来,整个体系会越用越顺手。