☰
自然语言+回放缓存:E2E测试的高效低成本落地实践
2026/10/18 9:40:50 网站建设 项目流程

这期开源项目分享,聊一个很戳我痛点的工具:e2e。它解决的是我从接触AI辅助测试开始就惦记的问题——E2E测试能不能用自然语言写,写完之后又不至于每次跑回归都烧大模型的调用费用。这个项目给出的答案是:先用自然语言描述意图,让Agent把操作跑通,然后把跑通过的操作序列录成一份“回放缓存”,之后同样的场景直接读缓存执行,模型只在缓存没命中的时候才出场。简单说,就是让AI帮你探路,让脚本替你重复。

适合谁看?被E2E脚本维护折磨过的测试开发,刚接触AI编程想找落地场景的前端同学,以及所有对“AI+测试”感兴趣但担心成本和不确定性的人。下面我把这个项目的核心思路、实现机制、完整落地步骤和排障经验一次讲透。

1. 项目整体思路拆解:为什么E2E测试需要“自然语言+回放缓存”

1.1 传统E2E脚本的三大痛点

传统E2E测试脚本,说白了就是把用户操作翻译成代码:定位元素、执行动作、写断言。写一次不难,难的是后面的维护。

痛点一,选择器脆弱。页面改个class、调一下DOM层级、按钮文案从“提交”改成“确认提交”,脚本就可能崩。你花半天排查,最后发现只是前端同事把CSS类名换了个写法。

痛点二,流程编码僵化。每个用例都是一段线性代码,登录流程、下单流程、创建流程各写各的,遇到稍复杂的分支、异步加载、弹窗,代码里就塞满sleep和轮询,恶心且难读。

痛点三,页面改版连带故障。E2E脚本和页面结构深度耦合,前端每次重构,测试脚本就得跟着返工。返工不是改一行,是一整条链路重新定位元素、重新调试断言。

PageObject、数据驱动这些模式能在一定程度上缓解问题,但解决不了本质:你仍然需要靠人去读页面、写选择器、维护映射关系。智能化程度高一点的录制回放工具有了很大进步,但生成出来的脚本一旦页面变化,照样一片红。

1.2 LLM入场后的转机与新瓶颈

大模型出现后,自然语言直接驱动浏览器操作成了热点。你用一句话描述“打开登录页,输入账号密码,点登录,验证跳转成功”,模型就能把这句话拆成一系列可执行动作并完成验证。

这带来的体验是革命性的:测试思路从“写代码”变成了“说人话”。对业务理解深但对代码不熟的人也能写用例,这是个很大的门槛降低。

但如果每个用例每次都走模型实时驱动,问题同样接踵而来。

成本问题最现实。一次普通页面的操作,可能需要几千到上万token。一个回归测试包含几十个用例,每个用例跑一遍,费用很快就上去了。CI里每天跑几轮,一个月下来账单非常可观。

延迟问题也不容忽视。模型推理速度再快,也要一两秒甚至更久,几十步操作串行下来,单用例跑个一分钟很正常,整体执行时间被拉长。

更大的问题是确定性。同一句话,模型两次生成的步骤可能不完全一样,元素定位逻辑也可能漂移。E2E测试最讲究“可重复”,如果测一次一个样,这种不确定性会让测试结果很难让人信任。

1.3 “回放缓存”这个设计的巧妙之处

e2e这个项目把自然语言驱动的高灵活性和传统脚本的高确定性拼在了一起,中间加了一层缓存。

完整流程是这样的:第一次跑某个场景时,模型负责把自然语言转换成操作计划,Agent逐一步执行并校验结果。执行成功后,系统把这套已经验证过的操作序列录成回放缓存。之后再次跑同一个场景,系统直接读缓存,按照录制好的步骤快速重放,模型完全不参与。一旦页面结构变化导致缓存步骤失败,系统才重新唤醒模型,规划新路径,再把新路径回写进缓存。

拿生活中的例子类比:你第一次去陌生城市出差,要开导航,边走边看路牌,最后找到酒店。第二次再去同一个城市,你直接凭记忆走熟悉的路,不用再打开导航。发现路被挖断或封路了,才重新掏出导航。

