ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Playwright的Web端到端UI自动化测试专项实践

基于Playwright的Web端到端UI自动化测试专项实践

如果你也是一个在 Web 项目里反复点页面的测试人,应该能理解我在接手“跑者天地”时的感受。“跑者天地”是一个面向跑步爱好者的社区类 Web 平台,核心功能包括跑步打卡、轨迹记录、赛事报名、社区帖子和个人数据统计。功能迭代两周一个版本,每次回归都要手工点核心链路,点得人发麻。这个专项的目标很明确:把那些高频、重复、容易漏点的回归工作,替换成一套能自动执行、自动检查、自动报障的端到端 UI 自动化测试体系。专项落地后,我们搭建了基于 Playwright 的测试框架,覆盖登录、跑步记录发布、赛事报名、社区互动四条核心链路,累计 30 多条用例全部接入 CI 流水线,稳定率稳定在 95% 以上。这篇文章记录的,是整个专项从 0 到 1 的全过程,包括选型时纠结过的框架、核心用例的设计思路、踩过的坑和排障细节,还有一些我特别想分享的稳定性处理经验。如果你是 QA 或者测试开发,又正打算给 Web 项目上 UI 自动化,这篇内容应该能给你省下不少时间。

1. 项目背景与自动化测试边界

1.1 跑者天地Web端的核心业务链路

设计一套有意义的 UI 自动化测试,第一步不是打开录制工具,而是先把业务链路理顺。我在专项启动前,专门花了大概一个下午,梳理了跑者天地 Web 端的功能地图,最后把测试重点收敛到三条必须保护的核心链路上,外加一个贯穿全局的数据验证场景。

第一条是跑步记录链路。用户从首页发起跑步,填写距离、时长、配速等信息,保存后生成一条跑步记录,同时个人中心的累计里程、月跑量等统计要同步更新。这是整个平台最基础也最高频的功能,几乎每天都有大量用户在使用,也是历次迭代中改动最多的模块,优先级最高。

第二条是赛事报名链路。用户从赛事列表进入详情页,阅读参赛须知,填写姓名、证件号、紧急联系人等信息,提交报名,最终在订单中心看到报名状态。这条链路涉及表单校验、名额库存、重复报名限制、订单生成等多个状态,是业务上最容易出问题、也最影响资金与声誉的环节,自动化价值非常高。

第三条是社区互动链路。用户浏览跑步动态、发表帖子、评论点赞。它的逻辑并不复杂,但涉及富文本编辑器、异步评论加载、点赞状态切换这些前端交互,是 UI 自动化比较容易踩坑的地方。再加上个人中心的数据统计页也离不开前端联动,我把“跑步记录发布后统计数字是否正确变化”单独抽成了一个数据校验专项用例。把这几个模块串起来,就形成了专项的主干测试矩阵,后续所有用例都围绕它们展开。

1.2 为什么这个阶段值得做UI自动化

启动专项之前,项目组里有一个很现实的声音:我们已经有接口测试平台了,为什么还要投入做 UI 自动化?这个问题值得正面回答,因为它直接决定了专项能不能拿到足够的资源和耐心。

接口测试覆盖的是接口层的数据逻辑,它能验证“给定请求返回了正确的结果”,但验证不了用户真正感知到的界面行为。举个具体例子:跑步记录的保存接口返回成功,但前端列表刷新逻辑如果有 bug,用户看到的还是旧数据,这时候接口测试是绿的,页面却是错的。UI 自动化存在的意义,就是补齐这最后一段用户可感知的链路,把“接口成功”和“用户看到成功”之间的缝隙堵上。

另一个原因是回归成本。团队两周一个版本,手工回归核心链路至少花掉一个人一天半。赛事报名、跑步记录这类涉及多步骤多状态的用例,手工点起来时间长,漏点还非常常见。自动化落地之后,每天夜间跑一遍全量,早上来直接看报告,该暴露的问题自动抛出来,整个反馈速度是完全不一样的。投入产出比很快就显现出来了。

1.3 测试范围边界:做与不做

UI 自动化不是万能的,范围没有划清楚的项目,后期一定会失控。我在立项时就把边界划得比较明确,既避免了后期维护量失控,也减少了扯皮。

专项重点做的是核心链路的端到端验证和回归,也就是上面列出的登录、跑步记录、赛事报名、社区互动、个人中心统计这几类用户可感知、跨模块、高频变动的场景。

