news 2026/11/3 22:04:11

Playwright UI自动化测试实战:定位、分层、并发与稳定性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright UI自动化测试实战:定位、分层、并发与稳定性

简介:面向自动化测试初学者与需要跨浏览器验证的测试开发人员,这份文档以 Playwright 的 Python 实践为主线,从概念、环境搭建到脚本编写给出完整学习路径,可用于入门训练和日常用例参考。整包仅含 1 个 docx 文件,大小约 17.16MB,便于直接阅读与检索。内容覆盖 Playwright 概述、多浏览器支持,PyCharm 与 pytest-playwright 安装、内置浏览器下载,以及首条脚本、百度输入框输入、点击按钮、Web 断言、命令行参数、录制用例和 base_url 使用等实战环节;方法篇还涉及 click、提示信息定位与断言、go_to 导航、hover 悬浮和下拉框操作。已有 389 人学习,适合按目录逐章跟练,建立从环境配置到基础脚本调试的认知。

1. 一份从装环境写到并发重试的 Playwright 实战讲义

拿到这份 docx,我先翻的是目录,不是正文。从 Python 环境、pytest-playwright 安装,到 get_by_role 系列定位、PageObject 分层,再到 storage_state 免登录、pytest-xdist 并发、Allure 失败截图,这条线是串起来的。市面上讲 Playwright 自动化测试的资料大多停在“打开页面、点一下按钮”,真正让人半夜爬起来改脚本的是后半段那些东西——登录态怎么复用、三处超时分别管什么、并发下报告为什么缺用例、表格和表单怎么封装成能反复调用的组件。

这份资料的价值就在后半段:它把 UI 自动化从演示脚本推到了能进流水线的程度。适合已经会写 Python、准备把手工回归迁成自动化的测试同学,也适合要给团队定一套脚本规范的技术负责人。下面按“先立住原理、再动手复现”的顺序拆,中间该抄的代码直接抄,该避的坑我标出来。

2. 环境落地:Python 3.8+ 到三内核浏览器的最小可跑闭环

2.1 版本矩阵与系统依赖,先确认再动手

UI 自动化环境最容易出问题的地方不在代码,在“装了一半”。Playwright 的运行链路是三段:Python 解释器 → playwright 的 Python 包 → 真正被驱动的浏览器二进制(chromium、firefox、webkit)。三段里任何一段版本对不上,报错信息都会指向一个跟真实原因无关的地方,这就是典型的“黑匣子”体验。

这份资料里给的基线是 Python 3.8 及以上;Windows 10 及以上;macOS 13 及以上;Linux 侧覆盖 Debian 11/12 与 Ubuntu 20.04/22.04/24.04 的 x86-64 和 arm64。这套要求不算苛刻,但有两个真实约束要提前想清楚:

环节关键约束不满足时的表现
Python 解释器3.8+,建议单一版本,别混用系统 Pythonpip 装完 import 失败,或插件版本解析冲突
浏览器二进制由playwright install单独下载,不在 pip 包里Executable doesn't exist类报错
Linux 系统库需要字体、音视频、图形库等运行时依赖浏览器启动即崩,日志里是.so找不到
显示环境无头模式不需要 X11,有头模式需要--headed在服务器上直接失败

我一般会先在目标机器上跑一句python -V和pip -V,确认解释器和 pip 属于同一个环境。多解释器机器上最常见的翻车是:pip 装到了 A 环境,PyCharm 里选的解释器是 B 环境,代码一跑就ModuleNotFoundError,然后开始怀疑是不是包名写错了。

提示:CI 镜像里不要用系统自带的 Python 直接装。容器化跑的话,把浏览器缓存目录单独挂出来做缓存,能省掉每次流水线几百兆的重复下载。

2.2 pip 与 playwright install:四条命令的顺序不能换

安装这四步有严格的先后关系,尤其第一步别省。老版本 pip 在解析 playwright 的 wheel 包时偶尔会退回到源码编译,然后卡在编译依赖上报一堆看不懂的错。

