
做自动化测试这几年我换过好几套方案Playwright 配合 Python 算是我用得最顺手的一组搭档。它本身就是开源的浏览器自动化框架主打 Web 端到端测试支持 Chromium、Firefox、WebKit 三种内核Python 版本同步异步 API 都做得很清爽。我接触它之前主要靠 Selenium 撑场面选择器不稳定、等待全靠 sleep、弹窗处理麻烦这些问题在 Playwright 里基本被机制层面解决掉了。很多刚入门的读者问过我同一个问题能不能用一下午时间搭出第一个能跑、能断言、能保留证据的自动化脚本我的回答是可以而且不用依赖任何复杂的外部测试网站。这篇会从环境搭建讲起带你写通最小脚本再把它改造成正式的 Pytest 用例顺便把定位器、断言、调试工具和常见坑位一次串完代码都放在对应章节里直接复制。1. 为什么选 Playwright自动等待、多内核与适用场景1.1 自动等待机制才是最大的分水岭先说说大家最熟悉的痛点元素等待。以前写 Selenium 脚本最怕的就是刚跳完页面就去找元素报找不到加 time.sleep 又不知道该等多久等短了照样翻车等长了拖慢整个测试。Playwright 的做法是从机制上改变这一切它对每个动作内置了可操作性判断。比如点击一个按钮它会先轮询检查这个元素是否可见、是否稳定、是否被其他元素遮挡、是否处于可交互状态全部满足才真正执行点击默认超时 30 秒。这个机制靠的是查询-检查-操作三步一体的流程。你在代码里传给它一个选择器它会反复重新查询 DOM等元素状态达到可交互再操作期间不需要任何显式等待代码。结果是脚本变得非常直白写法和手工操作步骤一一对应但稳定性却比塞了一堆 sleep 的老脚本高得多。我见过很多从 Selenium 转过来的同事改完第一批用例之后的第一反应基本都是原来真的可以不用等。1.2 三种浏览器内核一份代码全覆盖Playwright 的最大卖点之一是一套代码能在 Chromium、Firefox、WebKit 三种浏览器内核上跑而且不需要自己管理驱动。它有自己的浏览器二进制管理机制执行 playwright install 后会按固定版本把内核下载到本地缓存目录项目里锁定好依赖版本测试环境就能保持一致。这一点看似小实际省掉的坑非常多。Selenium 时代驱动版本和浏览器版本严格对应浏览器一自动升级驱动立刻报错Playwright 把浏览器二进制和库捆绑管理安装哪个版本就配套哪个内核升级也是主动执行的。对需要在多内核上验证兼容性的项目来说这一条基本可以无脑选它。哪怕你暂时只测 Chromium这个设计也意味着你在 CI 上构建出来的环境是确定的不会出现本地能过、CI 上莫名其妙挂的版本漂移问题。1.3 适用场景与选型建议什么项目该用它我个人的判断标准很简单只要需求是完整打开真实浏览器走一遍真实用户的路径就属于 Playwright 的射程。最典型的是 Web 端到端测试、登录注册流程回归、电商核心链路冒烟、自动化截图与页面监控、内部工具的数据回填和批量操作。很多偏运维的活儿比如每天定时打开某个后台导出报表或者批量对一组页面做可用性检查用它也能干得很顺手。反过来如果只是测纯接口、做单元测试就别拿它去凑热闹轻量方案在速度和资源占用上优势明显。另外要提醒一下自动化工具有能力不等于可以滥用去抓别人站点数据之前先确认目标站点的服务条款和使用边界这是从业者基本的自觉。工具本身不负责合规责任在你自己。2. 十分钟搭好环境Python 虚拟环境与 Playwright 安装验证2.1 准备 Python 环境和虚拟环境Playwright 的 Python 库要求 Python 3.8 以上我建议直接装 3.10 或更高版本类型标注和内置特性会让后面写代码更舒服。Windows、macOS、主流 Linux 的安装差异不大核心是先建一个干净的虚拟环境再往里面装依赖。python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate为什么一定要虚拟环境自动化测试项目往往不止一个依赖工具pytest、playwright、各种辅助库混装在一起时间一长必然出冲突。虚拟环境把项目依赖隔离到独立目录换项目就换环境谁也不碍谁。一个小细节Windows 上如果装了多个 Python 版本用 py -3.10 -m venv .venv 显式指定解释器版本避免建完环境才发现用的是旧版 Python。2.2 安装 Playwright 库与浏览器内核环境激活后输入两条命令pip install playwright playwright install chromium第一条装 Playwright 的 Python 库里面包含完整 API、命令行工具和选择器引擎第二条下载配套的 Chromium 浏览器内核。很多新手装完库就直接跑脚本然后报错说浏览器启动失败基本都是漏了后面这一步。如果后面需要在 Firefox 和 WebKit 上验证再把对应内核补上playwright install firefox webkit如果你的机器是比较精简的 Linux 环境浏览器内核装完可能还缺系统层面的运行库这时候可以先执行 playwright install-deps 让工具自动补齐系统依赖然后再执行 playwright install。我建议连同 pytest-playwright 一起装一步到位。它是 Pytest 的官方插件提供 page、context、browser 等现成夹具还能在命令行里切换有头无头、指定浏览器测试组织效率高很多pip install pytest-playwright2.3 冒烟脚本验证安装成功环境装完先别急着写正式用例跑一个最小脚本确认整条链路是通的。新建 smoke.pyfrom 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) print(页面标题:, page.title()) browser.close()执行 python smoke.py控制台输出页面标题: Example Domain环境就完全可用了。我把 headless 设成 True 是为了无头运行适合快速验证和以后挂 CI第一次想看真实浏览器长什么样改成 False 即可。这一步没有通过的话先检查虚拟环境是否激活、playwright install 是否执行成功两个点排查完基本就能解决。3. 吃透三大核心对象从最小脚本理解 Browser、Context、Page3.1 同步 API 和异步 API 怎么选Playwright 的 Python 库提供两套 APIsync_api 和 async_api。同步版本一行接一行往下执行心智负担小写测试用例时最直观也是官方文档和社区例子里的主流写法。异步版本配合 asyncio适合高并发和已经全面异步化的项目但它要求你在编码时认真管理任务生命周期难度高一个台阶。我的建议是新手和大部分测试项目直接用同步 API。同步版本已经能支撑绝大多数场景且和 Pytest 兼容得非常好。异步 API 好在并发吞吐但自动化测试里的并发通常更适合交给分布式任务框架去解决语言层面的异步不一定是收益最大的方向。等你同步版本用熟了再对照官方文档切异步核心调用结构几乎不变只是把每个动作变成 await转换成本很低。from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(https://example.com) print(await page.title()) await browser.close() import asyncio asyncio.run(main())3.2 Browser、Context、Page 的关系与隔离逻辑理解对象模型是写 Playwright 的分水岭。三者的关系可以这样记Browser 是整个浏览器进程Context 相当于一个独立的隐身会话Page 是这个会话里的一个标签页。每次创建一个 Context它都有一份全新的 cookie、存储、缓存和各种权限设置互相之间不共享。这个设计的价值在测试里极明显。你要模拟两个互不干扰的用户就用两个 Context一个用例要反复清空状态就创建新 Context 而不去手动清理旧数据。我经常看到有人直接在默认 Context 里连续开多个页面结果用例之间 cookie 串了登录态互相影响排查半天。养成每个测试场景一个独立 Context的习惯能在源头上消掉一大类 flaky 问题。3.3 启动、关闭的正确顺序与资源管理启动顺序是固定的先启动 Playwright 运行时再启动浏览器再创建上下文最后打开页面。关闭时反过来先关闭页面或上下文再关闭浏览器。用 with sync_playwright() 包住整个逻辑Playwright 会在退出时自动清理运行时和浏览器进程代码简洁也安全。但我仍建议在多浏览器或多上下文的项目里显式调用 context.close() 和 browser.close()这样资源释放时机可控排查并发问题时也清楚。另外要注意浏览器关闭会同时结束它下面所有上下文和页面不需要也不应该重复调用页面的 close 方法重复关闭有时候反而会触发不必要的异常。4. 手写第一个测试脚本本地页面实战与 Pytest 用例改造4.1 准备一个本地测试页面不用依赖外部网站为了让教程可复现我不打算拿某个线上站点当靶子而是直接在本地造一个登录页面。这样你不需要考虑外部网站的可达性、站点改版、权限验证这些额外因素专注理解 Playwright 本身的工作方式。新建一个目录 demo_pages把下面的 login.html 保存进去!DOCTYPE html html langzh-CN head meta charsetutf-8 title本地登录演示页/title /head body h2账号登录/h2 form idlogin-form label forusername用户名/label input idusername nameusername typetext placeholder请输入用户名 label forpassword密码/label input idpassword namepassword typepassword placeholder请输入密码 button idsubmit-btn typesubmit登录/button /form p idresult/p script document.getElementById(login-form).addEventListener(submit, function (e) { e.preventDefault(); var u document.getElementById(username).value; var p document.getElementById(password).value; var r document.getElementById(result); if (u admin p 123456) { r.textContent 登录成功欢迎 admin; } else if (u || p ) { r.textContent 用户名和密码不能为空; } else { r.textContent 用户名或密码错误; } }); /script /body /html保存完成后在 demo_pages 目录下启动一个最简单的 HTTP 服务命令是cd demo_pages python -m http.server 8000启动之后用浏览器打开 http://127.0.0.1:8000/login.html 确认页面正常显示这个操作会为后面的自动化脚本提供一个稳定的本地靶场。这个页面很简单但覆盖了自动化测试最核心的三类元素文本框、按钮、点击后的文本反馈。4.2 完整脚本拆解从打开页面到断言截图下面这段是本次实战的核心脚本。它做了一件完整的事打开页面、填表单、点登录、检查结果、截图留证整个过程就是你在手工测试时做的那几步现在完全交给代码。from playwright.sync_api import sync_playwright BASE_URL http://127.0.0.1:8000/login.html def login_with(username: str, password: str, headless: bool True): with sync_playwright() as p: browser p.chromium.launch(headlessheadless) page browser.new_page() page.goto(BASE_URL) page.get_by_label(用户名).fill(username) page.get_by_label(密码).fill(password) page.get_by_role(button, name登录).click() page.locator(#result).wait_for(statevisible) result_text page.locator(#result).text_content() print(页面反馈:, result_text) page.screenshot(pathlogin_result.png) browser.close() if __name__ __main__: login_with(admin, 123456) login_with(admin, wrong)逐行说一下关键点。get_by_label(用户名) 是利用 label 标签的 for 属性定位输入框这是最贴近表单语义的做法比硬写 CSS 选择器稳得多。get_by_role(button, name登录) 是按可访问性角色和按钮文字定位按钮即使按钮改了几个 class这段代码都不受影响。点击之后我没有立刻读结果而是先执行 page.locator(#result).wait_for(statevisible)。这个页面是同步 JS 直接改文本实际上不等也能读到但养成读数据前先等待目标出现的习惯遇到异步渲染的页面时就能少踩很多坑。screenshot 是证据保留环节出问题时一张图比一行日志直观太多。运行脚本后控制台会依次打印页面反馈: 登录成功欢迎 admin和页面反馈: 用户名或密码错误。如果你把 headless 改成 False 再跑一遍还能亲眼看到浏览器真的被自动操作了那种感觉比纯看输出要踏实得多。这一步建议新手都试一次对理解自动化和手工操作之间的对应关系非常有帮助。4.3 用 Pytest 升级成正式用例夹具与命令行参数上面那版脚本适合理解流程但真正拿来做回归测试还是要交给 Pytest。pytest-playwright 提供的 page 夹具里每个测试会自动获得全新的浏览器上下文用例之间天然隔离这是测试框架级别的保障比自己在脚本里创建浏览器、关闭浏览器干净很多。先在项目根目录建一个 test_login.pyfrom playwright.sync_api import Page, expect BASE_URL http://127.0.0.1:8000/login.html def test_login_success(page: Page): page.goto(BASE_URL) page.get_by_label(用户名).fill(admin) page.get_by_label(密码).fill(123456) page.get_by_role(button, name登录).click() expect(page.locator(#result)).to_have_text(登录成功欢迎 admin) def test_login_wrong_password(page: Page): page.goto(BASE_URL) page.get_by_label(用户名).fill(admin) page.get_by_label(密码).fill(wrong) page.get_by_role(button, name登录).click() expect(page.locator(#result)).to_have_text(用户名或密码错误)注意 expect(...).to_have_text(...) 不是普通断言它会自动重试直到文本匹配或超过默认超时时间。这对异步渲染的页面特别友好元素还没更新完它就一直等而不是一上来就失败。执行方式也很灵活pytest -v pytest --headed -v pytest --browserfirefox -v第一条按默认的 Chromium 无头模式跑全部用例第二条弹出真实浏览器窗口方便调试第三条切到 Firefox 验证跨内核兼容。如果你想在 CI 里保留故障现场推荐加 --screenshotonly-on-failure 和 --videoretain-on-failure失败时自动收集截图和录像排障效率立刻上来了。5. 定位器选择与交互操作写出稳定脚本的核心方法5.1 首选角色定位器让选择器贴近用户视角脚本稳定性很大程度上取决于选择器怎么写。我最推荐的是角色定位器 get_by_role它基于可访问性树来定位元素本质上描述的是这个元素在用户和辅助技术眼里是什么、叫什么。比如 get_by_role(button, name登录)找的就是页面上语义为按钮、可访问名称为登录的元素。为什么它比 CSS 稳定因为 CSS 选择器依赖结构页面改一个 class、调一层 DOM脚本立刻断可访问性名称则跟着页面语义走只要按钮文案和角色不变重构样式也不会影响脚本。写新用例时我基本都会先试试 get_by_role配不上再考虑其他方式。一个实用小技巧用 name 参数做匹配时可以通过正则传参get_by_role(button, namere.compile(登录|提交)) 这类写法在文案经常微调的项目里非常实用。5.2 其余定位方式怎么选标签、占位符、CSS 和 XPath角色定位器不是万能的有些元素语义不明显就得配合其他策略。常用的一并给你列清楚定位器使用场景示例get_by_label表单输入框有 label 关联时优先page.get_by_label(用户名)get_by_placeholder只有占位提示的输入框page.get_by_placeholder(请输入邮箱)get_by_text通过可见文本定位普通元素page.get_by_text(账号登录)get_by_alt_text图片等带替代文本的元素page.get_by_alt_text(logo)get_by_title带 title 属性的元素page.get_by_title(帮助文档)locator(css...)没有语义时的兜底选择器page.locator(#submit-btn)locator(xpath...)复杂层级或依赖文本的极端场景page.locator(xpath//button[contains(text(),登)])我的取舍原则是能用语义定位就不写样式结构选择器CSS 兜底也尽量用稳定属性。id 和稳定的>with page.context.expect_page() as new_page_info: page.get_by_role(link, name打开新窗口).click() new_page new_page_info.value new_page.wait_for_load_state() print(new_page.title())iframe 里的元素不能直接在当前页面里定位要先进 frame_locator类似切到另一个文档内部去找按钮page.frame_locator(#main-frame).get_by_role(button, name提交).click()系统弹窗要提前挂监听默认情况下不处理就会一直挡住后续操作page.on(dialog, lambda dialog: dialog.accept()) page.get_by_role(button, name触发弹窗).click()文件上传用 set_input_files把文件路径传给对应的 input 元素即可page.set_input_files(#upload-input, [report.pdf])这四个场景单独看都不复杂加在一起几乎能覆盖大多数业务系统的交互边界。真正写的时候建议每个都实际跑一遍因为跨标签页和跨 frame 的错误很隐蔽容易在项目里大面积爆发。6. 常见报错速查与调试实录这些坑我替你踩过了6.1 高频报错速查表我把实际项目里出现频率最高的几类问题汇总成一张表方便你遇到报错时对照排查。报错或现象常见原因处理思路TimeoutError 等待超时元素没出现、选择器不准确、页面脚本异常先用 codegen 确认选择器改用 expect 等待条件打开 trace 看现场strict mode violation选择器同时匹配了多个元素增加限定条件让定位器唯一确实有重名时用 .nth() 明确索引Element is not visible / not stable元素被遮挡、页面还在滚动或动画等待遮挡层消失点击前先处理 fixed 弹层必要时 scroll_into_view_if_needed()点击后没有任何反应有 dialog 挡在事件流前面page.on(dialog, ...) 提前挂监听处理弹窗新标签页打开后元素找不到当前的 page 对象还指向旧标签页用 expect_page 捕获新页面切换到新 page 再操作iframe 里元素定位不到定位器停留在外层页面用 frame_locator 进入 iframe 后再定位中文输入出现乱码部分输入框对模拟按键不友好优先用 fill 直接赋值避免逐字 send_keys无头与有头表现不一致渲染、字体、视口差异显式设置 viewport必要时用 headed 模式复现现场这张表解决的不全是方案问题很多是习惯问题。比如超时第一时间不是加大超时时间而是确认元素在页面里真实存在、选择器真的写得对。调试工具比盲目调参高效得多。6.2 调试四件套codegen、Inspector、Trace 与证据收集我调试 Playwright 脚本几乎不靠读日志硬想有一套固定动作。第一个是 codegen它能把手动操作直接录成脚本。启动命令是playwright codegen http://127.0.0.1:8000/login.html它会打开一个带操作面板的浏览器你点哪里、填什么右侧自动生成对应代码。写新用例时先点一遍生成的代码再做修改效率非常高。第二个是 Inspector 调试器在脚本里某行插入 page.pause()运行后浏览器会停顿并打开 Playwright Inspector 面板可以一步步查看元素、执行操作、验证选择器。它还能快速测试当前选择器匹配了几个元素定位严格模式违规特别有用。第三个是 Trace 追踪。运行 pytest 时加 --tracingretain-on-failure失败用例会自动生成 trace.zip然后执行 playwright show-trace trace.zip打开一个包含完整时间线的查看器能看到每一步操作的快照、网络请求和控制台日志。很多疑难杂症我都是靠这个时间线定位的。第四个是证据收集前面提过的 --screenshotonly-on-failure 和 --videoretain-on-failure这两组参数建议团队统一写进 Pytest 配置里每个人跑用例挂了都知道去哪看截图和录像。6.3 几个最典型的踩坑记录分享几条我记忆比较深的经验教训。第一永远不要依赖用例执行顺序。Pytest 默认按文件名和行号执行但你要假设下一次执行顺序是随机的。某个用例依赖前一个用例的登录态一旦加了并发就全废。正确做法是每个用例独立准备数据用独立 Context 隔离登录态用例之间不共享任何东西。第二别覆盖掉插件提供的 page 夹具。pytest-playwright 已经给了很完善的 page、context、browser 夹具有些同学为了一点定制需求自己写了一个叫 page 的 fixture 把它覆盖了结果测试隔离性直接失效。如果真要定制启动参数用 pytest 配置里的 launch 参数或者自定义一个名字不同的 fixture 组合出你需要的东西。第三写用例的时候多用固定测试数据。真实业务系统里常见的问题是同一个用户名反复注册会冲突、验证码没法自动读取、时间戳导致断言不稳定。要么用固定测试账号并在用例里做好清理要么生成唯一随机数据再断言。自动化的敌人从来不是复杂而是不确定性所有让结果可预测的努力都值得。最后再从个人使用经验出发多说一句写脚本时我习惯把关键操作封装成小的页面动作函数比如 login_with(username, password)让用例主体读起来像自然语言。团队协作时别人扫一眼就能看懂测试意图后续维护成本低很多。接下来值得继续做的方向是把 Pytest 配置里的 base-url、浏览器和证据收集参数固定下来然后接到 CI 上每次提交自动跑一遍回归。我自己维护的几个项目都走这个路子跑得越勤发现的回归越多脚本反而越写越稳。