自动化工具 聊一个绕不开的老问题:Web E2E 用例里的登录,能不能只做一次

w471176877 · 2026年10月01日 · 804 次阅读

一个大家都遇到过的小问题

「被测系统要登录」——做 Web 自动化的团队基本都绕不过去。最早的写法也都很直接:每条用例开头三步,打开登录页、填账号密码、点登录。

用例少的时候没感觉,等到几十上百条,问题会集中出现:

  • 每条用例都多跑一遍登录,回归时间凭空多出一截;
  • 登录页一改版(按钮文案、加验证码、多一步跳转),所有用例一起红,排查半天发现是登录流程变了;
  • 想测登录后的页面、想造数据,都得先把登录流程复刻对;
  • 账号密码散落在脚本里,换个环境就得全局改一遍。

这篇文章想聊的是:登录能不能从「每条用例里的一步」,变成「项目级的一次配置」。下面是我们这边的取舍,以及实现时真正踩到的坑 —— 后面这几条跟用不用 AI 没关系,都是 Web 自动化的老问题。

一、三种做法,先把取舍摆出来

做法 速度 抗改版 前置条件 典型问题
每条用例都走一次 UI 登录 慢 差 无 登录页一改版全局红;验证码 / 会话中断
调登录接口拿 token 塞 Cookie 快 中 依赖被测系统的登录接口 拿不到 localStorage 里的登录态,SPA 里还是「未登录」;接口一改即失效
录制一次浏览器登录态(Cookie + localStorage)复用 快 好 需要录一次 登录态会过期,所以必须能「重录」

我们选了第三种,理由很朴素:它复用 Playwright storageState() 那套东西,不依赖被测系统的任何接口,也不要求把账号密码交给工具。

二、设计:登录是「项目级配置」,不是「用例的一步」

三个定义决定了这个功能长什么样:

  • 存什么:登录后的浏览器状态 —— Cookie + localStorage,正好是 storageState() 的形状;
  • 挂在哪:项目级。一个项目可以有多份配置(管理员 / 普通用户、测试环境 / 预发环境各一份),可以设一份默认;第一份自动成为默认,删掉默认的会自动把最新一份提上来;
  • 用在哪:回放(用例详情里的「登录配置」下拉)、AI 生成(生成页同一个下拉)、项目默认(用例列表右上角)。

登录配置管理:弹窗里是已有配置列表 + 新增表单

登录配置管理:左侧蓝色提示写清了「打开浏览器 → 手动登录 → 停止并保存」的流程。

新增一份配置:填起始地址和配置名,点「开始录制」

新增配置:起始地址填登录页 URL,配置名可留空(自动命名),点「开始录制」才弹浏览器。

有一个刻意的取舍:不做自动登录。工具不存账号密码,也不会自动帮你填表单登录 —— 那意味着明文凭据入库,而且被测系统一加验证码 / 滑块就全线失效。我们做的是把「人工登录一次」的结果,搬运到之后的每一次运行里。

三、录制:为什么不能只调一次 storageState()

最直觉的实现是:打开有头浏览器 → 等用户登录 → 调一次 context.storageState() 存下来。真跑起来才发现两个坑。

坑一:storageState() 会为「已经离开的 origin」新建临时页面去读 localStorage。录制的同学会莫名看到一闪而过的窗口 —— 明明只打开了一个站点,任务栏却在闪。

后来改成只从当前仍打开的页面及其 iframe 里采集,同时保留已离开站点的最近一次快照:

// 只从现有页面/iframe 采集,避免 storageState() 为已离开的 origin 开临时页
const cookies = await context.cookies();
const origins = new Map(prev.map((o) => [o.origin, o])); // prev:已离开站点的最近快照
const frames = context.pages().filter((p) => !p.isClosed()).flatMap((p) => p.frames());
await Promise.all(frames.map(async (frame) => {
  const state = await frame.evaluate(`(() => {
    if (!/^https?:$/.test(location.protocol)) return null;
    return { origin: location.origin,
      localStorage: Object.keys(localStorage).map((name) => ({ name, value: localStorage.getItem(name) })) };
  })()`);
  if (state) origins.set(state.origin, state);
}));
return { cookies, origins: [...origins.values()] };

坑二:浏览器一关,Cookie 和 localStorage 就再也读不到了。