# 1) 先升级 pip,避免旧版解析器在 wheel 上翻车 python -m pip install --upgrade pip # 2) 安装 pytest-playwright,它会自动把 playwright 作为依赖带进来 pip install pytest-playwright # 3) 下载三个浏览器内核到用户缓存目录 playwright install # 4) 仅 Linux 需要:补齐浏览器运行所需的系统库 playwright install-deps

第 2 步装的是pytest-playwright而不是裸的playwright,这个选择是有理由的:它把page、context、browser这些对象做成了 pytest fixture,还注册了--headed、--browser这类命令行参数,后面写用例时省掉大量样板代码。只装playwright也能写脚本,但等于放弃插件层的能力,团队协作时每个人都要自己搭一套 fixture,规范就散了。

第 3 步的下载产物不在虚拟环境目录里,而是落在用户级缓存:Windows 在%USERPROFILE%\AppData\Local\ms-playwright,macOS 和 Linux 在~/.cache/ms-playwright。这个路径很关键——CI 缓存要指向它,Docker 镜像的多阶段构建也要把它拷过去,否则运行时照样报“浏览器不存在”。

第 4 步是 Linux 专属。很多人本地 Windows 跑得好好的,一上流水线就崩,日志里是缺libnss3、缺字体、缺libgbm之类。install-deps干的就是补这些,需要 root 权限。Windows 和 macOS 跳过即可,但如果 macOS 上第一次运行被系统拦下来,去“隐私与安全性”里放行一次。

验证安装是否真的成功,别只看 pip 输出,写一条最小的冒烟脚本:

from playwright.sync_api import sync_playwright with sync_playwright() as p: # headless=True 是默认值,先跑无头确认二进制没问题 browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("about:blank") print(page.title()) # 空页面,只要能打印就说明链路通了 browser.close()

这段脚本只做三件事:启动浏览器、开一个空页、取标题。它绕开了网络和被测算系统,纯粹验证“Python 包 + 浏览器二进制 + 系统依赖”这条链路。跑不通就按 2.1 的表逐段排查,别急着写业务用例。

launch()里的参数值得单独说一句:headless控制有无界面,第一次排查问题我一般会临时改成False看真实渲染;slow_mo是给每一步操作加延迟,单位毫秒,只在调试时用;args可以塞浏览器启动参数,比如容器里的--no-sandbox。这三个是排查期最常用的旋钮,正式跑用例时都该收回去。

2.3 第一条脚本与命令行参数:--headed、--slowmo、--tracing 各管什么

环境通了之后,第一个业务脚本建议就写登录。登录是绝大多数系统的入口,也是后面免登录复用(storage_state)的基础。下面这条按 pytest 风格写,跟资料里的实战章节是一个路子:

from playwright.sync_api import expect def test_login_success(page): # page 是 pytest-playwright 提供的 fixture,不用自己 launch page.goto("/login") # 依赖 base_url 配置 page.get_by_label("用户名").fill("demo_user") page.get_by_label("密码").fill("demo_pass") page.get_by_role("button", name="登录").click() # 断言用 expect 而不是 assert 文本相等,它自带轮询等待 expect(page.get_by_text("工作台", exact=True)).to_be_visible(timeout=10_000)

逻辑说明:goto("/login")用的是相对路径,前提是配置了base_url,这样切换测试环境只改一处配置;get_by_label走的是表单标签关联,比起 CSS 选择器更抗前端改版;expect(...).to_be_visible()是 Playwright 的自动等待断言,在超时窗口内反复轮询,直到元素可见或超时,避免自己写time.sleep。

参数说明:timeout=10_000是毫秒,指的是这条断言的总等待预算,不是轮询间隔;exact=True表示文本完全匹配,不加这个参数时“工作台”会同时命中“工作台-我的任务”这类元素,导致 strict mode 报错——这个坑我踩过不止一次。

base_url的配置放在pytest.ini或conftest.py里都行,推荐后者,可以按环境变量切:

# pytest.ini [pytest] addopts = --headed --slowmo=300 --browser=chromium