这套设计把几个核心问题一次解决:运行成本降下来,因为绝大多数步骤走缓存并不调模型;执行速度和传统脚本一样快,甚至更快,因为省去了模型的推理时间;结果确定性也有了,回放走的是固定序列,不依赖模型情绪的波动;页面小变化也有了容错,因为缓存失败会自动降级回LLM探索,而不是直接报错。

用一张表看得更清楚:

维度传统脚本/PageObject纯LLM每次实时驱动自然语言+回放缓存
用例编写方式写代码,需要掌握框架API自然语言描述自然语言描述
每次运行成本只有算力,几乎为零高,token实时消耗极低,命中缓存时不消耗
执行耗时快,秒级慢,受模型推理速度限制快,与脚本相当
结果确定性确定性高,但脆弱不确定性明显确定性高,回放固定序列
应对页面小改版选择器一改就挂能自适应,但每次都要模型出场缓存失效时自动降级,模型重新规划
长期维护成本高,靠人力逐条修中,靠模型兜底但成本高低,只在页面逻辑变化时重录

2. 核心机制解析:从自然语言到可执行步骤,再到缓存命中

2.1 自然语言用例描述的正确姿势

e2e的用例文件本质上是一份带操作指令和断言的Markdown文档。它不需要你用任何框架语法,但也不是随便写一句人话就行。

我梳理下来,比较推荐的结构是:动作 + 目标元素 + 补充条件,三段式。目标是哪个页面、哪个按钮、哪个输入框,动作是什么,有没有额外条件,都写清楚。比如:

打开页面 https://demo.xxxxxx.com/login 输入框“用户名”输入 test_user 输入框“密码”输入 test_pass 点击按钮“登录” 等待出现“欢迎回来”

“点击登录”“输入账号”这种极简描述其实也能跑,但遇到页面上有多个相似文案的元素时,模型就得靠猜。稍微写具体一点,比如“页面右上角的登录按钮”“表单里的用户名输入框”,命中率和稳定性都会有明显提升。

断言也要用自然语言描述,但要注意描述的是“可观察的状态”,不是“抽象的业务逻辑”。比如断言“订单状态变为已支付”就不如“页面出现‘支付成功’提示且按钮变为‘查看订单’”直观。Agent是靠当前页面的DOM、文本、可见性来判断断言是否成立的,描述越贴近实际页面表现,校验越准。

2.2 Agent的执行链路:感知、规划、操作、校验

这一步是整个项目的技术核心。模型能不能把自然语言准确映射到真实页面,决定了缓存录出来的东西可不可信。

执行链路大致是:感知、规划、操作、校验,四步循环。

感知阶段,Agent先抓取当前页面的DOM结构、可访问性树、可见文本和关键控件信息,形成对页面状态的完整“观察”。这一步很关键,如果页面是用大量前端框架渲染出来的复杂应用,DOM树会很庞大,直接全量塞给模型会浪费大量token且降低响应速度,所以通常会做一轮精简,只保留按钮、输入框、链接、表单等可交互元素。

规划阶段,模型结合用户写的自然语言和当前页面状态,拆出下一个动作。可能是点击、输入、滚动、等待、切换iframe,也可能是断言校验。这里模型输出的是结构化指令,不直接去操作浏览器,而是交回给执行层处理。

操作阶段,执行层按照结构化指令调用浏览器自动化接口真实操作页面。如果目标元素有多个匹配,会按置信度选择最优的,拿不准时宁可多问一次模型把条件细化,也不要盲目点击。

校验阶段,Agent检查操作是否真正生效:点击后页面有没有跳转,输入后值有没有回显,按钮是否从禁用变为可用。校验不通过就触发纠偏逻辑,比如重新定位元素、换一种方式操作、或者继续观察等待异步渲染完成。

四个阶段循环推进,直到自然语言描述的所有步骤和断言全部完成,才算一次成功的执行。这个完整过程会被记录为操作日志和页面快照,理论上可以完整还原每一步发生了什么。

2.3 回放缓存的数据结构与匹配机制