而「登录完随手关掉浏览器」恰恰是所有人的本能动作。所以录制期间每 250ms 采样一次快照,页面导航、页面关闭时补采;浏览器被关闭时,用关闭前最近一次成功的快照去保存。界面上那句提示就是这么来的:「录制或重新录制时关闭浏览器,会自动使用关闭前最近保存的状态创建或更新配置」;真想放弃就点「取消录制」——那是唯一会丢弃状态的路径。

实机录制:填好起始地址点「开始录制」

实机录制:填起始地址(登录页 URL)和配置名,点「开始录制」就会弹出浏览器等你手动登录。

另外两个细节,值得顺手提一句:

  • 凭据不进 WebSocket 广播。浏览器关闭事件只通过 WS 发一个「已关闭」通知,storageState 由前端回调 stop 接口时领取;客户端断线又没人来取的会话,10 分钟后清理。
  • 录制窗口用项目配置的视口(移动端页面可以设 390×844 之类),因为有些站点的登录态跟设备 / 视口是绑的。

四、回放:登录态怎么「塞回去」

回放侧不是 newContext({ storageState }),而是复用连上来的默认 context(回放通过 CDP 连接浏览器,没有新建 context 的自由),所以要手动注入:

// Cookie 直接加到 context
if (state.cookies?.length) await context.addCookies(state.cookies);

// localStorage 必须在页面脚本执行前写好,否则 SPA 首屏就判成未登录
await context.addInitScript((origins) => {
  const hit = origins.find((o) => location.origin === o.origin);
  if (hit) for (const { name, value } of hit.localStorage) localStorage.setItem(name, String(value));
}, state.origins);

这里踩过最隐蔽的一个坑:storageState() 里 origins[].localStorage 是 [{ name, value }] 数组。如果按普通对象的映射方式去遍历写入 localStorage,写进去的键名会变成数字下标、值变成 "[object Object]" —— 页面上看「注入成功了」,实际上登录态是空的,脚本一路跑成未登录。必须显式按 name / value 写入。

用 addInitScript 而不是「注入后再刷新一次」,是为了让登录态在页面任何脚本运行之前就位:SPA 通常在首屏 JS 里读 localStorage 判断登录,晚一步就会闪一下登录页,甚至直接跳走。

每次运行的日志里也会把登录态状态写明,避免「以为选了配置其实没生效」:

[浏览器窗口] 按项目配置使用 390x844
[登录配置] 未选择,以未登录状态运行
[0] goto → PASSED

真实运行日志:没选配置时会明说「以未登录状态运行」;选了配置则写明配置名与来源 origin。

运行日志里的 [登录配置] 行

五、几个必须说清的限制

  • 登录态会过期。有效期完全取决于被测系统(session 超时、Cookie 策略、单点登录各不同),工具替你保不了鲜。建议每次开始跑之前重录一次;管理弹窗里对已有配置提供「重新录制」,直接覆盖存储状态。用过期登录态跑出来的一批失败,非常浪费时间。
  • 只覆盖 Cookie + localStorage。如果被测系统把 token 放 sessionStorage / IndexedDB,这份状态里就没有。目前没做,因为这两类存储更偏「会话内」语义,跨次运行注入的收益也小 —— 如果你的系统正好这么干,欢迎聊聊你的处理方式。
  • 不做自动登录。确实要测登录流程本身(错误提示、验证码、锁定策略)的用例,仍然把登录步骤写在用例里,账号密码用环境变量 {{username}} / {{password}} 引用,别写死。
  • 多角色要多录几份配置,再在用例详情按用例选;暂时没做「按用例批量指定登录配置」。
  • 登录态本身是凭据:存在本地 SQLite 里,管理列表接口只返回名称 / 默认 / 时间这些元信息,不回传 storageState 本体。

项目列表:行操作里的 🔒 就是登录配置入口

六、想听听你们团队的做法

我们这边暂时收敛到「录一次、多处复用」,但登录这件事明显没有唯一解。我见过的做法还有:接口拿 token 再注入 Cookie、维护统一测试账号池 + 前置用例登录、干脆只用未登录态跑只读用例、以及把登录交给网关做免登(测试环境开后门)。

你们团队现在怎么处理登录态?踩过哪些坑? 欢迎在评论区聊聊,尤其是那些「看起来能用、跑一段时间就出事」的方案。

现状与限制

利益相关:我是 TestDog 的作者,本文写的是自己的实现与踩坑,不构成第三方评测。

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册。