命令行参数是这套环境里被低估的一块。--headed打开有头模式,--slowmo=300每步慢 300 毫秒,两者搭配就是“慢动作回放”,用来复现偶发问题时比打断点高效。--browser可以选 chromium、firefox、webkit,跨浏览器回归时直接在命令行切,不用改代码。--tracing=retain-on-failure只在失败时落 trace 文件,配合playwright show-trace可以逐帧回放当时的 DOM 快照和网络请求,这是排偶发问题的后悔药。

注意:addopts里写死了--headed上线会出大问题——无头服务器没有显示环境,直接启动失败。我的习惯是addopts里只留稳定参数,有头模式靠命令行临时加。

命令行还能传自定义参数,比如环境切换:

# conftest.py def pytest_addoption(parser): parser.addoption("--env", default="test", choices=["test", "staging"]) @pytest.fixture(scope="session") def base_url(pytestconfig): return { "test": "http://127.0.0.1:8000", "staging": "http://staging.internal", }[pytestconfig.getoption("--env")]

这里覆盖了插件自带的base_urlfixture,名字保持一致,用例里就不用感知差异。要注意的是覆盖内置 fixture 时签名必须对得上,否则会自动等待的地方拿不到值,报错信息还特别绕。

3. 元素定位与控件交互:把十几类操作压成可复用的写法

3.1 语义定位优先:get_by_role 到 get_by_title 的取舍

定位器的选择顺序,直接决定脚本能活多久。资料里把 Playwright 自带的定位器列了一整章,我按稳定性从高到低排一下实际使用顺序:get_by_role>get_by_label/get_by_placeholder>get_by_text>get_by_title> CSS > XPath。

get_by_role挑的是可访问性树里的角色和名称,等价于“用户看到并会去点的那个东西”。前端把div换成button、换套 UI 框架、改 class 名,它都不受影响。get_by_text稳在文案上,但文案是最容易变的东西——产品改一个字,用例就挂,所以要配exact=True收缩匹配范围。CSS 和 XPath 是最后手段,一旦用了就等于把前端实现细节焊死进测试代码。

# 语义定位:前端改样式不改语义,这几条基本不会因为改版挂掉 page.get_by_role("button", name="提交").click() page.get_by_role("link", name="订单管理").click() page.get_by_label("手机号").fill("13800000000") page.get_by_placeholder("请输入任务名称").fill("月度对账") page.get_by_text("保存成功", exact=True) page.get_by_title("刷新列表").click() # 退化到结构定位:只在语义定位实在拿不到时用 page.locator("div.el-table__body tr").first.click() page.locator("//button[contains(@class,'primary')]").click()

get_by_role的name参数支持正则,遇到“共 12 条”这种带数字的文案,用name=re.compile("共 \\d+ 条")比写死数字稳。exact=True这个参数值得养成肌肉记忆,Playwright 默认是子串匹配,一个“确定”能同时命中“确定”和“不确定”,strict mode 直接抛错。

还有一个细节:多个元素匹配到同一个定位器时,Playwright 不会像某些老框架那样“就用第一个”,而是直接报错。这个设计一开始很烦,习惯之后反而是保护——它强迫你把定位写精确。

3.2 表单六件套:input、textarea、select、radio、checkbox、switch

表单是后台系统里占比最高的交互。这六类控件的操作语义完全不同,混着写就是无穷无尽的“点了没反应”。

# 1) 普通输入框:fill 会先清空再输入,适合覆盖场景 page.get_by_label("任务名称").fill("月度对账-2024") # 2) 文本域:同样是 fill,换行用 \n page.get_by_label("备注").fill("第一行\n第二行") # 3) 原生下拉框:必须用 select_option,别用 click page.get_by_label("所属部门").select_option(label="研发中心") page.get_by_label("优先级").select_option(value="P1") # 4) 单选框:check() 幂等,已选中再调也不会取消 page.get_by_role("radio", name="按月").check() # 5) 复选框:uncheck() 取消,check() 选中 page.get_by_role("checkbox", name="同意服务协议").check() # 6) 自定义开关:没有原生语义,只能点外层容器 page.locator(".el-switch").first.click()