同时,我也明确砍掉了一些内容。视觉像素级对比不做,因为设计稿还在快速演进,一个按钮改颜色的需求就能让视觉用例全线飘红,维护成本大于收益。性能测试不做,那是另一个专项的事情。纯接口的校验也不碰,已经有现成的接口测试平台在管。图片懒加载、富文本编辑器复杂排版这类前端特有问题,我采用了“只做关键断言”的方式,不追求端到端还原,比如发帖只校验帖子出现在列表里,不校验排版是否逐像素正确。

注意:UI 自动化最怕的不是功能点多,而是范围失控。每接一个新场景,都要先问自己三个问题:它是不是高频回归?失败后影响有多大?自动化维护成本能不能控制住?三个问题里至少有两个答案是“是”,才值得纳入专项范围。

2. 框架选型与基础设施搭建

2.1 主流框架对比与取舍

工具选型上,我认真对比了当前 Web 端 UI 自动化最常用的三个方案:Selenium、Playwright、Cypress。说实话,我在很长一段时间里是坚定的 Selenium 派,毕竟它生态成熟、资料多、什么语言都能写。但这次专项,我最终选了 Playwright,原因是它解决了我过去几年最痛的一个点——稳定性。

Selenium 的优势是兼容性和社区,劣势在于它本身不提供自动等待能力。以前写 Selenium 用例,最烦的就是到处加 sleep,加多了用例又慢又假,加少了又偶发失败。元素加载慢一点、网络抖动一下,测试就跑挂了,稳定性全看运气。虽然可以自己在封装层做等待工具,但每个团队维护一套此类公共逻辑,质量参差不齐,整体效果并不理想。

Cypress 对前端工程师很友好,API 设计现代,调试体验也不错。但它在多标签页、跨域场景的支持偏弱,而且默认只能跑在自己的测试运行器里,接入 CI 和扩展调试都要多做一些处理。跑者天地里存在不少第三方跳转的登录流程,这类场景用 Cypress 会绕不少弯路。

Playwright 最打动我的是三点:内置自动等待、Trace Viewer 调试信息、原生的多浏览器支持。自动等待意味着我不需要在每条用例里写一堆 sleep,框架会在元素可操作时才执行操作;Trace Viewer 会记录每一次点击、输入、请求响应和页面截图,排查失败时可以直接回放现场;多浏览器支持正好对应跑者天地要兼容 Chrome、Firefox、Edge 的需求,一套用例就能覆盖三个内核。

我整理了一张选型对比表,方便你有直观感受:

维度SeleniumPlaywrightCypress
自动等待需自行封装内置内置
多标签页与跨域支持支持弱
多浏览器支持支持支持
排查调试能力一般Trace Viewer 强较好
编程语言多语言Python/Node 等Node 为主
团队上手成本低但易滥用适中前端友好

2.2 测试环境与数据准备

UI 自动化对测试环境的依赖远高于接口测试。跑者天地早期的测试环境不太稳定,数据库经常被开发同学改来改去,用例一跑就是一片红,根本分不清是环境问题还是代码问题。这次专项我做了两个关键决定,把这类问题基本杜绝了。

第一个是搭建独立的核心测试环境。这个环境不承担开发自测任务,专门给自动化测试使用,数据库有固定实例,部署版本和线上版本保持一致。配合数据库初始化脚本,每次执行前可以把数据重置到已知状态,保证用例的可重复性。环境问题不是 UI 自动化专项里最难的,但它是很多人最常踩的坑,环境不稳定,后面所有工作都是在沙滩上建楼。

第二个是准备种子数据脚本。写一个脚本一次性造好测试所需的初始数据,包括若干个专用测试账号、每个账号预置的跑步记录、社区帖子、赛事数据等。种子数据不要放在用例里逐条创建,那样每个用例都要做前置步骤,又慢又容易互相干扰。集中创建之后,用例只需要依赖固定的账号 ID 和固定的数据标识,简单可靠。

2.3 框架目录与Page Object模型

框架搭建我没有把工程做得太复杂,一开始就遵循了一个适度结构化的原则,方便团队成员快速接手。最终的目录结构大致如下:

runners_ui/ ├── config/ # 环境地址、账号信息、超时配置 ├── pages/ # Page Object 页面对象 ├── cases/ # 测试用例 ├── data/ # 测试数据与种子脚本 ├── reports/ # 报告输出 └── utils/ # 公共工具:截图、重试、等待

Page Object 模型是这个框架的核心。简单说,就是把每个页面封装成一个类,页面上的元素定位和可操作动作都定义在类里,测试用例只负责按业务场景执行动作并断言结果。这样做的好处非常明显:页面结构改了,只需要改对应 Page 类,用例本身不受影响;多个用例要复用同一段操作时,直接从 Page 类调用,不用复制粘贴。