缓存不是简简单单把操作步骤存成一段文本,它要解决“下次怎么找到这份缓存”“怎么判断这份缓存还能不能用”的问题。

一个缓存条目,通常包含这么几类关键信息:场景标识、操作序列、元素指纹、前置状态指纹、资源快照。

场景标识就是一个场景的唯一ID,比如根据用例文件路径和哈希值生成一个固定ID。操作序列是每一步的原子指令,例如OPEN_PAGE、FILL_INPUT、CLICK_BUTTON、WAIT_FOR、ASSERT_TEXT,每一步都带有具体参数。元素指纹用于描述目标元素的特征,不是简单的CSS选择器,而是一组特征集合:标签类型、可见文本、可访问性名称、在DOM里的相对位置、周围文本的片段。为什么不用单一选择器?因为页面调整层级时,单一选择器很可能失效,而特征集合匹配起来容错空间要大得多。前置状态指纹则记录执行到这一步时页面的状态摘要,比如URL、标题、关键元素的文本哈希。

匹配流程也挺清晰:先按场景ID找缓存条目,找不到就直接走LLM规划。找到了,就开始逐步骤重放。每一步都先校验当前页面状态与该步骤记录的前置状态指纹是否匹配:URL有没有变,目标元素还在不在,关键文本对不对。吻合就继续重放,不吻合就立即中断并触发LLM降级,同时把新规划的路径更新进缓存。

这里有个度的问题。严苛的匹配,几乎要求每一步都与录制时一致,安全性高但容易误报;宽松的匹配,只校验核心元素存在,容忍页面无关部分的变动,误报少但偶尔会把结构变但逻辑已变的情况放过去。项目默认是折中策略:核心断言元素必须精确校验,辅助元素允许存在缺失。

缓存的更新策略也很务实。场景新走通一条分支,会追加到缓存序列里;重放失败后模型修复了路径,系统会把旧的失败步骤替换掉;高频失败的步骤,会自动标记高优先级下次优先由模型重新规划。这实际上形成了一个滚动的自我完善机制,用得越多,缓存越贴合当前页面。

3. 实操落地:从安装到缓存生效的完整流程

3.1 环境准备与初始化

e2e以命令行工具的形式发布,Node.js运行时是基础要求,版本建议在18以上。浏览器侧需要有一份可调用的浏览器运行时,这块大多数环境下默认就有了,不需要额外配置。

安装直接用包管理器拉包就行:

npm install -g @e2e/cli

装完先在项目里做初始化:

e2e init

init命令会在当前目录生成一份配置文件,同时创建用例目录和缓存目录。默认结构大概是:

my-project/ e2e-scenarios/ login.e2e.md .e2e-cache/ e2e.config.json

配置文件里最需要关注的是模型接入参数和缓存策略:

{ "model": { "baseUrl": "http://your-llm-endpoint/v1", "apiKey": "sk-your-key", "name": "your-model" }, "browser": { "headless": false, "viewport": { "width": 1280, "height": 800 } }, "cache": { "dir": ".e2e-cache", "mode": "prefer-cache", "maxAgeDays": 30 } }

模型接口只要兼容主流API风格就能用,本地部署的模型也行。apiKey建议通过环境变量注入,别直接写死在配置文件里提交到仓库。

3.2 编写第一个自然语言测试场景

初始化完成后,在e2e-scenarios目录下新建一个场景文件,比如login.e2e.md。

一个完整的场景文件,我通常分成三块:场景说明、操作步骤、断言验证。操作步骤是主体,断言可以在步骤中穿插,也可以集中放在最后。

以一个“登录并创建草稿”场景为例:

# 登录并创建草稿 ## 场景说明 用户使用测试账号登录后台,进入控制台,新建一篇草稿。 ## 操作步骤 - 打开页面:https://demo.xxxxxx.com/login - 输入框“用户名”输入 test_user - 输入框“密码”输入 test_pass - 点击按钮“登录” - 等待出现“控制台首页” - 点击按钮“新建草稿” - 输入框“标题”输入:季度总结报告 - 点击按钮“保存草稿” ## 断言验证 - 页面显示提示“草稿已保存” - 列表中出现标题“季度总结报告” - 页面右上角显示当前用户“test_user”

