1. 框架的起源从能用到好用的必经之路写 pytestplaywright 做 UI 自动化不算什么新鲜事网上一搜一大把 demo。但真正让人头疼的是跑通一个脚本只需要半小时把脚本变成一个能应对业务迭代、多人协作、环境切换的成熟框架至少需要两三周的重构和沉淀。这篇文章接的是前面两篇的基础——我们已经解决了 playwright 的环境搭建、pytest 的集成方式、以及怎么把两者结合起来跑最基本的用例。这一篇的核心任务是把这些散落的脚本按照工程化的标准重新组织成一套可复用的框架。也就是说解决几个痛点用例之间怎么传递登录态、怎么管理不同环境里的配置、复杂的业务怎么抽成公共方法、测试报告怎么做成能发出去看的 HTML、以及最关键的开销问题——能不能并行跑。这套框架的特性我先列一下后面逐个展开pytest fixtures 做依赖注入、conftest.py 做全局控制、Page Object 模式做页面封装、YAML 做数据驱动、Allure 做报告美化、pytest-xdist 做并行执行。整体不依赖任何重量级平台纯 pytest 生态装完依赖就能用拿到新项目里改改配置也能快速落地。我见过很多人一上来就搞自研测试平台把简单的自动化搞成了前后端分离的工程等到人走茶凉维护成本直接压垮团队。我的建议始终是先用轻量级框架把流程跑通等到用例数量和团队规模真的到了那个量级再评估是否引入平台层。这篇的框架方案就是轻量级里比较成熟的一条路。2. 框架设计思路先分目录再谈分层2.1 目录结构一眼能看明白项目的代码布局很多初学者喜欢把所有脚本塞到一个文件夹里跑是能跑但一旦用例数超过 30维护成本就开始失控。我的经验是目录结构在项目第一天就要定好宁可前期多花十分钟也别等一夜之间变成屎山再后悔。我常用的目录结构是这个样子ui_auto_framework/ ├── config/ # 配置文件目录 │ ├── __init__.py │ ├── settings.py # 全局配置项 │ └── env_config.yaml # 多环境配置dev/staging/prod ├── pages/ # Page Object 页面对象目录 │ ├── __init__.py │ ├── base_page.py # 页面基类封装通用操作 │ ├── login_page.py # 登录页面对象 │ └── home_page.py # 首页对象 ├── testcases/ # 测试用例目录 │ ├── __init__.py │ ├── conftest.py # 用例级 fixtures │ └── test_login.py # 登录模块用例 ├── data/ # 测试数据目录 │ ├── login_data.yaml │ └── user_data.yaml ├── utils/ # 工具类目录 │ ├── __init__.py │ ├── logger.py # 日志封装 │ ├── read_data.py # 数据读取封装 │ └── allure_utils.py # 报告辅助工具 ├── reports/ # 测试报告输出目录 ├── logs/ # 日志输出目录 ├── conftest.py # 全局 fixtures 和 hook ├── pytest.ini # pytest 配置 └── requirements.txt # 依赖清单每个目录职责单一、互相不越权这是框架能长期维护的基础。比如 pages 目录只放页面操作testcases 只放用例编排data 只放测试数据这样当业务变动时大概率只需要改 pages 里的某个文件testcases 层基本不动。2.2 分层原则Page Object 模式为什么是主流方案Page Object 模式简称 PO的核心思想是把页面上的元素定位和操作逻辑从这个页面里抽离出来封装成一个类。测试用例只关心业务动作不关心这个按钮的 xpath 是什么。举个例子未使用 PO 的测试代码是这样的def test_login(browser): browser.goto(https://example.com/login) browser.fill(#username, admin) browser.fill(#password, 123456) browser.click(button[typesubmit]) browser.wait_for_selector(.welcome) assert browser.text_content(.welcome-text) 欢迎回来看起来没毛病但这个用例如果要在三个模块里分别用到登录同样的代码就得复制三份。一旦页面结构改了三个地方都要改而且容易漏改。使用 PO 模式之后def test_login(login_page): login_page.login(admin, 123456) assert login_page.get_welcome_text() 欢迎回来页面元素和操作细节全部收敛到 login_page 对象里。如果登录页的输入框选择器变了只需要改 login_page.py 一个位置。纯 PO 模式还不够我会在这个基础上加一层业务层设计。简单说就是把更复杂的、跨页面的业务流也抽出来比如完整下单流程这一个动作可能在用例里只需要调一句 order_flow.complete_order(product_idxxx)但内部却调用了商品详情页、购物车页、结算页等多个页面对象。这种做法的好处在于用例的可读性直线上升测试报告里看起来就像人在一步步操作一样清楚。2.3 配置管理一个 yaml 搞定多环境切换UI 自动化的环境切换是个老大难。开发环境、测试环境、预发布环境的 URL 和账号体系都不一样如果这些信息硬编码在代码里每次切换都要改文件极其痛苦。我在框架里用了一层 yaml 配置来解决这个问题。# env_config.yaml default: dev dev: base_url: https://dev.example.com admin_user: username: devadmin password: dev123456 timeout: 10000 staging: base_url: https://staging.example.com admin_user: username: stagingadmin password: staging123456 timeout: 15000 prod: base_url: https://example.com admin_user: username: prodadmin password: prod123456 timeout: 20000然后在 settings.py 里通过环境变量来读取当前使用哪个环境import os import yaml with open(config/env_config.yaml, r, encodingutf-8) as f: env_config yaml.safe_load(f) ENV os.getenv(TEST_ENV, env_config.get(default, dev)) CURRENT_CONFIG env_config[ENV] BASE_URL CURRENT_CONFIG[base_url] ADMIN_USER CURRENT_CONFIG[admin_user] TIMEOUT CURRENT_CONFIG[timeout]使用的时候命令行直接指定环境TEST_ENVstaging pytest -s -q这样环境切换就变成了一次命令行参数不用改任何代码。我现在几乎所有的项目都采用这种方式做多环境支持实测下来非常稳团队里新人也几乎不会配错。3. 核心机制conftest.py 和 fixtures 的全局调度3.1 fixture 到底怎么设计才不算过度设计pytest 的 fixture 机制是这套框架的骨架。简单理解fixture 就是一个可以被多个测试函数调用的前置条件它的返回值会直接注入到测试函数里作为参数。比如登录你可以写一个 session 级别的 fixture只在所有用例执行前登录一次然后把登录态 cookie 保存下来复用给后面所有需要登录的用例这个开销可以忽略不计。但 fixture 使用不当也会非常混乱。我见过一些人写了上百个 fixture互相引用看得人晕头转向。我的原则是fixture 只控制测试前置条件和测试环境资源业务逻辑一律别往 fixture 里塞。注意被测业务的准备动作比如创建订单是可以在 fixture 里做的但判断订单状态是否符合预期这种逻辑还是应该放到测试函数里去做。在这套框架里我设计的几个核心 fixture 是browsersession 级负责启动浏览器实例返回可操作的 page 对象用例结束自动关闭login_statesession 级依赖 browser执行登录并把登录后的存储状态保存到全局变量避免每个用例都重新登录base_urlsession 级从配置中心读取当前环境的 base_urltest_data函数级读取指定的 yaml 数据文件并返回数据字典通过配合一个测试函数可以同时拿到页面对象、登录信息、测试数据不需要在函数内部分别初始化。3.2 登录态复用让每个用例都免登录的聪明方案UI 自动化最拖慢速度的操作就是反复登录。有些人每个用例都走一次完整的 UI 登录流程一跑几十个用例光等登录就得多花好几分钟。实现登录态复用的方案在 playwright 里有两种第一种是storage_state方式也就是登录后把 storageState包含 localStorage、cookies 等信息导出到一个 json 文件里之后启动 browser 时直接指定这个文件即可完整跳过登录步骤。second是用一个 session 级别的 fixture 做一次登录然后通过改动 cookie 信息来实现上下文共享。我采用的思路是这样# conftest.py import json import pytest from playwright.sync_api import sync_playwright pytest.fixture(scopesession) def browser_context(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() page.goto(BASE_URL) # 执行一次完整登录 page.fill(#username, ADMIN_USER[username]) page.fill(#password, ADMIN_USER[password]) page.click(button[typesubmit]) page.wait_for_url(**/home) # 存储登录状态 storage context.storage_state(pathlogs/storage_state.json) context.close() browser.close() yield storage然后把存储状态应用到每个新 context 上pytest.fixture(scopefunction) def page(browser_context): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context(storage_statelogs/storage_state.json) context.set_default_timeout(TIMEOUT) pg context.new_page() yield pg context.close() browser.close()这样修改之后每个用例在启动时直接携带登录态不再重复走登录表单整体耗时能缩短 40% 到 60%。唯一的注意点是被测系统的登录态过期时间是有限的。如果业务会话是 10 分钟有效那超过 10 分钟的用例集执行到后面就会掉登录全篇飙红灯。遇到这种情况我一般把 storage_state 的更新逻辑放到 fixtures 里做增量刷新——比如每小时重新登录一次而不是只登录一次。3.3 trace 和截图用例失败时留好第一手证据自动化用例失败的排查最忌讳的就是只给一个expected X to contain Y的错误消息。真到了半夜跑挂了开发找过来问这个按钮为什么没点到页面长什么样你手头连张截图都没有会非常被动。Playwright 原生支持 failure 痕迹收集截图、视频、trace 文件含页面 DOM 快照、执行日志、网络请求记录。我在 conftest.py 里的 fixture 加了一段失败处理逻辑# conftest.py pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: page item.funcargs.get(page) if page: timestamp time.strftime(%Y%m%d_%H%M%S) screenshot_path flogs/screenshots/{item.name}_{timestamp}.png trace_path flogs/traces/{item.name}_{timestamp}.zip page.screenshot(pathscreenshot_path, full_pageTrue) context page.context context.tracing.stop(pathtrace_path)这段代码写进 conftest.py 里以后每次用例失败会自动生成截图文件和 trace 文件。Playwright trace 文件可以用playwright show-trace logs/traces/xxx.zip直接打开会以时间轴形式展示整个会话里每一步操作的 DOM 状态和网络请求真的是调试UI用例的利器。关于 trace我补充一句它默认是关闭的用的时候需要显式调用 context.tracing.start()。因为 trace 文件体积不小每次都记录会影响执行速度。我的建议是失败时记录 trace按上面代码这样正常执行时不记录只在需要调试某个特定用例时手动加一个环境变量来控制开关。4. 数据驱动与断言设计测试数据脱离代码4.1 yaml 数据源一条用例覆盖多组输入场景UI 自动化的用例设计我强调数据驱动因为它的本质是同一套操作步骤你用不同的输入数据去跑验证不同的预期结果。例如登录用例要覆盖正确账号、错误密码、空字段、账号锁定等场景操作步骤其实一样只是输入数据不同。用 pytest 的pytest.mark.parametrize配合一个读取 yaml 的工具函数就能实现。先在 data/login_data.yaml 里写好测试数据# data/login_data.yaml login_success: - username: validuser password: validpass expected: 登录成功 login_fail_wrong_password: - username: validuser password: wrongpass expected: 用户名或密码错误 login_fail_empty: - username: password: expected: 请输入用户名然后在测试用例中读取# testcases/test_login.py import pytest from utils.read_data import load_yaml_data login_data load_yaml_data(data/login_data.yaml) pytest.mark.parametrize(case_name,data, login_data.items()) def test_login(login_page, case_name, data): login_page.navigate() result login_page.login(data[username], data[password]) assert result data[expected], f{case_name} failed: {result}这样新增一条测试数据只需要在 yaml 文件里加一段完全不用动代码。等数据积累多了以后甚至可以让手工测试人员直接维护这个 yaml 文件因为他们只需按格式填写不需要懂 pytest。4.2 断言的艺术不能只断言是否成功我刚入行的时候写过很多断言基本就是 assert 某个元素出现就完事。但后来发现很多页面错误其实是异步出现的比如操作后界面先显示成功提示过一秒又弹出错误弹窗。所以现在我的断言方式更丰富一些必做的基本断言页面关键元素存在page.wait_for_selector推荐附加的状态断言请求结果、接口返回状态码锦上添花的断言图片加载、网络请求、性能指标拿登录来说通用做法是断言跳转后的 URL、页面的欢迎语、顶部用户名这三个点都通过才算登录成功。其实很少需要断言更多——这三个点已经能覆盖绝大多数登录功能的验证逻辑了。不过值得提醒的是断言别写得太过头。我见过有人断言登录后页面十几个元素结果页面上一个动态广告位的出现时机稍微慢了一点整个用例就挂了实际上业务功能完全正常。断言的粒度保持在验证核心业务结果层面就好宁可少断言也别因为过度断言搞得用例像惊弓之鸟一样动不动就红。4.3 隐式等待和显式等待怎么定 timeout 才合理Playwright 的处理方式和 Selenium 不同——它默认会自动等待元素出现可交互状态也就是自带显式等待机制不需要像 Selenium 那样手动写 WebDriverWait。它针对每个操作如 fill、click、goto内置了默认 timeout 30 秒。我框架里设置的时间有两种一个全局 default timeout另一个是特定操作的局部 timeout。# conftest.py context.set_default_timeout(10000) # 全局默认 10 秒对于某些加载比较慢的业务页面单独加局部等待page.wait_for_selector(.order-list-item, timeout15000)全局时间不建议设太大10 秒就够用因为10秒都加载不出来的页面基本可以判定有性能问题局部时间最多给到 30 秒再多就是摆烂了。这里还想强调一下playwright 的wait_for_selector是等元素出现在 DOM 里等登录之后页面有一个跳转过渡动画用wait_for_url(**/home)等着就好不要用time.sleep(3)这种固定休眠。固定休眠会让用例集整体变慢而且要命的是在不同电脑上表现不一致经常一台机器过了另一台机器失败。5. 多标签页和 iframe 处理UI 自动化绕不开的两座大山5.1 多标签页切换从找不到元素到精准切页实际业务里经常有点击按钮后唤起新标签页的场景比如第三方支付、报表中心这些业务跳转不是在同一标签页内完成的。Playwright 处理多页的逻辑很直观就是通过监听事件捕获新 page 对象。正确做法是在点击之前先注册一个等待事件# 用 context.expect_page 捕获新开页面 with context.expect_page() as new_page_info: page.click(button:has-text(打开支付页面)) new_page new_page_info.value new_page.wait_for_load_state()很多新手容易犯的错误是直接page.click完后用browser.pages[-1]去取新页面这在多数场景下也能工作但有时候标签页创建顺序不确定容易出现脏数据或取到旧页面的情况。用 expect_page 这种事件驱动的方式是从机制上保证我一定能等到我要的那个 page。多个 page 对象并存时别搞混了。一个通用经验是给每个 page 命名在注释里甚至是在变量名里体现用途并且操作完之后及时切回主页面。比如支付流程完成后代码里我一般会显式main_page.bring_to_front()确保后续操作作用在主页面而不是停留在关闭的支付页上。5.2 iframe 定位两种方式解决跨框架问题iframe 在 UI 自动化里是经典坑尤其在老系统里登录框、富文本编辑器、客服对话窗口全是 iframe 包裹的。Playwright 对 iframe 的处理比 Selenium 友好不需要 driver.switch_to.frame 来回切换而是直接用 frame_locator 来定位。# 方式一使用 frame_locator login_iframe page.frame_locator(iframe[titlelogin-frame]) login_iframe.locator(input#username).fill(admin) login_iframe.locator(input#password).fill(123456) login_iframe.locator(button[typesubmit]).click()# 方式二先获取 frame 对象再操作 frame page.frame(namelogin-frame) # 或者 page.frame(urlhttps://...) frame.fill(input#username, admin) frame.fill(input#password, 123456) frame.click(button[typesubmit])注意多种方式之间的区别frame_locator 适合在加载不到 name 或者多个 iframe 动态创建的情况下使用而 frame 对象适合明确知道 frame 的 name 或 URL 的情况。如果页面嵌套了多层 iframeiframe 里有 iframe就一级一级往下取每层都用 frame_locator 再叠加。排查 iframe 问题时有个技巧直接在 trace 里看 DOM 树能准确知道元素在第几层 iframe 里。盲猜 selectors 效率太低配合 trace 看结构会快很多。6. 多浏览器支持与并行执行让用例跑得更快更稳6.1 通过配置切换浏览器类型一次写脚本处处跑UI 自动化很少只跑 Chrome有些系统在 Firefox 和 WebKit 下的行为有细微差异。Playwright 一个很省心的地方是同一套代码换浏览器基本就是换 launch 的参数。在框架里我把浏览器类型也做成配置项# settings.py BROWSER_TYPE os.getenv(BROWSER_TYPE, chromium) # chromium / firefox / webkitconftest.py 里启动时动态选择浏览器if BROWSER_TYPE chromium: browser p.chromium.launch(headlessheadless) elif BROWSER_TYPE firefox: browser p.firefox.launch(headlessheadless) elif BROWSER_TYPE webkit: browser p.webkit.launch(headlessheadless)实测下来同一套用例在三个浏览器上的兼容性是有差异的WebKit 对某些动画和特殊控件解析会有额外耗时Firefox 上个别页面的 xpath 定位会稍有诡异。我一般默认跑 Chromium在核心关键用例上定期跑一次 Firefox 做交叉验证就够了不需要每轮全量跑三个浏览器时间成本太高。6.2 pytest-xdist 并行多进程跑用例的正确姿势UI 自动化在并行这块有个特殊性不像接口测试那样可以直接把所有用例打散开。UI 自动化如果并行意味着多个浏览器窗口同时跑CPU 和内存压力会很大而且如果用例之间共享了登录态storage_state 文件并行时会有读写冲突。我的经验做法是pytest-xdist 建议按模块或按目录级别分配而不是按用例粒度打散pytest testcases -n 4 --dist loadscope让每个 worker 负责一个 .py 文件避免同一个文件的多个用例在不同进程里抢资源尤其是 storage_state 文件的读写。并行数不是无限高的。4 核 CPU 的机器设置-n 4往往比-n 8更稳定因为浏览器每个进程都会吃不少内存开多了反而互相拖慢。这里有个常见的坑就是--dist loadscope和--dist loadfile的区别。loadscope 是按 pytest 的 scope 分发给不同 worker一个 conftest.py 里的 session 级 fixture 会在不同的 worker 里各自初始化一次loadfile 是按文件分发同一个文件的所有用例强制在同一个 worker 跑。对于有全局 session fixture 的场景我推荐--dist loadfile这样文件内的执行顺序和状态依赖是确定性的。如果用例之间确实存在状态依赖比如 A 用例创建了某个订单B 用例依赖这个订单才能推单那我建议这种用例就不要并行跑要么通过手工打 mark 标记串行执行要么把依赖关系做进 setUP 阶段不要依赖用例执行顺序。在我接触过的所有自动化框架里用例独立性是最重要的工程纪律没有之一。7. 报告体系Allure 的接入与定制7.1 Allure 报告能带来什么从看日志到看图说话pytest 自带的报告比较简陋就一个文本输出真正到了给团队其他同事同步结果的时候没有一份像样的 HTML 报告是非常掉价的。Allure 是现阶段 pytest 生态下最好用的报告方案没有之一。接入非常简单pip install allure-pytest运行测试时加参数pytest testcases -n 4 --dist loadfile --alluredirreports/allure-results生成报告并起本地服务查看allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report报告里面能看到每个用例的执行时间、每个步骤的日志截图、附带的附加信息比如环境配置、请求参数、响应内容。从管理的角度一份 Allure 报告比一堆命令行日志强太多了。7.2 在用例里加 Allure 标注步骤、描述、附件一个不能少光有默认报告还不够要利用 Allure 的注解机制把关键信息挂到报告上import allure allure.epic(商城系统) allure.feature(登录模块) allure.story(用户登录成功场景) allure.title(测试-正常账号密码登录) allure.severity(allure.severity_level.BLOCKER) def test_login_valid(login_page): with allure.step(打开登录页面): login_page.navigate() with allure.step(输入账号密码并提交): login_page.login(validuser, validpass) with allure.step(校验登录结果): assert login_page.get_welcome_text() 欢迎回来使用 allure.attach 可以把接口请求参数、页面源码等关键信息挂到报告里allure.attach(login_page.page.content(), name登录后页面源码, attachment_typeallure.attachment_type.HTML)这一步做完之后报告就不再是干巴巴的三个断言通过了而是变成一份完整测试执行说明文档——每一步做了什么操作、页面变成了什么状态、关键数据是什么一目了然。团队里其他开发通过这份报告就能定位问题不需要每次都跑过来问你这个失败用例到底是什么情况。8. 日志系统排查问题第三步就靠它测试报告解决的是最终结果可视化但排查问题时还差一环——完整的日志链。Allure 报告展示的是测试步骤和结论而日志记录的是程序运行的脉络和实施细节。我的日志封装比较简单基于 Python 自带的 logging 模块加一个自定义的格式器# utils/logger.py import logging import os import time def setup_logger(nameui_auto): log_dir logs os.makedirs(log_dir, exist_okTrue) logger logging.getLogger(name) logger.setLevel(logging.INFO) file_handler logging.FileHandler( os.path.join(log_dir, ftest_{time.strftime(%Y%m%d)}.log), encodingutf-8 ) console_handler logging.StreamHandler() fmt logging.Formatter( %(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s ) file_handler.setFormatter(fmt) console_handler.setFormatter(fmt) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger在使用时我在页面对象的关键操作里加日志class BasePage: def __init__(self, page): self.page page self.logger setup_logger(self.__class__.__name__) def click(self, selector): self.logger.info(f点击元素: {selector}) self.page.click(selector) self.logger.info(f点击完成: {selector})两层日志页面对象记录操作细节用例层记录业务目标实现过程。这样日志文件里就能还原出用例执行的时间线排查问题时一眼看到底卡在哪个环节。日志和 allure 报告的区别做个类比Allure 报告是给人看的报表日志是给程序员排查问题用的流水账。两者都不可少在框架构建的优先级上日志甚至可以排到报告之前因为报告集成很简单日志缺失却会在排查问题时让人抓狂。9. 实操场景一从零到一把完整用例跑起来全流程复盘假设现在要测试用户登录后在个人中心修改昵称这个业务场景。我把完整开发流程过一遍看看框架里每个组件是怎么协作的。第一步页面对象层# pages/user_center_page.py class UserCenterPage: def __init__(self, page): self.page page self.nickname_input #nickname self.save_button button:has-text(保存) self.nickname_display .nickname-text def navigate(self): self.page.goto(BASE_URL /user/center) self.page.wait_for_selector(self.nickname_input) def update_nickname(self, new_name): self.page.fill(self.nickname_input, new_name) self.page.click(self.save_button) self.page.wait_for_selector(.toast-success) def get_nickname(self): return self.page.text_content(self.nickname_display)第二步测试用例层# testcases/test_user_center.py import allure from pages.user_center_page import UserCenterPage from utils.logger import setup_logger logger setup_logger(__name__) allure.feature(个人中心) def test_update_nickname(page): 验证用户修改昵称成功后显示更新 logger.info(测试开始修改昵称) user_center UserCenterPage(page) with allure.step(进入个人中心页面): user_center.navigate() with allure.step(修改昵称): new_nickname f测试用户_{random.randint(100,999)} user_center.update_nickname(new_nickname) with allure.step(验证昵称更新): assert user_center.get_nickname() new_nickname, 昵称修改失败 logger.info(f测试结束最终昵称为{user_center.get_nickname()})第三步跑用例TEST_ENVdev pytest testcases/test_user_center.py -v --alluredirreports/allure-results这一步执行中conftest 里的 page fixture 会启动浏览器、注入登录态、进入测试用例函数用例执行完自动关闭浏览器和上下文不留孤儿进程。这套流程就是这个框架里最典型的开发节奏先写页面对象再写用例直接用命令行跑改哪里看哪里升级维护也都是在这个轨道上运转。10. 实操场景二轮询重试与异常恢复让自动化更抗造再完美的框架也不可能百分百避免偶发失败要么是页面加载慢了半拍要么是网络抖了一下。对于这种非业务性失败与其让整个用例集中断重跑一遍不如在框架层面加一层可控的重试机制。pytest 官方有一个插件叫 pytest-rerunfailures配置方式极简单pip install pytest-rerunfailures运行参数加--reruns 2 --reruns-delay 1表示失败后自动重跑 2 次每次间隔 1 秒。但这里我要泼一盆冷水重试不能滥用。如果一条用例本身断言写错了重跑多少次都会失败反而会掩盖真实问题让人误以为用例偶尔通过就行。我的策略是对偶发的页面渲染类故障允许重跑 1 次对数据相关的用例不建议重跑因为数据状态可能已经被污染重跑只会继续错所有最终失败的用例必须在报告里标明首次失败即重跑的痕迹怎么标记重跑痕迹呢pytest-rerunfailures 会把重跑信息写进 Allure 报告里如果多次重跑最终仍然失败报告上会显示 retries 数量。团队看到 retries0 的失败用例就会知道这不是一次稳定失败需要再看具体的失败原因。另外还有一个在 UI 自动化里非常实用的异常恢复技巧当检测到页面出现某个预期之外的弹窗比如 cookie 授权弹窗、版本升级公告时自动关闭弹窗再继续操作。这种处理方式判断起来有风险如果你预期弹窗一定会出现也可以写进统一处理逻辑里但需要严谨评估是否会影响正常业务。我通常只在测试环境使用到了预发环境就关掉这个特性毕竟每个环境的弹窗策略可能有差异。11. 常见问题速查表这些坑我替你先踩了UI 自动化框架搭建过程中有一些高频问题我总是反复被问到。我把它们整理成一个速查表现象可能原因解决方案用例执行时全部提示元素找不到登录态过期storage_state 文件失效重新生成 storage_state或检查会话过期时间并发执行时偶发失败storage_state.json 被多进程同时读写改用--dist loadfile保证文件归属同一 worker页面元素能定位到但点击无反应元素被遮挡或者处于不可交互状态使用page.click(selector, timeout...)并配合expect_popo或wait_for_selector(statevisible)iframe 元素定位不到iframe 的 name 或 id 是动态的改用 frame_locator 通过 URL 属性或 css 属性定位Allure 报告中看不到截图hook 里截图代码未执行或路径未匹配确认 hook 写在全局 conftest.py 中用绝对路径输出浏览器关闭后页面对象仍被引用用例清理顺序不正确使用 fixture 的 yield 机制保证反向清理执行速度极慢大量固定 sleep 等待全部替换为 playwright 自带的自动等待机制用例之间互相影响用例共享了全局变量或存储状态排查测试数据冲突确保用例独立性有自己的排查技巧在里面比什么都有用。犹记得有一次全量用例在周一跑全红排查半天发现是周末有人往测试环境导了一份脏数据导致所有账号的登录态都失效了。从那以后我就把登录状态有效性检测写成了一条全局前置用例跑到第二步先快速探活登录态探活失败直接终止全量执行提前报警而不是让几百条用例集体挂红玩大合唱。12. 框架的后续演进从基础搭建到 AI 辅助的过渡区这套框架成型之后可以直接复用到新项目的 UI 自动化里。它的优点在于依赖极轻、结构清晰、提供的数据驱动和多环境切换机制几乎适配所有 Web 系统。往后如果要继续增强有几个方向是值得投入的第一把 API 调用融合进来作为 UI 自动化的数据准备层。比如创建测试订单与其走 UI 点半天表单不如在 setUP 阶段直接调接口创建UI 只负责验证展示结果。这样的用例更稳定执行时间也更短。第二把 CI 集成做好在 GitLab 或 Jenkins 里跑定时任务早上自动执行一遍冒烟用例把 Allure 报告发布到公司内部的报告服务失败自动通知相关人。接 CI 这一步通常能让自动化测试的落地率翻一倍——因为不再是只有你手动跑而是整个团队每天都能看到质量反馈。第三当前热门的 AI 辅助方向可以考虑用 LLM 结合测试用例描述自动生成步骤代码帮我节省不少重复性的用例编写时间。但注意的是目前这类工具的稳定性和准确性还在迭代人为主导校验仍然是必须的。框架的意义就是把这种AI 生成代码快速嵌入到一个干净的骨架里不至于东拼西凑。最后再提醒一句别追求万能框架。每个团队的测试对象、业务形态、人员能力都不一样框架的核心价值是解决你们自己的痛点而不是抄一套大而全的东西在自己项目里水土不服。动手去写、去跑、去踩坑才是搭建这套东西最有效率的方式。