比如跑步记录页的 Page 封装里,我会暴露 start_run()、fill_metrics()、save_record() 这些方法,上层用例就是一行行方法调用,读起来像在描述业务步骤。这也是后面 30 多条用例能写得干净利落的关键基础。

3. 核心测试场景设计与实现

3.1 登录注册链路自动化

登录是所有链路的入口,也是自动化用例里最要命的一个模块,因为验证码处理不好,后面全是空谈。跑者天地的 Web 端支持手机号验证码登录、密码登录、第三方账号登录三种方式。

设计上,我把验证码问题拆成两级来处理:测试环境关闭图形验证码,短信验证码由后端测试接口直接返回固定测试码。这意味着用例里只需要在验证码输入框填入固定测试码即可,不依赖真实短信通道,稳定性和执行速度都有保障。前缀提醒一句:这种“万能验证码”机制只能在测试环境开启,上线前必须通过配置开关关闭,否则就是安全漏洞。我在 config 里单独建了一个 enable_static_code 开关,从根上防住误操作。

登录模块我封装了 LoginPage,核心方法如下:

class LoginPage: def __init__(self, page: Page): self.page = page self.phone_input = page.locator("#login_phone") self.code_input = page.locator("#login_code") self.login_button = page.locator("#login_submit") def login_by_phone(self, phone: str, code: str = "123456"): self.phone_input.fill(phone) self.code_input.fill(code) self.login_button.click() # 登录成功会跳转首页,这里等待首页标识出现 self.page.wait_for_selector(".home-dashboard", timeout=10000)

登录模块我设计了三条核心用例。第一条是成功路径:有效手机号加固定验证码登录,断言跳转首页;第二条是失败路径:输入错误验证码,断言出现“验证码错误”提示,并且停留在登录页;第三条是登录态校验:未登录状态下直接访问个人中心 URL,断言页面跳转到登录页。这三条覆盖了登录模块的主流程、异常输入和权限拦截,虽然看起来简单,但它们是所有后续用例的基座,必须稳定。

3.2 跑步记录发布与轨迹上传

跑步记录模块是跑者天地最核心的功能,UI 自动化要验证的不只是“能保存”,还包括保存后的数据正确回显和统计联动。

正常流程是这样的:用户从首页进入跑步页面,选择跑步类型,填写距离、时长、平均配速,点击保存,生成记录,跳转到记录详情页。自动化用例里,我重点做两个断言:一是记录详情中显示的距离、时长与填写值一致;二是回到个人中心,总里程统计增加了对应值。第二个断言尤其关键,它验证了前端列表和数据统计联动的正确性,这是纯接口测试覆盖不到的盲区。

这里有个细节需要特别注意:记录详情页会展示一张轨迹地图,依赖第三方地图渲染引擎,地图素材加载往往比普通 DOM 元素慢。如果只判断“地图容器出现了”,可能容器是空的,canvas 还没画出来。我的做法是等待地图组件内部暴露的“渲染完成”标记,比如一个只有在异步加载结束后才会出现的子节点,或者地图实例挂载后写入的特定 class。写过这类场景的人都懂,这个等待点找对了,用例稳定性会提升一大截。

再补充一个 GPX 轨迹上传的用例。用户可以从文件上传入口提交一份 GPX 轨迹文件,系统解析后在地图上绘制轨迹。测试时要准备标准 GPX 文件和异常 GPX 文件。标准文件上传后断言轨迹点数展示正常、地图进入轨迹展示状态;异常文件上传后断言页面出现“文件格式不支持”的提示。GPX 文件用最小有效集合就够了,我之前见过有人为了“尽量真实”传了十几 MB 的文件,结果用例跑一次要几十秒,完全没有必要。

3.3 赛事报名全流程

赛事报名是跑者天地里业务支撑价值最高的模块,流程本身长,状态又多,是非常适合自动化的场景。完整流程是:从赛事列表页选择一个赛事,进入详情页,阅读参赛须知并勾选同意,填写姓名、身份证号、紧急联系人等信息,提交报名,跳转到报名成功页。

自动化用例设计上,我关心三个状态:报名成功主路径、名额已满的抑制路径、已报名用户的重复报名提示。成功主路径要覆盖表单必填项校验:不填姓名直接提交,应该出现“请输入姓名”的提示;填对全部信息则提交成功,跳转到成功页,并在订单中心出现对应的报名记录。这属于验证前端表单校验与订单生成的联动。

满员状态在真实环境很难稳定模拟,因为名额是随时变化的。我在种子数据里预置了一个名额为 0 的赛事,用例打开这个赛事详情页,直接断言报名按钮为禁用状态且展示“名额已满”。这种思路摆脱了对动态业务数据的依赖,稳定且好维护。