这里有个容易踩的坑:场景描述里尽量不要写“如果”“假设”这类条件句,Agent对条件分支的处理能力虽然强,但会让执行链路变长、出错概率变大。一个文件专注一条主流程,分支和异常路径单独建场景文件,排查起来也清晰。

3.3 首次运行录制与缓存重放验证

场景文件建好后,第一次运行加上录制标记:

e2e run scenarios/login.e2e.md --record

首跑过程中,Agent会一步步规划并执行操作,控制台会打印每一步的来源:这一步是由模型实时规划出来的,还是一开始的意图解析,以及执行结果是否通过。首次跑完,日志里能看到类似这样的输出:

[1/8] OPEN_PAGE -> EXEC http://your-llm-endpoint 已访问,页面标题正确 [2/8] FILL_INPUT -> EXEC 用户名已输入,值回显正确 [3/8] CLICK_BUTTON -> EXEC 登录按钮已点击,等待页面跳转 [4/8] WAIT_FOR -> EXEC 已等待到“控制台首页” ... [8/8] ASSERT_TEXT -> EXEC 断言通过:“草稿已保存”

录制成功意味着这一整套可执行的操作序列已经被写入了缓存目录。之后不录缓存运行,命中缓存时日志会明显不同,每一步旁边会出现HIT_CACHE标记:

[1/8] OPEN_PAGE -> CACHE_HIT [2/8] FILL_INPUT -> CACHE_HIT [3/8] CLICK_BUTTON -> CACHE_HIT [4/8] ASSERT_TEXT -> CACHE_HIT

从HIT_CACHE这一步开始,模型就没有被调用过。

想确认模型是否真的没被调用,可以看两个指标:一是执行耗时,首跑登录场景通常需要几十秒,二次命中缓存后能压到一两秒,量级完全不一样;二是配置文件里的模型调用日志或token计数,如果二次运行后没有新增请求记录,就说明整个流程走的是纯回放。

3.4 缓存治理与日常维护命令

项目提供了几个比较顺手的缓存管理命令:

# 查看缓存状态,包括命中率和缓存条目 e2e cache status # 清理全部缓存 e2e cache clean # 强制某个场景重新走模型录制 e2e run scenarios/login.e2e.md --record --force

缓存默认目录是.e2e-cache,它里面的内容可以放进Git仓库,也可以忽略掉。放进去的好处是团队所有人都能共享已录制好的缓存,省去各自首跑的成本;缺点是页面变动后大家得记得同步刷新。我的建议是项目主仓库忽略缓存,另外单独维护一份缓存的话,用CI定期跑一次强刷生成。

如果项目跑的速度很快,但偶尔会莫名失败,先别怀疑是缓存问题,看看是不是执行环境不一致。本地浏览器和CI环境字体、分辨率差异,可能导致元素定位结果不同。项目里配置viewport统一成一个固定尺寸,能有效降低这类差异。

3.5 多场景编排与CI集成

单场景跑通只是第一步,真实项目里得把多个场景串起来才能进回归流程。

项目支持在一个文件里引用多个场景,也支持批量跑一个目录下所有场景文件:

e2e run scenarios/ --record

批量模式下,缓存就非常关键了。核心高频场景全部走缓存,新增或改动过的场景走模型,整体回归时间可以被压到一个很可观的量级。

CI里我建议这样配置:push触发的快回归只跑缓存,不允许模型调用,最大程度保证速度;每天定时任务跑一次全量回归,允许模型降级,页面结构变化时能自动修复路径并刷新缓存,同时把修复的diff推给相关人。这种组合配下来,既稳又省。

4. 常见问题与排障实录

4.1 页面改版后缓存大量失效

现象:某次前端重构后,跑回归时日志里大面积出现CACHE_MISS,很多场景回到模型驱动,速度和成本又回到了老水平。