关键点在select_option。原生select元素在 Playwright 里被特殊处理,用click()展开再点选项,在无头模式下经常拿不到下拉面板(很多浏览器把原生下拉渲染在页面之外)。select_option直接改选中值并触发change事件,稳得多。它支持三种参数:label按显示文本、value按 option 的 value 属性、index按序号,能拿到 value 就优先用 value,文案会变,value 一般不变。

check()和click()的区别也值得记一笔。check()是幂等的——元素已经是选中态时它什么都不做;click()会在选中和取消之间来回切,用例重跑或者前置状态不确定时就会翻车。开关类控件(switch)通常没有原生语义,get_by_role拿不到,只能退到容器 class 上点,这类地方建议在 PageObject 里包一层,把不稳定的定位收在一个方法里,前端改版只改一处。

日期组件是另一类麻烦。原生input[type=date]可以直接fill("2024-06-01"),但多数后台用的是自绘日历面板,得点开面板再点日期格子。我的做法是:能直接fill到底层 input 的就fill,实在不行再走点击流程,点击流程一定要先expect面板可见再点日期,中间穿插动画的话不加等待就会点空。

3.3 上传、下载与拖拽:三个上下文管理器

这三类操作有个共同点:它们都由页面事件触发,不是普通的点击。Playwright 用expect_*上下文管理器来接这些事件,写起来比监听回调干净得多。

# 文件上传:先挂监听,再触发,顺序反了就拿不到 chooser with page.expect_file_chooser() as fc: page.get_by_role("button", name="上传附件").click() fc.value.set_files("data/import.xlsx") # 文件下载:同样先挂监听 with page.expect_download() as dl: page.get_by_role("button", name="导出报表").click() download = dl.value download.save_as(f"downloads/{download.suggested_filename}") # 鼠标拖拽:drag_to 内部模拟完整拖拽序列 page.locator("#drag-source").drag_to(page.locator("#drop-target"))

expect_file_chooser的坑在于顺序:必须先进入with块挂上监听,再执行触发上传的点击。反过来写会得到一个空的事件,报错信息是“等待超时”,看起来像是按钮没点到,实际上是监听错过。set_files支持单文件和列表,路径用相对路径时要确认工作目录是项目根目录。

expect_download拿到的对象里,suggested_filename是服务端返回的文件名,save_as决定落盘位置。这里有个并发问题:文件名如果写死,多进程跑的时候会互相覆盖,我一般会在文件名里拼上 worker id 或时间戳。

drag_to在标准 HTML5 拖拽上工作良好,但有些自研拖拽库(基于鼠标事件的)不认,这时候要退到手动序列:hover→mouse.down()→mouse.move()→mouse.up()。资料里在这一块给了鼠标拖拽的基本用法,实际操作中如果发现drag_to后元素没动,八成就是这个原因。

3.4 filter、nth/first/last 与 iframe 穿透

列表页和表格是自动化脚本的重灾区,因为元素长得一模一样。这里靠的是定位器的二次收窄能力。

# filter:在已有定位器集合上按文本/子元素再筛 rows = page.locator("table tbody tr") rows.filter(has_text="待审核").first.click() rows.filter(has=page.get_by_role("button", name="删除")).first.click() # 序号定位:nth 从 0 开始,first / last 是语法糖 page.locator(".task-item").nth(2).click() page.locator(".task-item").last.click() # iframe:用 frame_locator 而不是先取 frame 对象 frame = page.frame_locator("#content-frame") frame.get_by_role("button", name="确定").click()

filter(has_text=...)做的是子串匹配,适合“这一行里包含某个值”的场景;filter(has=locator)更精确,按子元素的定位器筛,适合“这一行里有个删除按钮”这种结构化条件。两者可以链式叠加,但叠加过多时建议直接写一条更精确的 CSS,可读性更好。

iframe 这块,frame_locator是推荐写法。老写法是先page.frame(name=...)拿到 frame 对象,问题是 frame 的加载时机不确定,拿早了会拿到 None,还得自己加重试。frame_locator是惰性的,每次操作时才去解析,天然带等待。多层嵌套就链式调用frame_locator,但超过两层就该考虑是不是页面结构本身有问题了。