重复报名场景则是预置一个已经报名过某赛事的测试账号,再次进入报名页时断言页面出现“您已报名该赛事”的提示,报名按钮不可点击。这类状态用真实业务数据很难构造,但通过种子数据可以任意堆叠,这也是为什么我反复强调种子数据脚本是基础设施。

3.4 社区互动与个人中心统计

社区互动功能逻辑上不算复杂,但它影响平台用户留存,使用频率也很高。我的自动化覆盖了发帖、回帖、点赞三件事。发帖用例关注富文本编辑器:填入标题和内容,点击发布,断言新帖子出现在列表最前面。回帖用例验证评论输入框提交后,评论区出现新评论且评论数从 0 变成 1。点赞用例关注点赞后的按钮状态变化,从空心到实心,点赞数加 1。这三个动作都是用户每天高频使用的功能,任何一次迭代改动都可能影响它们,放在回归集里非常有必要。

个人中心统计页我设计了一个“数据联动校验”的用例:先用种子数据构造一个测试账号,该账号预置 5 条跑步记录,总里程固定;然后执行一遍跑步记录发布用例,新增一条记录;再回到个人中心,断言总里程等于预置里程加新增里程。这个用例把跑步记录发布和个人统计串成完整链路,一旦统计逻辑出错,自动化就能在第一时间暴露。相比反复校验单个页面,这种跨页面联动断言的价值更高,也更接近用户真实的使用路径。

此外,空数据状态一定要测。新注册账号没有任何跑步记录时,个人统计页应该展示引导文案“还没有跑步记录,去开始你的第一跑吧”,而不是一片空白。空状态是前端最容易漏测的场景,很多实际 bug 都是在没有数据时出现的,自动化覆盖这个点,投入产出比极高。

4. 执行调度与稳定性加固

4.1 用例分级与CI调度

用例建起来之后,执行策略决定了它的使用体验。我把用例分为两级。

第一级是冒烟集,对应登录、跑步记录发布、赛事报名成功、发帖这四条主链路,每次版本发布前必须在 CI 流水线上强制通过。这部分用例控制在 10 条以内,执行时间控制在 5 分钟以内,追求的是第一遍必须全绿。冒烟集是开发提交代码后最快的一道自动防线,跑不通就直接打回,不让问题流到测试环节。

第二级是全量回归集,包含全部 30 多条用例,覆盖异常路径、数据联动、空状态等细节场景。全量集每天夜间定时执行一次,第二天早上团队直接看报告。夜间执行的好处是执行时间不受研发联调影响,环境也相对安静。我在 CI 流水线上加了触发规则:定时任务跑全量,标签触发跑冒烟集。开发在提交代码时也可以手动触发冒烟集,这样核心流程在合并前就能先跑一遍。

4.2 等待策略:告别脆弱sleep

UI 自动化稳定性最关键的一点是等待策略,没有之一。Playwright 内置了动作等待,click()、fill() 这些方法会自动等元素可见、可交互之后再进行操作,这已经解决了 70% 的问题。但业务层面有些等待仍需自己定义,比如“等待列表数量从 5 变到 6”“等待统计数字更新为指定值”,这些属于业务状态的等待。

我封装了一个断言等待器,用于等待某个 UI 状态达到预期:

def wait_until(page, expect_func, timeout=10000, interval=500): end_time = time.time() + timeout / 1000 while time.time() < end_time: try: if expect_func(): return True except Exception: pass time.sleep(interval / 1000) return False

这种条件等待方式配合显式断言,能做到不早不晚,稳定又不会白白浪费时间。我不建议在用例里写死 sleep(5),因为不同机器、不同网络环境下同样的 5 秒含义完全不同。条件等待是依据实际页面状态判断的,网络慢就多等,执行快就少等,这才是合理的等待逻辑。

4.3 失败处理与可观测性

再完善的用例也有跑挂的时候,关键是挂完之后你能否在几分钟内定位问题。我的做法是三重可观测性。

第一重是失败截图。每个用例失败时自动捕获全屏截图,保存到报告目录。截图时机要控制在断言失败的那一刻,因为这时页面还停留在现场,能看到真实问题所在。第二重是录制视频。Playwright 可以给整个会话录制视频,失败后把对应视频片段一起归档,很多时序和动画问题通过回放看得非常清楚。第三重是 Trace 文件。这是 Playwright 特有的调试信息,记录完整的操作步骤、网络请求和响应,配合它自带的调试工具可以逐帧查看整个用例的执行过程。

