Playwright Web自动化测试完整攻略:从入门到实战
做自动化测试这些年我见过太多测试同学在选型上反复纠结。如果你正在评估Web自动化框架近两年有一个名字绕不开Playwright。它不是又一个Selenium的简单替代品而是从架构层面重新思考了浏览器自动化的整套实现。我自己从Selenium转到Playwright之后最大的感受是很多以前要靠“睡眠等待”“显式等待”和各种hack技巧才能解决的问题在Playwright里几乎是默认行为。这份攻略不是官方文档的翻译而是我这段时间实际项目里踩坑、调优、排错之后整理出来的完整路线。从怎么安装、第一个脚本怎么写到定位器、自动等待、录制回放、网络拦截、移动端模拟再到一个接近真实业务场景的实战Demo以及我踩过的那些坑一次性讲清楚。适合刚接触自动化测试的新手也适合想从Selenium切换过来的老手。1. 为什么是Playwright先搞清楚它解决了什么1.1 Selenium时代的老大难问题在Playwright出现之前Selenium几乎是Web自动化的事实标准但用过的人都知道它有四个让人头疼的痛点。第一等待机制太原始。早期Selenium代码里到处都是time.sleep后来有了显式等待WebDriverWait但你需要为每个交互手动写Expected Condition写多了之后代码非常啰嗦而且等待条件覆盖不全稍不留神就出现“元素可点击但点击没生效”“页面刷新后元素重新附着失败”这类玄学报错。第二多页面和iframe处理繁琐。Selenium里每打开一个新窗口或者iframe都要来回切换driver.switch_to的上下文切换错一个层级元素就定位不到最后只能靠try-catch碰运气调试成本极高。第三跨浏览器的一致性差。同样的脚本在Chrome上跑得好好的换到Firefox就到处失败。因为Selenium只是把命令发给浏览器驱动很多底层行为差异要靠测试代码自己兼容。第四安装和运行环境麻烦。WebDriver需要单独下载、版本必须和浏览器精确匹配一旦浏览器自动升级测试环境就崩了。1.2 Playwright的核心设计差异Playwright对上面这些问题的处理思路是完全不同的。它采用的是浏览器内核级别的CDPChrome DevTools Protocol协议但同时也封装了完整的跨浏览器抽象层不管底层是Chromium、Firefox还是WebKit暴露给你的API几乎一致。这意味着你写一套脚本在三种内核上跑出来的行为基本统一跨浏览器兼容性问题大幅减少。等待机制上Playwright内置了“Actionability”检查。它会在执行点击、输入、悬停这些操作之前自动等待元素完成一系列可操作性检查元素是否附加到DOM、是否可见、是否稳定、是否接收事件、是否被其他元素遮挡。这一套组合检查做完之后绝大多数“过早操作”导致的失败都被消灭了。我见过很多从Selenium转过来的同事第一反应是“我的显式等待全部删掉代码好像短了一半”。上下文隔离也是Playwright的大杀器。每个BrowserContext相当于一个独立的浏览器会话和别的上下文之间完全隔离cookie、localStorage、缓存都是独立的。你在同一个浏览器实例里开多个上下文就能模拟多用户并行操作这对并行测试来说价值极大。还有一个容易被忽略的设计Playwright把浏览器下载做成了内置命令。playwright install直接帮你下载对应版本的浏览器内核不需要再去手动找WebDriver从根本上解决了“浏览器版本不匹配”这个经典噩梦。2. 动手前的准备安装、浏览器与第一个脚本2.1 以Python为例的完整安装流程Playwright官方支持Python、JavaScript/TypeScript、Java和.NET。我个人最常用的是Python版本原因很简单Python生态里pytest配合得非常顺滑断言、报告、数据驱动都能复用现有经验。下面以Python为例。建议先建一个独立的虚拟环境避免污染全局环境mkdir playwright_demo cd playwright_demo python -m venv venv source venv/bin/activate # Windows下为 venv\Scripts\activate然后安装Playwright库本体和对应浏览器内核pip install playwright playwright install这里多说一句playwright install默认会下载三个内核Chromium、Firefox、WebKit。如果你只是日常做Web测试可以只装Chromium来省时间playwright install chromium一般国内网络环境下这个过程可能有点慢但它有断点续传实在等不及可以设置镜像环境变量再执行。装完之后验证一下python -c from playwright.sync_api import sync_playwright; print(ok)能正常输出ok说明环境就绪。2.2 第一个脚本打开页面并截图写第一个自动化脚本我习惯从“打开一个页面然后截图”开始这是一个最基础但能完整验证整个链路是否通畅的动作。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://example.com) page.screenshot(pathexample.png) print(page.title()) browser.close()这段代码背后的三个核心概念先搞清楚后面所有脚本都绕不开它们browser对应一个浏览器实例相当于你打开了一个浏览器程序。context通过browser.new_context()创建对应一个独立的会话上下文。一个浏览器可以开多个上下文上下文之间彼此隔离数据。page对应编辑器里的标签页绝大多数操作都在page上完成。一个上下文里可以继续开多个page。用生活场景来类比browser是一台电脑context是这台电脑上的不同用户账号page是某个账号打开的浏览器窗口标签页。第一次跑这个脚本如果你用的是headlessFalse会看到一个浏览器窗口一闪而过然后目录里多出一张png文件并且控制台打印出页面标题。这个最基本的闭环一旦跑通说明安装、内核下载、API调用全部正常可以进入下一步了。这里的headlessTrue是无头模式也就是浏览器不弹界面非常适合跑自动化任务。调试的时候更建议用headlessFalse能够直观看到每一步执行到哪里。3. 核心API与定位器新手必须吃透的几个关键点3.1 定位器是怎么解决“找不到元素”问题的写过自动化的人都有过这种体验用find_element_by_id、find_element_by_xpath直接操作WebElement一旦页面结构轻微变动脚本就跟着碎。Playwright把这一层抽象成了Locator定位器核心思想是不是“先找到元素再操作”而是“声明一个定位规则操作时再基于这个规则实时查询”。官方推荐的定位策略优先级是这样的get_by_role()按ARIA角色定位比如page.get_by_role(button, name提交)。语义清晰跟用户看到的页面元素对应最不容易受CSS类名变动影响。get_by_label()按表单标签定位对输入框特别友好。get_by_placeholder()按占位提示文字定位。get_by_text()按可见文本定位适合定位普通内容元素。get_by_test_id()按自定义测试ID定位这是最稳的方式推荐前端在测试环境加>page.fill(#login-form div:nth-child(2) input, abc)我更推荐写成page.get_by_label(用户名).fill(abc)两种写法都能跑但后者就算前端调整了DOM层级结构类名和结构变了只要label文案没变脚本依然稳定运行。3.2 自动等待到底在等什么Playwright的自动等待机制底层其实是一组“可操作检查”。每次click()、fill()、check()、select_option()之前它都会自动等待以下条件同时满足元素被附加到页面DOM上元素是可见的拥有非空的边界框元素是稳定的在连续两次动画帧之间不移动元素能接收事件没有被遮罩层挡住元素处于可编辑状态对输入类操作判断是否enabled这个机制最大的价值在于你不再需要手写“等待元素出现”的逻辑。以前Selenium要写WebDriverWait(driver, 10).until(EC.element_to_be_clickable(...))现在直接page.click()框架会自动帮你等到可点击再点。需要注意自动等待也有默认超时时间默认是30秒。如果你知道某个操作在极端情况下可能超过30秒可以在操作级别单独指定timeout参数page.get_by_text(提交订单).click(timeout60000)3.3 常用交互操作速览我整理了日常测试里最高频的操作及其用法方便你随时查阅操作代码示例说明查找定位器page.get_by_role(button, name登录)按角色与名称组合定位点击locator.click()自动等待可点击后点击输入文本locator.fill(hello)会先清空再输入适合表单渐进输入locator.press_sequentially(hello, delay100)模拟真人逐个按键适合触发搜索联想下拉选择locator.select_option(option_value)支持value、label、index勾选locator.check()/locator.uncheck()自动滚动到可见并点击获取文本locator.inner_text()拿元素内文本获取属性locator.get_attribute(href)拿元素属性键盘操作page.keyboard.press(Enter)发送按键事件悬停locator.hover()模拟鼠标悬停上传文件locator.set_input_files(file.txt)直接塞文件路径无需真实文件选择框一个我踩过坑的地方上传文件时很多人会用page.keyboard去模拟真实的文件选择对话框其实完全没必要。set_input_files直接定位到input typefile元素把文件路径传进去就可以简单稳定。3.4 断言也有一套独立体系Playwright把断言放在了expect这个对象上。Python版本里用法是from playwright.sync_api import expect expect(page).to_have_title(登录页面) expect(page.get_by_text(登录成功)).to_be_visible() expect(locator).to_contain_text(订单号: 10086)这些断言同样具备自动重试机制。也就是说断言失败时它不会立刻报错而是在超时时间内反复检查直到条件满足或者超时为止。这一点和前面说的可操作检查形成一个完整的“自动等待自动断言”闭环对动态渲染页面比如Ajax请求数据非常友好。4. 从能用到好用录制回放、网络模拟与移动端4.1 录制回放最快生成脚本的方式新手入门时与其对着API文档硬写不如先把录制功能用起来。执行playwright codegen https://你的目标测试站点它会自动打开一个浏览器窗口你在窗口里的每一次点击、输入、跳转都会被实时记录下来并且在旁边的生成器面板里自动生成对应的脚本代码。录制结束后可以一键切换查看Python / JavaScript / Java等不同语言版本的代码直接复制保存即可。录制生成的代码质量比我预想的高很多。它走的是官方推荐的定位器策略get_by_role、get_by_label等不是粗糙的XPath硬编码。我平时遇到不熟悉的业务页面会先用codegen快速录制一条主流程然后在生成的代码基础上做参数化、加断言、处理分支逻辑。这样一个新业务的冒烟测试通常十分钟内就能完成。4.2 网络拦截让测试不再依赖外部环境自动化测试最头疼的一类问题是被测页面里有第三方请求比如统计脚本、登录SDK、外部图片CDN这些服务一抖动测试就跟着失败而且原因根本不在你的业务代码里。Playwright的page.route()可以拦截和修改页面发出的请求。最简单的用法是直接中止不需要的请求page.route(**/analytics.js, lambda route: route.abort())也可以拦截后返回模拟数据实现接口Mockdef mock_image(route): route.fulfill(status200, content_typeimage/png, bodyb...) page.route(**/captcha-image, mock_image)这一点在测试环境中价值巨大。比如测试登录流程不想真的依赖短信验证码服务就可以用route拦截短信接口并直接返回一个固定验证码整个测试链路完全本地化稳定且高效。4.3 多标签页和iframe终于不再折磨人前面说过Selenium的窗口切换和iframe切换是老大难。Playwright在这两方面做得很顺手。多页面场景监听context.on(page)事件自动获取新页面对象不用手动切换with context.expect_page() as new_page_info: page.get_by_text(在新窗口打开).click() new_page new_page_info.value new_page.wait_for_load_state()iframe场景更简单直接通过frame_locator()进入对应iframe后续操作和普通页面完全一致frame page.frame_locator(#iframe-container) frame.get_by_placeholder(请输入密码).fill(123456)不需要任何“切换上下文”的API逻辑就是“定位到frame再在frame里继续定位元素”非常符合直觉。4.4 移动端模拟一套代码覆盖手机场景移动端适配的回归测试以前要么上真实设备要么用模拟器环境搭建成本很高。Playwright内置了一套设备参数可以直接复用主流手机型号的视口大小、UA、触摸特性devices p.devices iphone devices[iPhone 13] context browser.new_context(**iphone)这样创建出来的上下文页面会认为你是在一部iPhone 13上访问触摸事件、Retina缩放、Meta标签解析都按移动端逻辑处理。如果你担心WebKit内核里跑iOS页面和真实Safari仍有差异至少能拦截大量基础的移动端适配问题。另外多设备并行回归也不是难事。写一个循环依次模拟几种主流机型各跑一遍关键流程整套下来几分钟就能出结果比堆测试机成本低太多。4.5 并行执行与测试报告单独跑Playwright脚本可能不够日常工作中我更建议把它接入pytest。官方维护的pytest插件直接整合了fixture机制可以这样写def test_login(page): page.goto(http://localhost:8080/login) page.get_by_label(用户名).fill(admin) page.get_by_label(密码).fill(123456) page.get_by_role(button, name登录).click() expect(page.get_by_text(欢迎回来)).to_be_visible()配合pytest-xdist可以多进程并行跑pytest test_demo.py -n 4每个worker进程里天然拥有独立的浏览器上下文数据和状态彼此隔离这是Playwright相对传统框架在并行测试上的核心优势。调试复杂问题时务必开启trace录制。在创建上下文时设置context browser.new_context() context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue)测试结束后context.tracing.stop(pathtrace.zip)然后打开playwright show-trace trace.zip你能看到完整的页面快照、网络请求、控制台日志像回放视频一样重构整个执行过程。排查定位器失效问题这个工具比反复打印调试信息高效得多。5. 实战演练从登录到完整业务流前面讲的都是单个API和功能点现在把它们串起来写一个完整的实战Demo展示一个“从登录到网站后台处理业务”的完整链路。5.1 测试场景设计假设被测对象是一个本地部署的商城管理系统这里用本地Mock服务模拟不涉及任何真实业务系统。测试数据用测试环境专用账号脚本设计覆盖四步打开登录页输入账号密码登录进入商品管理列表新建一条商品记录校验列表中出现了新建的商品在动手写代码前我习惯先把这一步的数据整理出来步骤页面元素识别操作登录用户名输入框、密码输入框、登录按钮fill click进入商品管理菜单中的“商品管理”click新建商品点击“新增商品”按钮填写表单click fill校验结果商品列表中的名称单元格expect to_contain_text5.2 代码实现与要点from playwright.sync_api import sync_playwright, expect def test_main_business_flow(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() # 登录 page.goto(http://localhost:8080/login) page.get_by_label(用户名).fill(tester01) page.get_by_label(密码).fill(pass123) page.get_by_role(button, name登录).click() expect(page.get_by_text(欢迎回来tester01)).to_be_visible() # 进入商品管理 page.get_by_text(商品管理).click() expect(page).to_have_url(http://localhost:8080/products) # 新增商品 page.get_by_role(button, name新增商品).click() page.get_by_label(商品名称).fill(Playwright测试商品) page.get_by_label(商品价格).fill(99.9) page.get_by_label(库存数量).fill(100) page.get_by_role(button, name保存).click() # 校验是否出现在列表中 expect(page.get_by_role(row, namePlaywright测试商品)).to_be_visible() browser.close()这个脚本里有几个值得注意的细节。首先登录成功的断言我用的不是“直接断言URL”而是先断言“欢迎回来”文本可见再用to_have_url校验页面跳转。双断言的好处是如果登录异步处理出了问题第一个断言能更快暴露问题。其次保存操作点击后列表数据是异步加载的我没有手动等待直接对目标行做可见性断言自动等待机制会处理中间的时间差。5.3 数据驱动一份用例跑多组数据上面的硬编码方式演示足够但真实项目里往往需要覆盖多组账号和商品数据。用pytest的参数化可以把数据从脚本中拆出去import pytest pytest.mark.parametrize(username,password,expect_text, [ (tester01, pass123, 欢迎回来tester01), (tester02, pass123, 欢迎回来tester02), ]) def test_multi_user_login(page, username, password, expect_text): page.goto(http://localhost:8080/login) page.get_by_label(用户名).fill(username) page.get_by_label(密码).fill(password) page.get_by_role(button, name登录).click() expect(page.get_by_text(expect_text)).to_be_visible()测试数据也可以进一步放到外部JSON或者YAML文件里基于pytest的fixture机制动态读取。这里不展开那么多作为一个扩展方向记住就行。5.4 报告与CI集成本地跑通之后最终要进入CI流水线。代码仓库里建议增加pytest配置一键输出HTML报告pip install pytest-html pytest test_demo.py --htmlreport.htmlCI环境里记得关闭调试用的trace录制也不需要无头模式之外的参数调整。一般在CI机器上执行playwright install --with-deps chromium即可把系统依赖一并装上省去手动安装各种动态库的麻烦。6. 常见问题排查与避坑手册6.1 元素定位不到先别急着改定位器新手遇到“元素找不到”第一反应往往是“换个XPath试试”这是最无效的路径。按照下面几个方向排查大多数情况下能先于“改定位器”找到根因第一检查是否在iframe里。这是我见过最高频的坑。用DevTools先看元素是否被包裹在iframe标签内如果是就得用frame_locator进入对应iframe再定位。第二检查元素是否在shadow DOM里。现代前端框架经常用shadow DOM封装组件内部结构Playwright对open shadow DOM支持不错定位时会自动穿透但如果是closed状态就无能为力。遇到这种优先考虑给前端提需求加>playwright install --with-deps chromium它会自动用包管理器安装全部依赖。如果公司内网环境无法访问外网需要提前在相同发行版的机器上把依赖打齐这是CI环境最常踩的坑之一提前在流水线里固化好依赖安装步骤能省很多事。6.4 资源占用过高并行执行时如果直接用browser.launch()每个worker都会启动完整浏览器实例内存占用很容易翻车。更合理的使用方式是browser.new_context()共享浏览器进程上下文之间相互隔离但底层复用同一个浏览器内核进程内存开销会小一个数量级。如果你在跑大量用例记得在每个用例结束或fixture teardown时调用browser.close()或context.close()避免浏览器进程越积越多。pytest插件模式下fixture会自动管理生命周期这点确实省心。6.5 问题速查表症状大概率原因解决方向元素定位不到iframe / shadow DOM / 多个匹配确认元素实际位置改用语义化定位操作超时页面渲染慢/动画持续/资源加载卡住调整wait_until策略或forceTrue点击没反应元素被遮罩层遮挡检查是否有弹窗或loading浮层headless闪退系统缺动态库playwright install --with-deps并行后数据错乱测试数据隔离不彻底每个测试用独立上下文/独立账号浏览器版本对不上自动更新导致内核与驱动不匹配固定内核版本在CI里重新install6.6 一些实际的个人经验最后分享几个我自己折腾Playwright过程中的真实体会。第一测试环境里尽量给前端提需求加>