首页 / 资讯中心 / 文章详情

PlayWright自动化测试实战:从脚本编写到CI集成全指南

PlayWright自动化测试实战:从脚本编写到CI集成全指南 ★ FEATURED ARTICLE
PlayWright这个词我第一次认真研究是在一个自动化改造项目里。当时手头有一套老脚本用的是传统WebDriver方案跑一遍要二十多分钟还经常因为元素加载慢、选择器失效突然挂掉。后来切到PlayWright稳定性和编写效率都有明显提升。这篇东西不是我随手整理的API列表而是我边用边踩坑之后沉淀下来的完整路线从安装环境、写第一个脚本到定位交互、断言截图、网络拦截、CI集成再到维护层面的经验。无论你是测试开发、前端工程师还是平时想让脚本帮忙做点重复劳动的人只要愿意花几小时读完都能很快上手。1. 为什么选PlayWright而不是其他自动化工具1.1 三大主流工具的横向对比做浏览器自动化绕不开三个名字Selenium、Cypress、PlayWright。你如果刚入行很容易被各种推荐搞晕。我先给个直观的对比表后面再展开讲我的选择逻辑。对比维度SeleniumCypressPlayWright自动等待主要靠手动等待写错就sleep内置自动等待相对省心内置自动等待Action前自动检查可操作状态浏览器驱动需要单独下载和管理Driver不需要但对浏览器有限制不需要安装时会自动下载对应浏览器多标签/多页面支持但切换逻辑偏繁琐限制较多体验一般原生的多页面管理API简洁iframe处理支持需要切来切去较好较好推荐用frame_locator网络请求拦截弱要借助第三方代理支持原生支持写法很直接调试手段主要靠日志和截图自带IDE体验好Codegen录制 Trace Viewer回放定位问题很强从这个表能看出来PlayWright在等待机制、调试体验和网络拦截上占优势。我当初选择它不是因为它在所有维度都碾压而是它在日常写脚本最关心的几个点上平衡得非常好少写很多等待逻辑调试时有迹可循切换到不同浏览器也方便。1.2 真正能提升效率的几个特性说几个我实际用下来感受最明显的点。第一是免驱动管理。用Selenium时每换一个浏览器版本就要重新下载对应Driver路径不对就报错。PlayWright在安装的时候会把浏览器一起装好调用的时候自动复用省掉了这一层麻烦。第二是动作前的自动等待。很多脚本挂掉不是操作逻辑错是页面还没加载完元素没出现就点过去了。PlayWright的click、fill这些动作内置了等待元素可见、稳定、可接收事件的机制能大幅减少那种无意义的time.sleep。后面我会专门讲这个因为很多人没理解透。第三是Codegen录制器。直接在命令行跑playwright codegen 地址它会打开浏览器你手动操作一遍它就生成对应的脚本。我平时遇到复杂页面会先录一遍再手工改比纯手写快很多。第四是Trace Viewer。跑完一个用例会生成一份追踪档案里面包含每一步的截图、DOM快照、网络请求时序。用例失败时我不需要靠猜直接打开看每一步到底发生了什么。2. 环境准备与第一个可运行脚本2.1 安装和基础配置用Python写PlayWright的场景比较多我就以Python为例。建议Python 3.7以上版本因为PlayWright的同步API依赖一些较新的语法特性。安装分两步先装库再装浏览器pip install playwright playwright install chromium我只装了chromium因为大多数业务系统跑在Chromium内核上够用。如果你后面需要测Safari或者Firefox再用同样的命令补装firefox和webkit。注意浏览器文件比较大下载时间取决于本地网络尽量别中途断网。装完之后建议顺手建一个虚拟环境。我见过不少人在全局环境里装了一堆依赖版本互相影响排查起来很痛苦。用virtualenv或者conda都行隔离干净最重要。2.2 写一个最小脚本先来一个最简单但完整的流程打开浏览器访问本地测试地址输出页面标题。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(http://127.0.0.1:8080) print(page.title()) browser.close()逐行说一下with sync_playwright() as p是上下文管理进入时启动PlayWright驱动退出时自动做清理。browser p.chromium.launch(headlessFalse)启动Chromium实例。headlessFalse是有头模式你能看到浏览器窗口适合调试。CI里跑一般用headlessTrue快一些。page browser.new_page()创建一个页面对象之后所有操作都围绕这个page展开。page.goto(http://127.0.0.1:8080)让页面跳转到目标地址。page.title()拿当前页面标题。browser.close()关闭浏览器释放资源。这里有个容易忽略的点如果你用的是本地开发服务地址写成127.0.0.1比localhost更快能避开一些网络解析上的坑。2.3 浏览器下载失败和常见环境问题playwright install最常遇到的问题是下载中断或者特别慢。我踩过的坑主要有几种pip安装没问题但浏览器下载走到一半超时。这种直接重试很多情况是网络抖动。磁盘空间不足。浏览器包动辄一两百MB装到C盘爆掉是可能的所以先检查剩余空间。企业网络有缓存策略导致下载路径被拦截。可以先确认本机能否正常访问下载地址。如果安装失败很久还解决不了一个省事的方法是找运维或基础团队要一个离线包装到固定目录后通过环境变量指给PlayWright。不过这只建议在网络受限环境里用正常开发还是在线安装最省心。3. 核心操作定位、交互与状态等待3.1 选择器优先级能让人看懂是第一原则定位元素是自动化脚本的根基。我见过新手写脚本F12复制一个超长XPath直接贴进去当时能跑页面一改就崩。PlayWright提供了更推荐的选择器体系优先级大概是这样的第一推荐基于角色和语义化元素。page.get_by_role(button, name保存).click() page.get_by_label(用户名).fill(admin) page.get_by_placeholder(搜索商品).fill(无线鼠标)这些API依赖可访问性语义页面结构微调时不容易失效。比如按钮从第一个位置挪到第二个位置只要按钮文本和角色没变脚本就还能跑。第二推荐简单的CSS选择器。page.locator(button.submit).click()CSS选择器本身可读性好配合data-testid这类稳定属性用效果很好。比如page.locator([data-testidcart-count]).click()最不推荐深层级XPath。page.locator(xpath/html/body/div[2]/div[1]/div[4]/button[3]).click()这种路径和页面结构强绑定前端稍微加一层div脚本就废了。能用角色和文本定位就用它们不行再加语义化属性最后才考虑XPath。3.2 自动等待机制别再用time.sleep了很多新手习惯在操作前写time.sleep(2)这是自动化脚本里最大的坏味道。固定时间等待只是掩盖问题不是解决问题。PlayWright的设计哲学是所有动作会先等待目标元素满足可操作条件然后再执行。比如page.click()会等待元素同时满足已经挂载到DOM、可见、稳定不抖动、能接收事件。所以大多数情况下你不需要额外加等待它自己会等。这个机制对脚本稳定性提升非常明显。如果真的需要等某个元素出现优先用显式等待page.wait_for_selector(.toast-message, statevisible)比如点击保存按钮后页面会弹一个提示框我希望等它出现再做断言。这种写法比sleep精确得多提示框500毫秒出来就是500毫秒3秒出来就等3秒不会盲目等待。那什么时候需要用sleep我基本只在某些动画场景才考虑比如图表从旧状态切换到新状态中间有个过渡动画等它完全渲染完也许要固定间隔。即便如此我也会优先等一个代表“动画结束”的信号实在没有信号才妥协。3.3 常用交互动作与表单处理讲几个日常使用频率最高的动作。fill和type很多人分不清。fill是直接设置输入框的值速度快、结果可靠type是模拟键盘逐个字符输入会触发键盘事件适合需要输入过程来触发搜索建议的场景。我的建议能fill就fill省时间只有需要测试输入联想时才用type。下拉选择用select_optionpage.get_by_label(城市).select_option(北京)文件上传用set_input_filespage.set_input_files(input[typefile], test.csv)复选框和单选框用check和uncheckpage.get_by_role(checkbox).check()需要注意set_input_files只对原生文件选择框有效。如果页面用了自定义上传组件可能要把文件路径设置到隐藏的input上或者走别的方式这要看具体实现。4. 实战从场景到端到端测试用例4.1 场景拆分与用例设计写自动化脚本之前先拆场景。我用一个常见的电商购物流程来举例。步骤操作校验点登录输入账号密码点击登录跳转用户中心显示用户名搜索在搜索框输入关键词回车结果列表包含商品卡片加购点击第一个商品的“加入购物车”购物车数量变为1下单进入购物车点击结算订单确认页打开每一步都要有校验点不能只操作不断言。否则脚本跑到最后你不知道前面哪一步悄悄失败了。测试数据也要注意。每个用例尽量用独立的账号和数据避免互相污染。比如登录一个账号往购物车加了一件商品如果不清理下一次跑断言购物车数量变成2就会挂。最好在测试开始前重建状态跑完后再清理数据。4.2 完整脚本示例下面这段是一个可运行的端到端脚本示例地址我写的是本地服务你可以换成自己项目。from playwright.sync_api import sync_playwright def run(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() try: # 登录 page.goto(http://127.0.0.1:8080/login) page.get_by_label(用户名).fill(demo_user) page.get_by_label(密码).fill(demo_pass) page.get_by_role(button, name登录).click() page.wait_for_url(**/account) assert page.get_by_text(demo_user).is_visible() # 搜索商品 page.get_by_placeholder(搜索商品).fill(无线鼠标) page.keyboard.press(Enter) page.get_by_role(button, name加入购物车).first.click() # 进入购物车校验 page.goto(http://127.0.0.1:8080/cart) assert page.get_by_text(无线鼠标).is_visible() finally: browser.close() if __name__ __main__: run()这里有几个细节page.wait_for_url(**/account)不是等待固定时间而是等待当前URL匹配指定模式。登录成功后页面会跳转到用户中心URL变了才继续往下走这样比断言“用户名是否出现”更前置能避免在跳转瞬间做断言造成的闪断。page.get_by_role(button, name加入购物车).first.click()里的.first是取第一个匹配元素。搜索结果页通常会有多个商品和多个“加入购物车”按钮不加.first的话遇到多个元素满足条件点击动作会报错。finally块保证无论断言失败还是异常浏览器都会被关闭避免残留进程占着系统资源。4.3 断言、截图与Trace记录官方推荐用expect来做断言from playwright.sync_api import expect expect(page).to_have_title(用户中心) expect(page.locator(.cart-badge)).to_contain_text(1)expect自带重试和自动等待比直接写assert更稳。比如一个元素可能延迟100毫秒才出现直接assert就挂了expect会等到它满足条件。失败时保存截图是排查问题的关键手段。我的习惯是在异常处理里加try: expect(page).to_have_text(下单成功) except Exception: page.screenshot(pathfailure.png) raise截图能帮你看到现场长什么样是定位问题最快的方式。追踪记录则更进一步。跑用例前开启Tracecontext.tracing.start(screenshotsTrue, snapshotsTrue) # 执行用例步骤 context.tracing.stop(pathtrace.zip)跑完生成的trace.zip可以用playwright show-trace trace.zip打开。里面的时间线能看到每一步的截图、DOM状态和网络请求排查那种“明明断言失败但不知道是哪一步开始出错”的问题效率极高。这是我日常最常用的功能没有之一。5. 进阶能力拦截、多页面与状态复用5.1 网络请求拦截和MockPlayWright的路由拦截能力能让你在不需要后端配合的时候直接控制前端拿到的数据。这在开发和测试联调阶段非常有用。屏蔽图片和静态资源测试时能节省很多流量和等待时间def block_resource(route): if route.request.resource_type image: route.abort() else: route.continue_() page.route(**/*, block_resource)模拟接口返回page.route(**/api/user/info, lambda route: route.fulfill( status200, content_typeapplication/json, body{name: mock_user} ))这样即使后端接口还没开发完前端页面也能用假数据跑起来。必须注意在route回调里fulfill、abort、continue_三选一必须调用一个否则请求会一直挂起。我见过有人漏掉某个分支结果页面等接口等了半天都不动还以为网络出问题了。5.2 多页面和多标签页有些业务操作会打开新标签页。如果你直接写page.click()然后马上操作新页面大概率会踩空。正确做法是用context.expect_page()来捕获新页面with context.expect_page() as new_page_info: page.click(a[target_blank]) new_page new_page_info.value new_page.wait_for_load_state()context.expect_page()会在点击动作后等待新页面创建完成然后返回新页面的page对象。拿到之后还要等它加载完再操作。如果你在同一个context里开了多个页面可以通过context.pages拿到所有页面列表根据URL或者标题找到自己要操作的那个。注意不要用browser.contexts[0].pages[0]这种硬编码索引页面顺序不稳定很容易引起“偶发失败”。5.3 复用登录状态自动化测试里最讨厌的事就是每个用例都走一遍登录。实际上登录成功后可以把状态保存下来context.storage_state(pathstate.json)下次直接加载context browser.new_context(storage_statestate.json)这个文件里存的是Cookie和localStorage数据插件会自动带过去。实际项目中一些后台系统登录比较繁琐用了这个方案后用例执行时间能缩短不少。但这里有个坑登录状态是有时效的。Token过期后加载旧状态会直接跳转回登录页用例照样挂。我建议把保存状态这个动作放在测试job的前置步骤里每次跑之前重新登录一次再保存而不是长期依赖一个固定文件。另外多套测试环境要分开保存别把预发环境的登录态带到生产环境去测。5.4 移动端模拟不需要真机也能模拟移动端浏览器。通过设置viewport和user-agent可以模拟常见手机尺寸context browser.new_context( viewport{width: 375, height: 812}, user_agentMozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) )这样可以测试移动端布局、响应式页面和移动端专属适配逻辑。配合page.touchscreen.tap()还能模拟触摸点击。对于没有移动端真机资源的团队来说这个方案成本很低可以作为基础回归手段。6. 常见问题与排查技巧实录6.1 超时问题超时是自动化脚本最经典的报错。常见表现是TimeoutError或者Element is not visible。我的排查顺序是先看是哪个Locator在等。报错信息里会告诉你它找不到哪个元素。如果元素真的存在大概率是页面还在加载可以把超时调大一点page.set_default_timeout(15000)如果元素不显示要看它是否被遮住。比如旁边有个遮罩层挡住按钮PlayWright会判断这个元素不可操作一直等到超时。还有一个隐蔽问题有些页面会持续发心跳请求导致networkidle这种状态永远等不到。所以我不建议滥用page.wait_for_load_state(networkidle)在长轮询页面里它会一直阻塞。优先用业务信号比如等待某个具体元素出现。6.2 选择器不稳定选择器不稳定通常有几种原因元素ID或class带动态后缀比如iditem_1689123456789。这种情况别用完整ID改用前缀匹配或者data-testid。页面改版后文本变了导致get_by_text找不到。这时优先用角色比如按钮的文案变了但语义不变get_by_role(button, name确认)可能还能工作。最稳妥的方案是推动开发在关键节点上加>frame page.frame_locator(iframe[title支付表单]) frame.get_by_label(卡号).fill(4111111111111111)如果遇到多层iframe嵌套就链式调用page.frame_locator(iframe[title外层]).frame_locator(iframe[title内层]).get_by_label(验证码).fill(1234)用这个写法就不用自己去维护iframe的切换状态。有一段时间我在老项目里写Selenium切iframe切到怀疑人生换成PlayWright之后这种烦恼少了很多。6.4 弹窗和确认框自动化碰到alert、confirm这类JS弹窗时如果不处理脚本会停在原地。处理方式是在页面上挂一个监听page.on(dialog, lambda dialog: dialog.accept())如果想点取消就改成dialog.dismiss()。遇到不同类型的弹窗可以在回调里判断类型再决定是接受还是取消def handle_dialog(dialog): if dialog.type confirm: dialog.accept() else: dialog.dismiss() page.on(dialog, handle_dialog)6.5 headless模式与有头模式差异有些脚本在有头模式跑得好好的切成headless就出问题。常见差异包括字体渲染、动画效果、GPU相关行为。排查思路很简单先用有头模式跑一遍确认通过再切headless跑。如果只在headless失败优先看是不是GPU相关。可以启动时加参数禁用GPUbrowser p.chromium.launch(headlessTrue, args[--disable-gpu])还可以加--no-sandbox容器环境里经常要用。6.6 并行执行资源占用除了测试正确性执行效率也很重要。并行跑用例时如果每个用例都启动新的浏览器进程资源占用会很高还容易互相干扰。我的做法是一个浏览器实例下用多个context和page来隔离用例。context browser.new_context() page context.new_page()每个用例独立context数据互不干扰但底层共享同一个浏览器进程资源消耗小很多。这也是PlayWright在架构上比传统方案有优势的地方。7. 在CI/CD中跑起来7.1 用pytest组织测试和夹具日常项目里我不会把脚本写成一个个裸函数而是用pytest来组织用例。先定义一个夹具管理浏览器生命周期pytest.fixture(scopesession) def browser_context(): with sync_playwright() as p: browser p.chromium.launch() yield browser browser.close()然后用例函数就可以这样写def test_login(browser_context): context browser_context.new_context() page context.new_page() page.goto(http://127.0.0.1:8080/login) # 业务步骤 context.close()pytest天然支持失败重试和测试收集配合pytest-xdist还能多进程并行执行非常契合自动化测试的需求。7.2 并行执行与分片策略本身比较独立的用例可以用pytest -n 4开4个进程并行跑速度提升明显。但要注意两点一是测试数据隔离。多个进程同时操作同一个账号很容易互相改数据产生随机失败。要保证每个进程或每个用例用的是不同数据。二是服务端负载能力。并发数太高被测系统可能触发风控或限流。如果发现并行后失败率上升先把并发数降下来再排查。7.3 报告与视频CI里最重要的不是跑多快而是跑挂了能快速定位。我一般会生成统一报告路径PLAYWRIGHT_HTML_REPORThtml-report pytest报告会在html-report目录下生成。如果并行执行HTML报告还会自动合并多个进程的结果。配合视频录制排查体验更好context browser.new_context(record_video_dirvideos/)每个用例结束把视频和HTML报告一起作为CI产物上传。失败时直接下载录像和报告打开一看几步内就能还原现场。8. 维护建议与最佳实践8.1 页面对象模型脚本越写越多最怕的就是选择器满天飞。页面对象模型可以很好地把元素定位和业务操作收拢到类里。比如登录页可以做成class LoginPage: def __init__(self, page): self.page page def goto(self): self.page.goto(http://127.0.0.1:8080/login) def login(self, username, password): self.page.get_by_label(用户名).fill(username) self.page.get_by_label(密码).fill(password) self.page.get_by_role(button, name登录).click()测试用例里就不用关心登录页具体怎么操作直接调用LoginPage(page).login(user, pass)。页面结构发生变化时只需要改类内部不用改几十条用例。这是自动化工程里很基础但很重要的设计。8.2 减少不必要的等待脚本稳定之后下一步就是让它快。很多拖慢执行速度的元凶是多余的等待。等待接口返回而不是等固定时间with page.expect_response(**/api/order/list) as response_info: page.get_by_role(button, name查询).click() response response_info.value assert response.status 200等待元素状态变化而不是睡两秒expect(page.locator(.loading-mask)).to_be_hidden()这些写法都比“先睡几秒再说”要精确得多执行时间也能压缩不少。8.3 资源清理与脚本健壮性资源清理是容易被忽视但很重要的点。不关浏览器CI机器上会积累大量僵尸进程最后把磁盘和内存撑爆。我通常把关闭逻辑放在finally里保证任何情况都会关闭。另外可以给启动参数加一些保护选项browser p.chromium.launch( headlessFalse, args[--disable-gpu, --no-sandbox] )实践里还有一个习惯base_url统一配置用例里只写相对路径。context browser.new_context(base_urlhttp://127.0.0.1:8080) page.goto(/login)这样环境切换时只用改一个配置不需要全局替换URL。8.4 关于测试数据的建议测试数据管理是自动化项目里能不能稳定跑的胜负手。依赖线上数据是最危险的做法线上数据一定会变你的断言也就跟着挂。尽量使用独立的测试环境并在环境里预置一套稳定的数据。还要注意验证码问题。测试环境如果开着验证码自动化脚本很难跑通。这个需要跟开发沟通在测试环境关掉或者留一个万能验证码。如果这个环节不解决后面所有努力都会被卡死。写到这里PlayWright的核心用法基本覆盖全了。最后再分享一个我的个人习惯每个新项目开始自动化改造时我不会直接铺开写几百条用例而是先挑一条最小业务流程从录制到断言完整跑通然后开Trace盯一遍确认没有隐藏的超时或脏选择器再逐层往上加。这样看起来慢但后期维护成本会少很多。希望你也能用它把手上的重复劳动一点一点省下来。
阅读完成 · 觉得有帮助?
咨询建站