三层信息都会通过告警通知随报告一起推送给团队成员。在重试机制上,我在用例级配置了失败重试 2 次。这里有一个关键提醒:重试必须是幂等的。像报名、发帖这类会产生数据的用例,重试前要保证数据源被重置,否则第二次执行会因为“已报名”或者“帖子已存在”而失败,重试反而帮了倒忙。对于写操作用例,我通常会给它们单独配一套专用账号,每次执行后用脚本清理数据,确保重试环境干净。

5. 专项推进中的典型问题与排障实录

5.1 元素定位到了却被遮挡:弹窗与消息提示干扰

专项第一批用例刚接入 CI 时,稳定性一度只有 70% 左右。最典型的假失败现象是:元素明明定位到了,点击却报超时。排查几次后发现,罪魁祸首是页面上的消息提示和弹窗浮层。

跑者天地保存跑步记录后,右上角会弹出一个轻提示。提示出现的时候,它恰好覆盖了下一步要点击的“完成”按钮,用例傻傻地去点击,就一直点不到,直到超时。解决方案有两个:一是点击前先滚动到元素,确保它在可视区;二是等待遮挡浮层消失再操作。我在 Page 封装里统一加了一个 dismiss_floating_layer 方法,凡是页面有弹窗类遮罩的,操作前先调用一次,稳定性立刻有了明显提升。

5.2 全量回归大面积变红:环境数据被重置

有一次全量回归早上跑完,报告里 20 多条用例全红。第一反应是产品提了代码,但开发确认前一夜没有代码改动。逐个排查后,发现是测试环境数据库在夜里被重建了,种子数据全没了,测试账号也不存在。登录用例本身能过,但后续所有依赖这些账号的用例全部失败。

这件事让我意识到一个重要问题:执行前必须做环境健康检查,而不是等失败报告堆了一大堆才去排查环境。我在 CI 前置阶段加了一个 check_environment 脚本,先检查测试账号是否存在、种子赛事数据是否存在、核心页面是否可访问,任何一个检查不通过就直接中止执行并告警。环境健康检查是对抗环境问题的第一道防线,强烈建议每个 UI 自动化项目都加这一层。

5.3 地图组件加载缓慢导致的误报

专项跑了一段时间之后,我发现失败率最高的模块居然是跑步记录详情的地图渲染,而且失败还很不稳定,间歇性发作。一开始我以为地图相关功能有 bug,后来反复对比发现,问题出在我的断言时机上。

地图引擎依赖网络加载,慢的时候要十几秒。我最初的判断条件是寻找地图容器 div 是否可见,但后来发现容器 div 在初始化时就存在了,地图实际还没画完,断言自然就会偶发地过早失败。之后我把判断条件改成了等待地图引擎在容器内插入的特定子节点,这个节点出现才代表渲染完成,同时把超时时间从默认 5 秒加长到 15 秒。这里分享一个通用经验:第三方组件渲染完成的标志,不要通过“元素存在”来判断,而要找一个只有渲染完成后才会有的状态,比如特定 class、特定子结构或者某个数据属性。

5.4 报表与通知:让自动化结果真正被看到

自动化跑完,结果如果只躺在报告里,价值就打了一半折扣。我在 CI 流水线末端做了两件事:第一,生成 HTML 格式测试报告,包含每个用例的执行状态、耗时、失败截图和 Trace 链接;第二,通过团队 IM 机器人把结果推送到群里,内容包括通过率、失败用例列表和失败原因摘要。

推送消息里的失败原因摘要很有用,它会把断言失败时的期望值和实际值直接打出来,成员不用点进报告就知道大概问题出在哪。我还有一个工作习惯:每周统计同一条用例重试后的失败次数,如果某条用例连续多日重试后仍然失败,基本可以判定是真实缺陷或者用例设计有误,必须当天处理,不能任由它一直挂着。

专项落地跑了一个月后,跑者天地的 UI 自动化稳定率稳定在 95% 以上,冒烟集每次发版前都跑,全量集每天夜里跑。我个人最大的体会是,UI 自动化的核心不在于能写出多少个用例,而在于能不能把最核心的业务链路保护得稳稳当当。宁可先把 10 条冒烟用例打磨到极其稳定,也不要一口气堆 100 条三天两头红一地的用例。另外,重试后仍然失败的那几条用例,你要当成宝贝一样去排查,它们往往就是真实缺陷还没有修复的征兆。最后再分享一个小技巧:跨浏览器巡检这类慢任务,放到夜间全量集里跑,日常发布不要被它拖着走,团队体验会好很多。希望这篇专项报告,能帮你少走一点我曾经走过的弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进