还有一种情况是元素在新标签页里。点击后context.expect_page()能捕获新页面对象,之后所有操作都切到新 page 上——注意是同一个 context 下的新 page,不是新开浏览器,登录态是共享的。

4. 从脚本到框架:pytest fixture 与 PageObject 的四层拆分

4.1 四层结构与 conftest.py 的职责边界

脚本能跑和框架能用,中间差的是一套分层。资料里给的四层是:用例层(test_*.py)→ 页面对象层(*Page.py)→ 组件层(Table、Form 之类)→ 基础设施层(conftest.py、数据构造、报告配置)。分层的目的不是好看,是让“前端改版”这件事的影响半径可控。

职责边界我是这么划的:

层只做这些事不该出现的
用例层组织业务步骤、写断言定位器、等待、元素操作细节
页面对象层一个页面的动作方法,返回 self 便于链式断言、测试数据
组件层表格、表单这类复合控件的通用操作具体业务语义
基础设施层fixture、配置、登录态、数据准备业务断言

conftest.py是整套东西的入口,它管三件事:浏览器与上下文的生命周期、登录态、环境配置。把它管好,用例层就能干净到只剩业务。

# conftest.py import pytest from playwright.sync_api import sync_playwright @pytest.fixture(scope="session") def browser_instance(): # 整个测试会话只启一次浏览器进程,worker 内共享 with sync_playwright() as p: browser = p.chromium.launch(headless=True) yield browser browser.close() @pytest.fixture(scope="session") def auth_state(browser_instance, tmp_path_factory): # 会话级登录一次,把 cookie + localStorage 落盘复用 state_path = tmp_path_factory.mktemp("auth") / "state.json" ctx = browser_instance.new_context() page = ctx.new_page() page.goto("http://127.0.0.1:8000/login") page.get_by_label("用户名").fill("demo_user") page.get_by_label("密码").fill("demo_pass") page.get_by_role("button", name="登录").click() page.get_by_text("工作台", exact=True).wait_for() ctx.storage_state(path=str(state_path)) # 关键:把鉴权信息序列化 ctx.close() return str(state_path) @pytest.fixture() def logged_page(browser_instance, auth_state): # function 级上下文:每条用例一个干净环境,互不串味 ctx = browser_instance.new_context(storage_state=auth_state) page = ctx.new_page() yield page ctx.close()

逻辑说明:browser_instance是 session 级,浏览器进程启动一次,省掉重复启动的开销;auth_state也是 session 级,登录一次把storage_state写成 JSON;logged_page是 function 级,每条用例开一个新 context,加载登录态但数据隔离。

参数说明:storage_state(path=...)保存的是 cookie、localStorage 和 IndexedDB 的快照,恢复时通过new_context(storage_state=...)传入;tmp_path_factory是 pytest 内置的临时目录工厂,用它存 state 文件的好处是每次运行目录都不同,不会读到上次的脏文件。

这里有个必须点出来的坑:不要把 fixture 命名为page。pytest-playwright 自带一个pagefixture,同名覆盖会导致插件的命令行参数(比如--headed)失效,现象是加了--headed还是无头跑,排查半天找不到原因。取名logged_page之类的自定义名,互不干扰。

4.2 fixture 作用域:browser、context、page 谁该是 session 级

作用域选错,是脚本“越跑越怪”的根源。三者的关系是:browser 管进程,context 管会话(cookie、存储、权限),page 管标签页。

对象建议作用域理由选错的后果
browsersession启动开销大,进程可复用function 级会导致每条用例重启浏览器,慢十倍
contextfunction隔离 cookie 与存储,防止用例互污session 级会导致用例 B 看到用例 A 改过的数据
pagefunction跟着 context 走session 级会残留弹窗、滚动位置、焦点

我见过有人图省事把page写成 session 级,本地单跑全绿,一套跑就出现“上一条用例的弹窗挡住了下一条的按钮”。这类问题极难定位,因为单独跑任何一条都正常。默认就按 function 级走,确实需要同一个会话连续操作的,用同一个 context 里多开 page 来串。

