1. 整体方案设计与技术选型
做 Web UI 自动化测试这些年,我先后用过 Selenium、Cypress、Robot Framework,直到最近半年把主力栈切到了 Playwright + Pytest,才真正觉得“写起来顺手、跑起来稳定、维护起来不心累”。如果你正在纠结 UI 自动化框架怎么选,或者已经在用 Selenium 但被各种隐式等待、显式等待、浏览器驱动版本不匹配折磨得够呛,那这篇文章应该能给你一个值得认真考虑的替代方案。
1.1 为什么是 Playwright + Pytest,而不是 Selenium 或 Cypress
先说结论:Playwright 负责“操作浏览器”,Pytest 负责“组织用例”,两者是当前 Python 生态里配合度非常高的一组搭档。Playwright 由微软团队开发,底层协议和 Chrome DevTools Protocol 走得非常近,所以它对浏览器行为的控制粒度比 Selenium 更细,而且内置了自动等待机制,绝大多数情况下你不需要像写 Selenium 那样疯狂塞time.sleep()。
Cypress 虽然用起来也很舒服,但它主要绑定 JavaScript/TypeScript 技术栈,在 Python 项目里想要落地会非常别扭。而 Pytest 是 Python 测试框架里事实上的标准,fixture 机制、参数化、插件生态都极其成熟。把 Playwright 的浏览器操作能力注入到 Pytest 的 fixture 体系里,等于同时拿到了“好开的车”和“顺手的导航”,这是这套方案最核心的价值。
另外,Playwright 天然支持Chromium、Firefox、WebKit三种浏览器引擎,也就是说你用一套代码可以跑 Chrome、Edge、Firefox,甚至是 Safari 的内核(WebKit)。这套能力在 Selenium 里虽然也有,但配置成本和稳定性差异很大,Playwright 开箱即用的体验明显更好。
1.2 这套方案适合什么项目
不是所有 Web 项目都适合做 UI 自动化,这点必须先说清楚。根据我的实践经验,以下三类项目用 Playwright + Pytest 收益最大:
- 核心业务链路比较固定的系统,比如电商的下单流程、后台管理系统的表单审核流程,这类场景脚本写一次可以反复跑很久。
- 需要多浏览器兼容验证的站点,前端框架升级或者浏览器版本迭代时,用一套脚本在三种内核里各跑一遍,比手工点半天靠谱得多。
- 接口稳定、UI 变动不频繁的中后台项目,这类页面通常结构规整,元素定位比较好维护,自动化测试的投资回报率很高。
如果你的项目正处于 UI 疯狂改版的阶段,或者验证码、滑块等反自动化机制非常强,那我建议先把精力放在接口自动化上,等前端趋于稳定再上 UI 层。否则你每天的工作就是在修脚本,而不是在验证业务功能。
1.3 技术选型横向对比参考
我把主流框架的几个关键维度整理成一张表,方便你在团队内部做技术选型时直接引用:
| 对比维度 | Playwright + Pytest | Selenium + Pytest | Cypress |
|---|---|---|---|
| 编程语言 | Python 为主,也可用 JS/Java/.NET | 多语言 | JavaScript/TypeScript |
| 自动等待 | 内置 Actionability 检查,几乎不用手写 | 需要显式/隐式等待配合 | 内置 |
| 多浏览器 | Chromium、Firefox、WebKit | Chrome、Firefox、Edge 等 | 仅 Firefox 和 Chromium 系 |
| 浏览器驱动 | 自动管理,无版本冲突 | 需手动下载并匹配版本 | 由框架内置 |
| 调试手段 | Trace Viewer、代码生成、录制 | 较弱 | 时间旅行调试 |
| 网络拦截/Mock | 原生支持,API 直接拦截 | 需借助 BrowserMob 等第三方 | 内置 |
| 报告生态 | 依赖 Pytest 插件(Allure/pytest-html) | 依赖 Pytest 插件 | 自带 Dashboard |
| 上手成本 | 中等,需要同时了解 Playwright 和 Pytest | 较低,文档丰富 | 较低,但需 JS 基础 |
这张表给我们的结论很清楚:如果你在 Python 技术栈内选型,Playwright + Pytest 在稳定性、功能完整度、后续维护性上都是目前比较优的答案。
2. 环境搭建与工程结构规划
这一节把从零到一的环境准备过程完整走一遍,包括 Python 虚拟环境、依赖安装、浏览器下载,以及工程目录怎么规划。很多新手在这里踩的坑,我会单独标出来。
2.1 安装 Playwright 并下载浏览器内核
首先需要准备 Python 3.9 以上的环境。我自己习惯用venv或conda创建独立的虚拟环境,避免污染全局环境。安装命令非常简单:
pip install pytest pip install playwright装完库之后,还需要下载 Playwright 对应的浏览器内核,这一步容易忘,忘了的话运行时会直接报错:
playwright install这个命令默认会下载 Chromium、Firefox 和 WebKit 三套浏览器,体积比较大。如果只需要跑 Chrome 或 Edge,可以只装 Chromium,终端提示使用playwright install chromium来单独安装。我在团队内部推荐优先装 chromium,因为大多数业务项目本身就以 Chrome 为主力浏览器,而且 Chromium 内核跑起来也更快。
提示:如果公司内网限制导致下载失败,可以配置镜像源。Windows 下默认浏览器内核会装到
C:\Users\<用户名>\AppData\Local\ms-playwright,Linux 下一般在/home/<用户名>/Library/Caches/ms-playwright。如果你用 CI 容器,还需要考虑安装系统级依赖,镜像里可以预装playwright install-deps命令拉取的系统库。
2.2 工程目录结构设计
工程结构直接决定了项目后期的可维护性。我现在的团队用的一套结构,供你参考:
web_auto_test/ ├── conftest.py # Pytest 全局 fixture,浏览器实例和页面对象的工厂方法 ├── pytest.ini # Pytest 配置,包括运行参数、marker 注册、超时时间 ├── requirements.txt # 依赖清单 ├── pages/ # Page Object 层,每个页面一个类 │ ├── base_page.py # 封装公共操作,如点击、输入、等待 │ ├── login_page.py │ └── dashboard_page.py ├── tests/ # 测试用例层,按业务模块组织 │ ├── test_login.py │ └── test_order_flow.py ├── utils/ # 工具类,如日志封装、随机数据生成 │ ├── logger.py │ └── data_generator.py └── reports/ # 测试报告与截图输出目录 ├── screenshots/ └── traces/很多人一上来就把所有逻辑塞进测试文件里,短期看是省事,但用例一多全是重复代码,改一个登录框定位要翻遍所有测试文件。Page Object Model(PO 模式)是 UI 自动化里最经典的设计思路,它把页面的“长什么样”(元素定位)和“能干什么”(业务操作)封装到页面类里,测试用例只写“用户视角的操作和预期结果”,维护成本会大幅下降。
2.3 conftest.py 中的核心 fixture 设计
conftest.py是 Pytest 里最核心的扩展文件,我把它当作整个测试工程的“发动机”。下面的 fixture 设计是我在多个项目中沉淀下来的版本:
import pytest from playwright.sync_api import sync_playwright @pytest.fixture(scope="session") def browser(): with sync_playwright() as p: # 这里可以把 headless 开关放到命令行参数或环境变量控制 browser = p.chromium.launch(headless=False, slow_mo=100) yield browser browser.close() @pytest.fixture() def page(browser): context = browser.new_context( viewport={"width": 1440, "height": 900}, locale="zh-CN", ignore_https_errors=True ) page = context.new_page() yield page page.close() context.close()这里有几个容易忽视的细节,逐个说明:
scope="session"表示整个测试会话只启动一次浏览器实例,大大提高执行效率。但也要注意,session 级浏览器实例如果出了问题,可能影响后面所有用例,所以在业务量大、用例相互依赖性强的场景下,可以考虑scope="module"甚至默认的 function 级别,代价是执行时间变长,但隔离性更好。
context 的概念是 Playwright 相比 Selenium 的一个很大优势。一个浏览器进程可以创建多个独立的 context,每个 context 之间的 Cookie、缓存、登录状态是互相隔离的。这意味着我们可以在同一个浏览器进程里并行跑多个用例,互不影响,这在回归测试里有非常大的实践价值。
locale 和 viewport的设置可以模拟真实用户环境。特别是中文场景下,把locale="zh-CN"设置好,能避免某些日期控件、分页组件因为语言环境导致渲染差异,这类问题在自动化测试中很隐蔽,建议一开始就固定好。
slow_mo=100是我调试阶段的习惯。它在每个操作之间插入 100ms 的延迟,让浏览器操作速度肉眼可见,配合headless=False可以直观地观察脚本执行过程。正式跑 CI 时再改成headless=True,两者结合非常方便。
写完这几个基础 fixture,整个测试工程的“地基”就算打好了。接下来可以开始写第一个用例。
3. 核心实操:从元素定位到完整用例
从这一节开始,我们进入真正的实战环节。先看 Playwright 的元素定位与自动等待机制,因为这两个是它比 Selenium 体验好很多的直接原因,然后再一步步实现一个真实的登录测试用例。
3.1 元素定位与自动等待机制解析
Playwright 的定位器(Locator)API 设计得非常符合直觉,可以用“用户在页面上怎么看这个元素”的角度去定位,而不只是依赖 CSS 选择器。常用的定位方式包括get_by_role、get_by_text、get_by_placeholder、get_by_test_id等。
举几个实际使用的例子:
# 通过文本定位按钮,适合“唯一文本”的场景 page.get_by_role("button", name="登录").click() # 通过占位符定位输入框 page.get_by_placeholder("请输入用户名").fill("testuser") # 通过表单标签文本定位输入框 page.get_by_label("密码").fill("password123") # 通过自定义 test-id 定位,需要前端配合加>class LoginPage: def __init__(self, page): self.page = page self.username_input = page.get_by_placeholder("请输入用户名") self.password_input = page.get_by_placeholder("请输入密码") self.login_button = page.get_by_role("button", name="登 录") def goto(self): self.page.goto("https://your-server.com/login") def login(self, username, password): self.username_input.fill(username) self.password_input.fill(password) self.login_button.click() self.page.wait_for_load_state("networkidle")然后写测试用例,放在tests/test_login.py:
def test_login_success(page): login_page = LoginPage(page) login_page.goto() login_page.login("admin", "correct_password") # 校验登录成功 page.get_by_text("欢迎回来,Admin").wait_for() assert page.get_by_text("欢迎回来,Admin").is_visible() def test_login_failed_with_wrong_password(page): login_page = LoginPage(page) login_page.goto() login_page.login("admin", "wrong_password") # 校验错误提示 error_tip = page.get_by_text("用户名或密码错误") assert error_tip.is_visible()这段代码里没有一处显式的 sleep,但两个用例都能稳定通过,靠的就是 Actionability 检查和wait_for()的兜底。这里wait_for()的作用是“等待某个条件满足后再继续”,它是显式等待的抓手,使用场景是在断言之前确认关键元素已经就绪。
任何一个测试框架都无法完全避免不稳定的情况,Playwright 的解决思路是用多层机制把概率降到最低。除了自动等待,它还提供了expect的轮询断言机制:
from playwright.sync_api import expect expect(page.get_by_text("欢迎回来,Admin")).to_be_visible(timeout=5000)expect会在默认的 5 秒超时内反复检查条件是否满足,直到满足或超时。相比直接assert,它的自动重试机制能极大提升用例稳定性,建议在断言阶段优先使用。
3.3 用 @pytest.mark.parametrize 实现多场景数据驱动
真实项目的登录逻辑不可能只有正确和错误两种场景,往往需要验证用户名为空、密码为空、账号锁定、验证码错误等一堆分支。如果每个分支都写一个测试函数,代码会非常臃肿。Pytest 的参数化装饰器可以帮我们用一套用例逻辑覆盖多组数据:
import pytest @pytest.mark.parametrize("username,password,expected_message", [ ("", "correct_password", "请输入用户名"), ("admin", "", "请输入密码"), ("banned_user", "any_password", "账号已被锁定"), ("admin", "wrong_password", "用户名或密码错误"), ]) def test_login_validation(page, username, password, expected_message): login_page = LoginPage(page) login_page.goto() login_page.login(username, password) error_tip = page.get_by_text(expected_message) expect(error_tip).to_be_visible()参数化之后,新增一条测试数据只需要往列表里加一行,测试报告里会自动拆分出独立的用例记录,方便追踪是哪个组合挂了。
另一个组合是Playwright 多浏览器参数化,一行代码让测试在 Chromium、Firefox、WebKit 三套内核里各跑一遍:
import pytest from playwright.sync_api import sync_playwright browsers = ["chromium", "firefox", "webkit"] @pytest.mark.parametrize("browser_name", browsers) def test_login_multi_browser(browser_name): with sync_playwright() as p: browser = getattr(p, browser_name).launch() page = browser.new_page() # 执行登录测试 browser.close()这种做法非常适合对外发布的产品,内网管理系统如果用户主要集中在 Chrome,则可以只跑 chromium,节省时间。测试稳定性和执行成本之间的平衡,需要根据实际场景决定。
3.4 网络请求拦截与 Mock 接口响应
UI 自动化里经常会碰到一个痛点:测试环境依赖的后端接口不稳定,或者某些第三方服务无法直接访问。Playwright 原生支持网络请求的拦截与响应伪造,这个能力是我非常喜欢的一点。
比如登录接口依赖一个短信服务,测试环境里你不想真的发短信,可以用下面的方式直接 mock 掉:
def test_login_with_mocked_sms(page): # 拦截短信接口,伪造返回成功 page.route("**/api/sms/send", lambda route: route.fulfill( status=200, content_type="application/json", body='{"code": 0, "message": "success"}' )) # 后面的测试逻辑正常走,但不会真正发出短信请求 page.goto("https://your-server.com/login") page.get_by_placeholder("手机号").fill("13800138000") page.get_by_role("button", name="获取验证码").click() # 验证码输入框出现后,填入固定值 page.get_by_placeholder("验证码").fill("123456")拦截**/api/sms/send这个模式串,route.fulfill直接伪造一个成功的响应返回给前端。这让测试不再依赖外部服务,稳定性大幅提升。
更常见的用法是监听接口响应,比如判断一次操作是否真的触发了保存请求、响应码是不是 200:
def test_order_submit(page): with page.expect_response("**/api/order/create") as response_info: page.get_by_role("button", name="提交订单").click() response = response_info.value assert response.status == 200 assert response.json()["code"] == 0expect_response是一个上下文管理器,它会等待与给定模式匹配的响应出现后返回,非常适合用来验证“点击后是否真的调用了某个接口”。调试阶段如果发现接口没被调用,往往是按钮点击没有生效或者前端校验拦截了提交,这时候可以结合网络面板和 trace 一起排查。
4. 疑难杂症排查与稳定性提升
自动化测试最怕的不是用例失败,而是毫无规律的随机失败。这类问题排查起来非常耗时,尤其是刚接触 Playwright 时,很多细节不了解会以为是框架 bug,实际上往往是一些小配置或环境因素导致。这一节我把常见的问题和解决方法整理成速查表,再补充几个让用例更稳定的建议。
4.1 元素定位不到的排查思路
在 Playwright 中遇到TimeoutError: locator.click: Timeout 30000ms exceeded是最常见的情况。先不要急着换选择器,可以按下面顺序排查:
- 确认页面是否真的停留在预期地址。加一行
print(page.url),看是不是发生了跳转或者重定向。 - 打开 headless 模式调试。把
headless=False跑一遍脚本,亲眼看看操作过程,有时候是页面弹窗、遮罩层盖住了目标元素。 - 检查是否有 iframe。如果目标元素在 iframe 内部,Playwright 的顶层 locator 是定位不到的,需要用
frame_locator进入再定位:
page.frame_locator("#main-iframe").get_by_role("button", name="确认").click()检查 shadow DOM。如果前端组件使用了 Shadow DOM,普通的选择器无法穿透进去,需要用
page.locator("css=selector")配合深度组合器,例如/deep/,或者让前端开发在组件上暴露>editor = page.frame_locator(".rich-editor-frame") editor.get_by_role("textbox").fill("这里是正文内容")还有一种情况是iframe 嵌套 iframe,这在广告位、地图组件场景比较常见,需要逐层进入:
outer = page.frame_locator("#outer-frame") inner = outer.frame_locator("#inner-frame") inner.get_by_text("确定").click()动态弹窗的处理方式也值得单独说。比如操作完成后会弹一个“保存成功”的提示,且几秒后自动消失。最稳妥的做法是不要盲目等待它消失,而是等待它出现后再进行断言:
toast = page.get_by_text("保存成功") expect(toast).to_be_visible() # 继续其他操作时,Playwright 会在下一次操作前自动等待 toast 消失这里注意一个小细节:如果 toast 遮盖了后面要点击的按钮,Actionability 检查会判定按钮不可点击,这是 Playwright 自动等待“严格模式”的一种体现。遇到这种情况,可以在断言 toast 可见后,使用
expect(toast).to_be_hidden()等待它消失再继续,或者直接点击页面其他空白区域让 toast 关闭。4.3 提升测试稳定性的 5 个习惯
- 尽量使用
get_by_role和get_by_label,这类语义化定位方式比“通过 CSS class 匹配”更接近用户视角,前端只要不改文案、不换标签语义,即使类名变化也不会挂。而 CSS class 在前端重构时经常说改就改,是最脆弱的定位锚点之一。 - 不要过度依赖快照断言。有些新手喜欢对整个页面做截图对比来验证 UI 正确性,这种做法在字体渲染差异、动效未结束时会产生一堆误报。优先断言关键元素的文本、可见性、属性状态,截图对比可以放在视觉回归专项里做。
- 严格控制测试数据。登录用例里的账号密码不要从上一步接口返回值里随机取,最好在测试前置条件里直接通过数据库或接口造好固定数据。每个用例跑完尽量清理数据,避免互相干扰。
- 区分功能用例和冒烟用例的目录。把核心链路标记为
@pytest.mark.smoke,让冒烟用例能在几分钟内跑完,完整回归在 CI 夜间执行。不要试图把几千条用例全部放在每一次提交的流水线里,否则排障成本极高。 - 截图不只在失败时保存。我习惯在每次用例结束时都截一张最终状态图,命名带上用例名和时间戳。这样出问题时不用重新跑一遍,看截图就能大概判断问题出在哪个环节。
4.4 常见问题速查表
现象 可能原因 解决方式 定位超时,但手动操作正常 元素在 iframe 或 shadow DOM 中 用 frame_locator 或深度定位器 偶发点击无响应 遮罩层 / toast 挡住元素 改用 expect 等待遮挡元素消失,或滚动到元素可见 页面加载完毕但接口还在返回 前端 AJAX 异步渲染 使用 wait_for_load_state("networkidle"),或对关键接口expect_response跨用例登录状态串了 浏览器 context 未隔离 每个用例创建新的 context,或在 fixture 里主动清理 cookie headless 模式下字体/布局不同 无头浏览器缺少系统字体 安装系统字体或用 launch(args=["--force-device-scale-factor=1"])多浏览器跑挂 1 个,另 2 个通过 WebKit 特有渲染差异 检查是否依赖仅 Chrome 支持的私有 API 或属性 这张表是我在实际维护过程中总结出来的高频问题,绝大多数团队遇到的坑都集中在这些点上。值得一提的是,遇到问题时“开一个能稳定复现的最小用例”是最高效的排查手段,先把复杂场景简化到十几个操作以内,再逐行判断。
5. 页面对象模式与工程级实践
要支撑一个中等规模的 Web 工程自动化回归,仅有“能跑的用例”远远不够,还需要一套工程化规范来应对版本迭代和团队协作。页面对象模式(Page Object Model)是其中最重要的一环。
5.1 如何设计 BasePage 与业务页面类
Page Object 模式的核心思想是把“页面”当成一个可复用的对象。这个模式并不是什么高深理论,它解决的实际问题是:当测试用例散落在各个测试文件中时,一旦页面 UI 调整,你不想在 20 个文件里逐个改选择器。
BasePage可以封装所有页面通用的操作,比如打开页面、等待元素、获取标题等:class BasePage: def __init__(self, page): self.page = page def goto(self, url): self.page.goto(url) self.page.wait_for_load_state("networkidle") def click_and_wait(self, locator, response_pattern=None): if response_pattern: with self.page.expect_response(response_pattern) as response_info: locator.click() return response_info.value locator.click() def fill_and_assert(self, locator, text): locator.fill(text) expect(locator).to_have_value(text)业务页面类继承自
BasePage,比如登录页、首页、订单页:class LoginPage(BasePage): def __init__(self, page): super().__init__(page) self.username_input = page.get_by_placeholder("请输入用户名") self.password_input = page.get_by_placeholder("请输入密码") self.login_button = page.get_by_role("button", name="登 录") def login(self, username, password): self.goto("/login") self.username_input.fill(username) self.password_input.fill(password) self.login_button.click() # 登录成功后默认跳转到首页 self.page.wait_for_url("**/dashboard")这样设计以后,测试用例的写法会非常收敛,几乎完全在描述“业务行为”:
def test_login_and_check_avatar(page): login_page = LoginPage(page) login_page.login("admin", "correct_password") dashboard_page = DashboardPage(page) expect(dashboard_page.avatar_area).to_be_visible()从维护角度看,如果登录页的输入框名称变了,只需要修改
LoginPage类里的定位器,测试用例本身不需要动。这一点在人手紧张、接口频繁调整的团队里特别有价值。5.2 目录级 fixture 与分层 conftest.py
conftest.py在 Pytest 中是可以多级存在的,根目录的 conftest 放全局 fixture,每个子目录还可以有自己的 conftest 放局部 fixture。这个特性非常适合按模块组织测试工程的场景。比如
tests/test_order/目录下需要创建一个“登录后的页面实例”作为前置条件,可以在该目录的conftest.py里新增:@pytest.fixture() def order_page(browser, page): # 先登录 login_page = LoginPage(page) login_page.goto() login_page.login("order_tester", "password") # 进入订单页 page.goto("https://your-server.com/order/list") page.wait_for_load_state("networkidle") return OrderPage(page)这样只有
tests/test_order/目录下的用例会执行这段前置逻辑,其他模块不受影响。很多团队把所有 fixture 堆在根目录的 conftest.py 里,导致文件越来越臃肿,其实按目录拆分会让代码更清晰,排查问题时也能快速定位是哪一层 fixture 出的问题。另外,fixture 的命名尽量业务化,例如logged_in_page、created_order,而不是笼统的setup、helper。5.3 敏感数据管理与多环境支持
UI 测试免不了要在不同环境执行,比如开发环境、测试环境、预发环境。把这些环境的地址和账号信息写死在代码里是大忌。推荐的做法:
- 使用
.env文件或环境变量配置BASE_URL、USERNAME、PASSWORD。 - Pytest 层面可以使用
pytest.ini里的命令行参数或自定义的--env选项切换环境。
例如,通过 Pytest 自定义选项实现环境切换:
def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="指定运行环境: test / staging / prod") @pytest.fixture(scope="session") def base_url(request): env = request.config.getoption("--env") urls = { "test": "https://test-server.com", "staging": "https://staging-server.com", } return urls[env]运行的时候:
pytest tests/test_login.py --env=staging这种做法还有一个额外好处:CI 流水线里可以针对不同分支、不同环境跑同一套用例,而代码仓库里不会出现任何明文密码。至于密码本身,可以存到 CI 平台的 secret 变量中,通过环境变量注入到测试进程,代码中只读取
os.getenv("TEST_PASSWORD"),避免敏感信息因代码泄露。5.4 测试报告与 Allure 集成
Pytest 生态里最成熟的报告方案是 Allure。它不仅支持失败截图、用例步骤、参数化展示,还能生成漂亮的 Web 报告,适合团队回顾和分析测试结果。
接入方式分为两步。第一步安装依赖:
pip install allure-pytest第二步在
pytest.ini里配置结果目录:[pytest] addopts = --alluredir=reports/allure-results testpaths = tests运行用例后,生成报告:
allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report为了让报告信息更丰富,我常用
allure的注解标记用例模块和功能点:import allure @allure.feature("登录模块") @allure.story("正常登录") def test_login_success(page): # 步骤名称也会展示在报告中 with allure.step("打开登录页并输入账号信息"): ... with allure.step("提交登录并校验结果"): ...加上
@allure.step之后,报告里会清晰展示每一步的执行情况,团队在复盘失败用例时能一眼看出是“输入账号”这步挂了还是“提交登录”这步挂了,排查效率会明显提升。6. 与 CI 集成和团队协作实践
自动化测试写出来是要高频运行的,如果不接入持续集成,脚本的维护热度很快就会过去。生产中我推荐的接入方式是 GitLab CI 或 Jenkins,核心目标是在每次代码提交、每个晚间定时任务里自动执行一套冒烟回归,失败时推送通知到群。
6.1 无头模式与 CI 运行配置
CI 环境没有显示器,必须把浏览器切到 headless 模式。改造之前的 fixture,用一个环境变量控制是否使用无头模式:
import os @pytest.fixture(scope="session") def browser(): headless = os.getenv("HEADLESS", "true") == "true" with sync_playwright() as p: browser = p.chromium.launch(headless=headless) yield browser browser.close()本地调试时运行:
HEADLESS=false pytest tests/test_login.py --headed -sCI 里默认就是
true,不用额外传参。还有一点很重要:CI 里运行的测试用例不要加slow_mo,否则执行时间会被拉长很多。本地看过程是一个需求,CI 追求的是快速反馈。6.2 定时任务与失败通知
我们团队的实践是:白天每次代码合并后跑“冒烟级用例”,大概 10~20 条,10 分钟内出结果;每天晚上跑“完整回归”,覆盖所有自动化用例,大约 200~300 条,30 分钟左右执行完。如果夜间回归有失败,第二天早上大家第一件事就是看测试报告,然后按模块分给负责人排查。
失败通知建议直接推到钉钉、企微或者飞书群。实现方式很简单,在 Pytest 的钩子函数里加一个
pytest_sessionfinish,如果failed > 0就调用 Webhook 发送摘要信息。也可以用现成的pytest-reportportal或pytest-azurepipelines插件对接已有的测试管理平台,团队有现成平台的话优先用现成的。6.3 从脚本到工具的演进
当你发现团队里越来越多的人开始依赖这套自动化用例时,就可以考虑把它包装成内部工具。比如做一个简单的命令行入口:
python run_tests.py --module=login --browser=chromium --env=staging --report=allurerun_tests.py内部其实就是解析参数后再调用pytest.main(),但对外暴露的接口更友好,非资深测试开发也能使用。这条路径就是从一个“个人脚本”演化为“团队基础设施”的历程,也是 UI 自动化测试工作最有成就感的部分。在落地过程中还有一个容易被低估的点:文档和代码注释。自动化用例的维护者不一定是写脚本的人,如果每一步操作缺乏注释,三个月后你自己可能都忘了某段逻辑当时为什么这么写。我要求团队里所有页面类的方法都必须有 docstring,测试用例必须有
# 业务预期注释,这个投入在长期维护中回报很大。最后说说我对这套技术栈的体会:Playwright + Pytest 不是银弹,但它确实把 Web UI 自动化的“下限”抬高了,你不需要成为前端专家也能写出可维护的自动化脚本。如果看到这里的你正准备选型,我的建议是先把登录、核心链路、分页查询这几个高频场景各写一个用例,跑两周看看稳定性和维护成本,再决定是否全量铺开。工具本身不复杂,真正的门槛在于你的团队有没有一套清晰的页面对象划分和持续集成的推动力。
- 尽量使用