排查方向:先区分是“页面结构变了”还是“业务逻辑变了”。结构变化,比如按钮位置调整、层级变化、文案微调,这种只需要让系统进入宽松匹配模式,核心元素检测到了就直接重放,很快能恢复大量缓存命中。逻辑变化,比如登录流程从两步变成三步,这种必须重新录制,千万不要强行沿用旧缓存。

我的操作习惯每次发布前主动跑一次全量录制,把缓存刷新到最新页面状态。与其等失败降级,不如提前主动重录。

4.2 动态文本与验证码类场景

动态文本问题:页面里有时间戳、随机订单号、当前日期这类每次都不一样的文本。如果断言写死“订单编号为2024001”,重放时会因为实际值是动态生成的对不上而失败。

处理办法:断言动态文本时描述“存在”“出现”,而不是“等于某个具体值”。例如写“页面出现‘订单号’字样”或“成功提示包含‘已提交’”这类相对普适的断言,就能避开这个问题。

验证码是另一个麻烦。这种场景我建议直接在测试环境关闭验证码,或者使用测试专用的万能验证码。如果实在不行,至少在自然语言里注明“跳过验证码校验”,让Agent知道这是无需验证的环节,不要被卡住反复等待。

4.3 Agent点错元素与断言误报

现象:元素有多个匹配,Agent选了第一个,但那个其实不是用户习惯操作的目标。断言通过,但测试结果失真。

这类问题多发于页面上有重复文案、重复按钮时。比如列表页每个表格行都有一个“编辑”按钮,Agent只根据“点击编辑”这句描述去定位,很容易点错行。

定位方法:一是看项目生成的执行快照,每一个步骤落盘时都有操作前后的页面截图,翻一眼截图就知道它点到了哪里;二是把描述写得更精准,给出行号或位置作为补充条件:例如“点击第三行右侧的‘编辑’按钮”;三是调整断言粒度,不只断言“编辑成功”,还断言“打开的是对应编号的记录”。

4.4 缓存正确性与过期风险

缓存最让人担心的不是“没用上”,而是“用错了”:页面结构看起来一样,但背后的业务逻辑已经变了,旧缓存依旧会跑通,测试变成摆设。

这是我强调最多的一点:缓存只是一条已探明的路径,不是业务正确性的证明。它最适合用在回归验证环节,用来快速确认“这次改动没有影响主要流程”,不适合作为发布前的唯一验收手段。核心业务场景,定期强刷缓存跑一遍模型重新校验,仍然非常有必要。

可以给缓存设置有效期,比如maxAgeDays设成30天,超过期限的缓存条目自动失效。这个期限切合项目发布节奏来定就好。

4.5 模型调用与成本观察

项目运行一段时间后,可以通过cache status看命中率。我会关注两个数字:场景命中率和步骤命中率。场景命中率是有多少场景完全走了缓存,步骤命中率是混合场景里有多少步走了缓存。

实测下来,页面结构迭代不频繁的项目,核心高频场景稳定后,步骤命中率能做到九成以上。剩余那不到一成的模型调用,基本都集中在新增页面和新场景上,token开销可以控制到很低的水平。

如果你的项目命中率一直很低,先别怪缓存机制,大概率是页面改版太频繁,或者说每个用例跑的目标页面结构差异太大。这时候可以牺牲一点速度,把cache mode调成prefer-llm,让模型实时规划为主,缓存只作为可选加速项,这个策略会明显更贴合频繁改版的项目阶段。

我个人在这一年多实践里的最大体会是,这个工具真正改变的是测试的书写方式和维护心态。以前写E2E,我写的是“怎么操作”;现在写e2e场景文件,我写的是“我想验证什么”。页面结构的变化交给缓存失效和自动降级去处理,我的注意力可以放回业务逻辑本身。如果你也被一堆脆弱的选择器折磨得够呛,手头又有可用的模型接口,强烈建议拿一个高频回归场景试试这套流程。

最后再分享一个小技巧:录制场景时,尽量选一个干净的初始状态,比如新建一个测试用户、清空历史数据。这样录出来的缓存才是“从零开始的完整路径”,而不是依赖某条已存在数据的半截路径。初始状态干净,缓存的可复用性会高出一大截。

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

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

立即咨询