多用户场景是 context 的真正用武之地:同一个 browser 里开多个 context,每个加载不同账号的 storage_state,就能模拟“A 提交、B 审核”这种双人协作流程。这也是 Playwright 相对老一代方案的核心优势——上下文隔离在进程内完成,不用起多个浏览器。

4.3 Table 与 Form 的组件化:把页面元素变成数据

后台系统里,表格和表单出现频率最高、重复代码最多。把它们抽成组件类,收益立竿见影。表格的核心思路不是“定位表格”,是“把表格当成二维数据结构来读”。

class Table: def __init__(self, root): self.root = root # 传进来的是表格根节点的 locator def headers(self): return [t.strip() for t in self.root.locator("thead th").all_inner_texts()] def cell(self, row_index, col_name): # 表头名 → 列序号,避免把列序号写死在用例里 col_index = self.headers().index(col_name) return self.root.locator("tbody tr").nth(row_index).locator("td").nth(col_index) def row_dict(self, row_index): # 一行 → 字典,断言时可以直接 row["状态"] == "已完成" cells = self.root.locator("tbody tr").nth(row_index).locator("td") return {name: cells.nth(i).inner_text().strip() for i, name in enumerate(self.headers())} def column(self, col_name): # 一列 → 列表,适合整体校验 col_index = self.headers().index(col_name) return [r.locator("td").nth(col_index).inner_text().strip() for r in self.root.locator("tbody tr").all()]

逻辑说明:headers()每次实时取表头,拿到的是当前渲染的列名顺序;cell()用列名换序号,列顺序调整时用例不用改;row_dict()把整行映射成字典,断言可读性最好;column()返回一列文本,做全量校验时一次拿完。

参数说明:row_index从 0 开始,跟nth()保持一致;all_inner_texts()返回的是列表,比循环取文本快,但要注意它不包含input里的值,遇到可编辑单元格要换成input_value()。

表单组件的封装思路不同,重点在“按控件类型分派”。写一个fill_form(page, data)的方法,传进去一个字典,内部根据字段的控件类型走不同分支:文本框fill、下拉select_option、单选check、开关click。这样新增一个表单,只要把字段名和值写成字典,不用重复写操作代码。

def fill_form(page, data: dict): for field, value in data.items(): # 字段名直接当 label 用,靠语义定位拿到控件 locator = page.get_by_label(field) if locator.count() == 0: raise ValueError(f"表单中未找到字段: {field}") locator.fill(str(value)) # 文本类统一走 fill

这段是骨架,实际项目里要按控件类型加分支。好处是把“填表单”从一个需要写十几行的动作压成一行调用,用例的意图变得清晰,维护成本也集中在组件类里。限制也要说清楚:这套依赖“label 文本 == 字段名”的约定,遇到 label 和实际业务字段对不上的系统就不适用,得改成显式映射表。

5. 避坑与排查:登录态、超时、并发三处最容易翻车的地方

5.1 登录态与上下文的三条血泪记录

现象一:本地跑全绿,流水线上第一条用例就停在登录页,之后所有用例跟着失败。原因:storage_state文件是用相对路径写的,CI 的工作目录和本地不一致,读到的是一份旧的、cookie 早就过期的文件。Playwright 载入 state 时不校验有效期,等到访问受保护页面被重定向才暴露。解决:state 文件路径一律用绝对路径(tmp_path_factory生成的路径天然是绝对路径),并且在会话开始时加一条自检——登录态失效就用账号重新登一次,别直接复用。我把这条自检做成 fixture,跑之前先访问一个需要鉴权的接口,状态码不对就重登。

现象二:单进程跑得好,-n 8并发时登录接口被打出一堆限流报错,甚至账号被临时冻结。原因:auth_state虽然是 session 级,但 session 的粒度是“每个 worker”而不是“整个 pytest 进程”。8 个 worker 就是 8 次登录,登录接口如果有频率限制,必挂。解决:把登录这一步前移到测试开始之前,用一个独立脚本生成 state 文件,所有 worker 只读这一份。多个 worker 同时写同一个文件时要加文件锁,避免读到写了一半的 JSON。这个改动之后,登录请求从 8 次降到 1 次,并发稳定性明显好转。

现象三:用例 A 改了一条数据,用例 B 断言失败,单独跑 B 又是绿的。原因:pagefixture 的作用域写成了 session,所有用例共用一个上下文,localStorage 里的筛选条件、弹窗的“不再提示”标记全都串了。解决:page/context一律 function 级,browser保留 session 级。隔离和速度之间,隔离优先——debug 一个串味问题花的时间,够跑几百次用例了。

注意:storage_state恢复的是快照,不是活的会话。如果被测系统在服务端做了会话失效(比如 token 只有 30 分钟有效期),长跑的任务要在中途刷新一次登录态,靠一次登录跑完整个套件是不现实的。

5.2 超时与并发报告的两条排查路径

现象四:偶发Timeout 30000ms exceeded,重跑就过,且报错位置每次都不同。原因:只配了default_timeout一处,导航超时和断言超时还是各自的默认值。三处配置不一致时,网络慢的场景下会出现“操作还没完成,断言先超时”的假失败。另一个常见原因是等的是元素“存在”而不是“可点击”,元素渲染出来但还在动画中,点击落空。解决:三处一起配,写在conftest.py里统一注入:context.set_default_timeout(15000)管元素操作,context.set_default_navigation_timeout(30000)管页面跳转,expect.set_options(timeout=10000)管断言。三者关系是“操作 < 断言 < 导航”,因为导航涉及网络往返,预算要留足。等待条件尽量用to_be_visible()、to_be_enabled()这类状态断言,少用固定的wait_for_timeout。

# conftest.py 里统一注入三类超时 @pytest.fixture() def logged_page(browser_instance, auth_state): ctx = browser_instance.new_context(storage_state=auth_state) ctx.set_default_timeout(15_000) # 元素操作 ctx.set_default_navigation_timeout(30_000) # 页面跳转 page = ctx.new_page() yield page ctx.close()

现象五:并发跑完,Allure 报告里用例数量对不上,失败截图串到了别的用例名下。原因:所有 worker 往同一个--alluredir写结果文件,文件名用时间戳生成,并发下撞名;截图按“用例名.png”存,同名用例直接覆盖。解决:每个 worker 写独立的 results 目录,跑完再合并;截图文件名里拼上uuid4和 worker id。

# 每个 worker 独立目录,跑完统一合并 pytest -n 4 --alluredir=./allure-results-${PYTEST_XDIST_WORKER:-main} allure generate ./allure-results-* -o ./allure-report --clean

pytest-xdist会给每个 worker 注入环境变量PYTEST_XDIST_WORKER(值是 gw0、gw1 这种),拿它拼目录名最省事。截图这块,用page.screenshot(path=...)时把uuid4().hex拼进文件名,再通过allure.attach挂到对应用例上,就不会串。

提示:重试机制要挑失败类型。断言失败重试有意义,定位器写错、元素根本不存在这类结构性错误重试多少次都是失败,白白拖长流水线时间。我一般只对超时和网络类异常开重试。

6. 高稳定用例的收口技巧:时间戳、预制资源与重试分级

写到这里,剩下的问题不是“会不会写”,是“写完三个月还跑不跑得动”。三个习惯能显著拉高稳定性。

第一个习惯:凡是新建数据的用例,名称一律拼时间戳。后台系统很少有真正的唯一约束校验,两条用例同时建“测试项目”,第二条就撞名失败,报错还指向一个跟名称无关的字段。做法很简单,模块级定义一个TS = datetime.now().strftime("%m%d%H%M%S"),用例里f"自动化项目-{TS}",并发也不会撞。

第二个习惯:预制资源。用例依赖的“部门、角色、字典项”这类基础数据,不要在用例里现建。现建的问题是:建完要等它出现在下拉框里,这个等待时长完全不可控;而且建失败了,后面一整串用例全挂。我的做法是跑之前用一个setup脚本把资源准备好,用例只负责“选用”,不负责“制造”。清理放在 session 结束的 teardown 里,按前缀批量删。

第三个习惯:重试分级。pytest-rerunfailures的--reruns参数,我最开始直接给 3 轮,结果一个有 bug 的用例跑了四遍、挂了四遍,流水线时间翻倍。后来改成两级:默认--reruns 1,只给标记了@pytest.mark.flaky的用例加轮次。重试轮次里,第一轮保留完整日志和截图,后面几轮只留失败摘要,报告体积能小一大截。

# 时间戳 + 前置清理,两条一起用才有效 TS = datetime.now().strftime("%m%d%H%M%S") @pytest.fixture() def temp_project(logged_page): name = f"自动化项目-{TS}" page = ProjectPage(logged_page) page.create(name) yield name page.delete_if_exists(name) # 收尾放 teardown,用例失败也能清

这个 fixture 有两个点值得说:yield之前的创建如果抛异常,yield之后不会执行,所以创建那一步要么加 try,要么接受脏数据留在环境里由定时任务兜底;delete_if_exists要容错,删除失败不该让本来通过的用例变成失败。

最后说一个验证习惯:改完 fixture 或者封装类,不要只跑一条用例就收工。跑一遍全量,看通过率,同时打开 trace 抽查两条,确认没有“靠重试才过”的用例混在里面。从那以后我每次动conftest.py都强制走一遍全量 + 抽查 trace,宁可多花十分钟,也不想第二天早上看到一片红。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 22:02:56

大模型辅助开题报告:选题、文献与技术路线的分工边界

一个模型、一次提问、一份看起来格式齐全的开题报告&#xff0c;这大概是每年开题季最常见也最容易翻车的操作。我见过不止一次这样的场面&#xff1a;报告交上去&#xff0c;格式漂亮、章节齐全、术语密集&#xff0c;导师翻到参考文献那页&#xff0c;随手点开第三条&#xf…

作者头像 李华
网站建设 2026/11/3 22:02:04

计算机组成原理习题解析:从流水线时空图到硬件信号级建模

简介&#xff1a;本资源是《计算机组成原理&#xff08;第六版&#xff09;》配套习题的权威解析手册&#xff0c;面向计算机专业本科生、考研备考学生及系统底层技术学习者&#xff0c;旨在系统攻克课程重点难点&#xff0c;强化对CPU结构、存储体系、总线机制、指令系统等核心…

作者头像 李华
网站建设 2026/11/3 22:01:59

WEB前端开发全流程解析与最佳实践

1. WEB前端开发流程概述前端开发作为网站建设的关键环节&#xff0c;负责将设计稿转化为用户可直接交互的网页界面。一个完整的WEB前端开发流程通常包含需求分析、技术选型、页面开发、测试调试和部署上线五个核心阶段。在这个过程中&#xff0c;开发者需要兼顾用户体验、性能优…

作者头像 李华
网站建设 2026/11/3 22:01:41

Codex CLI daemon权限报错排查:进程身份与文件属主错位修复

1. 报错现场还原&#xff1a;daemon 为什么总在权限上绊倒第一次遇到这个报错的人&#xff0c;十有八九会下意识敲一个sudo上去&#xff0c;然后发现命令确实跑起来了&#xff0c;但过两天又坏&#xff0c;而且坏得更隐蔽。Codex CLI 的权限类报错之所以让人抓狂&#xff0c;是…

作者头像 李华
网站建设 2026/11/3 22:00:42

Dell D-CIS-FN-01认证指南:IT基础设施运维入门

1. Dell D-CIS-FN-01认证概述Dell Infrastructure Services Foundations&#xff08;D-CIS-FN-01&#xff09;认证是戴尔科技集团推出的基础级技术认证&#xff0c;专门针对IT基础设施服务领域的专业人员设计。该认证验证持证者在戴尔服务器、存储和网络设备基础运维方面的核心…

作者头像 李华
网站建设 2026/11/3 21:58:51

数码产品高价无感化背后的消费心理与技术演变

1. 数码产品价格持续攀升背后的消费心理变迁最近两年有个现象特别有意思&#xff1a;旗舰手机从5000元档普遍涨到8000&#xff0c;笔记本电脑中高端机型纷纷突破万元大关&#xff0c;就连无线耳机都出现了4000元价位的新物种。但消费者面对这些价格标签时&#xff0c;反应却出奇…

作者头像 李华