ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python + Playwright 自动化测试实战:从Selenium迁移到现代框架

Python + Playwright 自动化测试实战:从Selenium迁移到现代框架 做Web自动化测试这些年Selenium一度是我项目里的标配包括很多老团队里也约定俗成“Python Selenium unittest”。直到我把Python生态里的Playwright正式用进项目之后才发现过去很多要写workaround的地方现在一行API就解决了。这篇内容围绕Python和Playwright这套组合从核心模型讲到环境搭建再用完整的pytest用例串一遍登录、断言、截图最后补上iframe、网络拦截、并发跑批这些进阶场景以及我实实在在踩过的一些坑。不管你是准备从Selenium迁移的团队还是刚接触自动化测试、想直接上手现代框架的新手都应该能从这里找到可以直接落地的思路。先说个结论Playwright不是“又一个测试库”它把浏览器自动化里最容易翻车的几个点——驱动管理、等待策略、上下文隔离、调试工具——一次性补齐了。我用了大半年之后最直观的感受是测试脚本的维护成本降了不止一个档次。下面我就按自己项目里的实际推进顺序把这些内容一步步拆开讲。1. 为什么是Playwright它解决了测试里的哪些老问题1.1 从Selenium切过来的真实感受我在之前的项目里维护着一套Selenium脚本几百条用例最痛苦的不是脚本本身写得多烂而是浏览器驱动版本管理。每次Chrome一升级chromedriver没跟上CI就红成一片。Playwright在这方面做得很彻底安装时把对应的Chromium、Firefox和WebKit内核一起拉下来测试环境里浏览器版本和库版本是严格匹配的版本升级由你主动控制不是“被动被浏览器厂商逼着升”。再一个是等待机制。Selenium时代最丑陋的写法就是time.sleep(3)更讲究一点的人会写显式等待但每个元素都要单独封装一段ExpectedCondition代码量上去了不说写的人水平参差不齐最终效果也不稳定。Playwright的定位器自带actionability检查它会一直等到元素可见、可交互、稳定不再抖动再去执行点击或输入。还有多标签页、iframe、弹窗、权限提示、下载文件这些历史痛点。Selenium对这类场景的支持不是没有但代码写起来绕来绕去经常要在不同的window handle之间切来切去。Playwright把“浏览器上下文”这个概念做扎实之后这些场景都变成了普通API调用。当然不是说Playwright是银弹。它只覆盖Chromium、Firefox和WebKit三大内核如果团队还在用IE或者远古浏览器做兼容测试那Playwright帮不了你。另外Java生态里Selenium的社区积累确实更厚很多老项目里复杂组件库的封装经验都是围绕Java的。但就我目前在多个Web后台、数据中台、营销活动页上的使用来看Playwright在稳定性和调试体验上的优势已经完全能覆盖中小团队的日常测试需求。1.2 浏览器上下文与会话隔离很多人第一次看Playwright文档时会被Browser、BrowserContext、Page三层概念绕晕。我用一个办公室类比来解释Browser就是整栋办公楼BrowserContext是办公楼里的一个个独立隔间Page则是隔间里的那张办公桌。每个隔间的物品互不干扰你在隔间A里放着登录态的Cookie隔间B完全不知道。这个隔离机制对测试的价值太大了。最典型的场景是测试不同权限角色管理员、运营、普通用户。用Selenium的时候要么反复登出再登录要么给不同角色准备不同浏览器实例资源消耗极高。用Playwright只需要在同一个浏览器进程下开多个contextwith sync_playwright() as p: browser p.chromium.launch() admin_ctx browser.new_context() guest_ctx browser.new_context() admin_page admin_ctx.new_page() guest_page guest_ctx.new_page()两个context里的localStorage、Cookie、缓存完全隔离等于两套独立的“登录状态”。跑并行用例时每个用例拿一个全新context互相不污染这是Playwright能稳定跑并发的根基。1.3 自动等待机制不用再手动塞sleep自动等待是Playwright最值得反复强调的设计。它的定位器在执行操作前会做一系列actionability检查包括元素是否可见、是否已启用、是否稳定、是否能接收事件。如果条件不满足Playwright会在默认30秒内持续重试直到超时。这和Selenium默认“立即执行、找不到就抛异常”的思路完全不同。意味着很多过去需要手写显式等待的场景现在直接变成正常代码写就能稳定通过。不过有一点我要提醒自动等待不等于所有场景都能无脑过。比如SPA页面里某个区块的骨架屏先渲染出来、数据后加载的情况定位器可能一开始就找到了这个元素但它内容还没填充完直接取值就会拿到空值。这时候还是需要显式等待一个关键数据状态我后面在“动态页面等待策略”一节里会专门展开。2. 环境搭建与项目初始化这套组合拳能少踩一半坑2.1 Python环境与虚拟环境准备Playwright官方支持Python 3.8以上版本但到了2025年我建议直接用3.10或3.12。3.8已经进入维护尾声很多新库的wheel不再提供遇到编译报错的概率会变大。Windows用户直接去官网下载安装包macOS用户可以用Homebrewbrew install python3.12Linux如果是Ubuntu/Debian系用apt安装后记得确认python3和pip3都正常sudo apt update sudo apt install python3 python3-pip -y python3 --version pip3 --version接着一定要建虚拟环境。这一步省掉的话后面pip依赖冲突会把时间浪费到怀疑人生。特别是多个项目同时存在时一个项目要求Playwright最新版另一个项目被钉在1.40没有虚拟环境就只能上演“装了这个坏了那个”的戏码。python3 -m venv .venv source .venv/bin/activateWindows下激活命令是.venv\Scripts\activate激活后命令行前会出现(.venv)前缀。确认在虚拟环境里再装任何依赖。2.2 安装Playwright并下载浏览器内核环境准备好之后安装Playwright本身非常简单pip install playwright但装完库还不够还要下载浏览器内核。这一步经常有人漏掉然后运行脚本时报“Executable doesn’t exist”。正确做法是playwright install chromium如果想把三个内核都装上playwright install我建议项目初期只装chromium体积小、跑通流程最重要。等需要验证WebKit或者Firefox兼容性时再补装其他内核。Linux服务器上运行还需要额外装系统依赖建议直接playwright install --with-deps这一步会把浏览器运行所需的libnss3、libatk等系统库一并装上省去手动排查缺失依赖的麻烦。如果在国内网络环境下下载超时可以配置镜像源export PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright/ playwright install chromium装完后验证一下是否能正常调起浏览器python -c from playwright.sync_api import sync_playwright; psync_playwright().start(); bp.chromium.launch(); pageb.new_page(); page.goto(https://example.com); print(page.title()); b.close(); p.stop()如果输出Example Domain说明环境已经通了。2.3 编写与调试的推荐组合pytest pytest-playwright我不太建议直接用Playwright自带的裸脚本写测试断言要手写、报告要自己拼测试框架的天然能力都浪费掉了。我的选择是pytest pytest-playwright插件pip install pytest-playwright这个插件会把page、context、browser作为fixture注入到测试函数里你不用自己管理浏览器启动和关闭用例执行完自动清理还能在fixture里做登录复用。新建一个conftest.pyimport pytest from playwright.sync_api import Page pytest.fixture(scopefunction) def logged_in_page(page: Page): page.goto(https://example.com/login) page.get_by_label(用户名).fill(test_user) page.get_by_label(密码).fill(123456) page.get_by_role(button, name登录).click() page.wait_for_url(**/dashboard) return page这里有两个细节值得说一下。第一scopefunction保证每个用例拿到一个全新页面互不影响第二登录完成后用wait_for_url而不是time.sleep(2)这是等待URL跳转的精准方式快而且稳定。调试工具方面VSCode配合Playwright官方插件体验很好可以直接在IDE里运行用例、查看trace。更常用的是命令行录制器playwright codegen https://example.com启动后会弹出一个浏览器窗口和一个代码生成面板你手动操作一遍页面Python代码就自动生成出来了。虽然生成的代码通常有点啰嗦需要整理但用来快速搞清楚某个复杂操作的选择器怎么写效率非常高。3. 基础实操用pytest写第一个登录测试3.1 定位器与选择器策略为什么优先用role和text定位元素是UI自动化里最影响维护成本的部分。很多老脚本里全是//*[idapp]/div[3]/div/div[1]/button这种XPath页面结构一变就挂而且挂得毫无预兆。Playwright提供了更符合用户视角的定位方式我的使用优先级是get_by_roleget_by_labelget_by_textget_by_placeholder CSS/XPath。原因是role和label这类定位方式接近UI语义前端开发改结构时按钮上的文字和控件语义通常不会随便改。比如提交按钮你用page.get_by_role(button, name提交)定位就算这个按钮从div包span改成button包span只要可访问性语义没变测试依然能过。CSS选择器适合定位那些有稳定测试ID的元素我建议前端团队在关键节点加>from playwright.sync_api import Page, expect def test_login_success(page: Page): page.goto(http://localhost:8080/login) page.get_by_label(用户名).fill(tester) page.get_by_label(密码).fill(abc123456) page.get_by_role(button, name登录).click() expect(page).to_have_url(http://localhost:8080/dashboard) expect(page.get_by_text(欢迎回来tester)).to_be_visible()这段代码的核心价值在于没有一处time.sleep所有等待都是自动的。expect自带轮询能力比如to_be_visible会持续检查元素是否出现默认超时5秒内一直重试。如果你担心某些环境下渲染慢可以显式调大超时expect(page.get_by_test_id(loading_toast)).to_be_hidden(timeout15000)再配合登录失败的用例def test_login_wrong_password(page: Page): page.goto(http://localhost:8080/login) page.get_by_label(用户名).fill(tester) page.get_by_label(密码).fill(wrong_password) page.get_by_role(button, name登录).click() expect(page.get_by_text(用户名或密码错误)).to_be_visible()到这里你已经有了一条能跑的端到端用例。用它跑一遍顺利的话会看到用例蓝色通过。跑不顺利的话放心下面还有排查章节。3.3 截图、录屏与失败自动保留现场UI自动化里最绝望的体验是测试挂了你完全不知道页面当时长什么样。截图和录屏是保留现场的第一道防线。全页面截图很简单page.screenshot(pathreports/home.png, full_pageTrue)只截某个区域element page.get_by_test_id(chart-container) element.screenshot(pathreports/chart.png)录屏要借助context的record_video配置context browser.new_context(record_video_dirvideos/, record_video_size{width: 1280, height: 720})更推荐的做法是把失败自动截图挂到pytest的hook里这样不用每个用例手写。下面这段可以放在conftest.pyimport pytest 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: page.screenshot(pathfreports/screenshots/{item.name}.png, full_pageTrue)这样每条失败用例执行完都会自动把当时页面截图存到reports/screenshots目录。再配合GitHub Actions或GitLab CI的artifact上传排查问题的时候不用再远程连服务器重跑直接看附件就能定位个七七八八。4. 进阶场景动态内容、iframe、多页面与网络请求4.1 动态页面等待策略与expect轮询现代前端基本全是SPA页面渲染分成好几个阶段HTML加载、JS执行、接口返回、数据绑定。很多测试失败都发生在“页面看起来加载完了但数据还没到”的阶段。我之前遇到过典型的例子页面上有个统计卡片要等后端聚合接口返回后才显示数字。直接断言数字“等于10”第一次跑可能页面还是0。解法不是time.sleep(5)而是用wait_for_function等待某个关键状态page.wait_for_function( document.querySelector(#statistics-card)?.textContent.includes(10) )如果不想写JavaScript也可以用expect轮询一个最终会出现的特征。比如数据加载完成后表格里出现某条记录表格加载前有一条进度条动画那就等进度条消失expect(page.get_by_test_id(table-loading)).to_be_hidden(timeout15000)判断到底该等什么是动态页面测试里最需要积累经验的点。我的经验是优先等待业务结果比如某个唯一的文本、某个URL参数、某个接口返回的数据而不是等固定的CSS类名或者加载动画消失。加载动画可能出现也可能一闪而过但业务结果大概率是稳定的。4.2 iframe内元素定位与跨域限制后台系统里嵌套iframe的场景很常见比如第三方登录、富文本编辑器、地图组件。Playwright对iframe的支持做了专门的封装用起来比Selenium的switch_to.frame顺手得多。frame page.frame_locator(#payment-iframe) frame.get_by_role(button, name确认支付).click()frame_locator会返回一个FrameLocator它的定位方法作用和Page上的定位器一样只是限定在iframe内部。跨域iframe也没有问题只要页面能正常加载该iframePlaywright就能在其内部定位元素。这里有个容易踩的坑如果iframe是动态创建的比如点击按钮之后才出现直接写frame_locator定位等不到内部元素会抛超时。解决思路和普通元素一样先等待iframe本身出现或者等内部元素可见。expect会帮我们做这个轮询所以我会直接写expect(frame.get_by_placeholder(请输入验证码)).to_be_visible()它会反复尝试直到超时不用你手动处理iframe创建时机。还有一点要提醒不要想着“能不能绕过iframe直接获取里面的文本”。自动化测试的意义是模拟真实用户操作用户在页面上就是在iframe里完成输入的你也应该老老实实在frame_locator里做。强行用JavaScript穿透iframe反而会测不到真实交互。4.3 并发跑批多标签页、多上下文与并行用例测试用例数量上来之后单线程跑会非常浪费时间。Playwright结合pytest-xdist可以轻松做到并行执行pip install pytest-xdist pytest -n 4-n 4表示开4个worker进程每个worker有自己的浏览器实例。并行能跑多快取决于机器CPU、内存以及被测系统能不能扛得住并发请求。我见过一个团队一开始就开8并发结果把测试环境的MySQL连接池打满了最后全项目用例超时。建议从-n 2开始观察资源占用再逐步加。如果需要在同一个浏览器里模拟多个标签页比如“主页面打开订单详情另一个页面打开订单列表互相切换查看”可以这样page1 context.new_page() page2 context.new_page()每个page都是独立的标签页。但要注意同一context下的page共享Cookie如果不同角色不能用同一个context要开新context。并发最怕的还不是浏览器资源而是测试数据互相污染。比如两个用例同时登录同一个测试账号可能导致“该账号已在其他设备登录”被踢下线。我的建议是给每个并发worker分配独立的测试账号或者用例数据都用随机化的方式创建。4.4 拦截与模拟接口把后端不稳定因素从测试中剔除UI自动化跑得稳不稳三分看脚本七分看依赖服务稳定不稳定。接口返回慢、第三方支付回调挂掉、短信验证码服务暂时不可用都会让你的测试用例红得莫名其妙。Playwright的route拦截功能可以帮你把后端这个不稳定因素从测试中隔离出去。模拟正常接口def handle_route(route): route.fulfill( status200, content_typeapplication/json, body{data: [{id: 1, name: mock_data}], success: true} ) page.route(**/api/user/list, handle_route)模拟接口延迟import time def slow_route(route): time.sleep(5) route.fulfill(status200, body{success: true}) page.route(**/api/statistics, slow_route)模拟接口500错误page.route(**/api/order/create, lambda route: route.fulfill(status500, bodyserver error))用这种方式前端异常态、空数据态、接口超时态都能在本地稳定复现而且不依赖测试环境是否造得出这些脏数据。我在项目里就是用route把支付渠道mock掉本地跑单测的时候不用真调第三方速度快而且稳定。5. 常见问题与排查技巧实录5.1 定位不到元素先看等待再看选择器这是每天都会遇到的头号问题。我的排查顺序固定不变。第一步把浏览器切到有头模式加headlessFalse用page.pause()打开Playwright调试工具手动在页面里查看元素是否存在。很多情况下你会发现元素其实在只是速度太快跑过了这时候问题不是选择器而是等待。第二步确认元素是不是在iframe里或者处于shadow DOM内。iframe的问题前面说了用frame_locator。shadow DOM的问题Playwright默认选择器可以穿透开放的shadow DOM但如果是关闭的shadow root浏览器自动化都拿不到只能让前端开发开放它或者用组件测试覆盖。第三步检查选择器本身是否唯一。用page.get_by_text(确定)定位页面上可能有两个“确定”按钮导致严格模式报错。可以加.first但不建议用这种掩盖问题的方式更好的做法是结合上下文用更具体的定位器比如page.locator(.modal).get_by_role(button, name确定)。5.2 超时问题的重试策略默认超时是30秒如果你觉得某个操作很慢直接顺着调大该步超时就好。我不建议全局把超时调到60秒因为这意味着定位器失败时你要白白等一分钟才知道结果。重试方面pytest有个pytest-rerunfailures插件pip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1但我一直认为重试是下策不是上策。如果一条用例需要重试才能过说明它有隐藏的不稳定因素。是等待条件不对还是测试数据污染还是被测系统本身有偶发问题应该先把根因挖出来。重试最多用来抗短暂的网络抖动断言失败的用例不应该重试否则问题会被掩盖在绿色报告里。5.3 安装失败或浏览器启动失败的几个原因有个非常典型的报错playwright._impl._errors.Error: Executable doesnt exist at /home/user/.cache/ms-playwright/chromium-XXXX多半是库升级了浏览器内核版本不匹配。重新执行playwright install chromium即可。Linux下如果报缺.so文件用playwright install --with-deps重新装系统依赖。还有一类报错是证书问题常见于公司内网环境访问外网下载浏览器内核时被安全网关拦了。解决办法是先配置镜像源我前面提过再重新install。如果依然不行让运维把下载域名加白名单。我整理了一张速查表覆盖我见过的大部分问题现象可能原因解决方向Executable doesnt exist浏览器内核未安装或版本不匹配重跑playwright install chromiumLinux启动浏览器报缺少libnss3等系统依赖缺失playwright install --with-deps安装内核时下载超时网络访问下载源不稳定配置PLAYWRIGHT_DOWNLOAD_HOST镜像Target page, context or browser has been closed代码里有提前close的操作检查是否手动调用了browser.close()Timeout 30000ms exceeded元素未出现或条件始终不满足调大单步超时并确认等待逻辑正确It looks like you are using Playwright Sync API同步异步API混用统一使用sync_playwright或async_playwright5.4 同步API与异步API的选择Playwright的Python库同时提供sync_playwright和async_playwright两套API。很多新手在同一个文件里混用就会看到上面表格里那个“It looks like you are using Playwright Sync API”的报错。如果是纯测试工程我建议直接用同步API。原因很简单代码写起来平铺直叙不需要纠结event looppytest集成也成熟。异步API的优势在两种场景比较明显一是被测项目本身就是异步爬虫或异步Web服务测试代码和业务代码风格保持一致二是需要同时发起大量网络请求并发操作异步可以用asyncio.gather优雅处理。pytest-playwright插件默认提供同步的pagefixture同时也提供async_page。你选好一种之后整个项目就用默认的fixture风格不要在一个用例里一会儿sync_playwright一会儿async_playwright。混用产生的报错信息非常隐晦排查起来很痛苦。6. 关于项目维护与团队协作的几点体会最后这部分不是技术但我觉得比技术更重要。Playwright再强大如果测试代码写得乱糟糟、没有固定规范项目跑到后面还是会变成一片废墟。先说定位器规范。我在前面强调了>browser p.chromium.launch(headlessTrue)本地调试时可以改成headlessFalse看真实浏览器操作。GitHub Actions这类云环境跑测试时注意缓存Playwright的浏览器内核路径否则每次CI都要重新下载一次白等好几分钟。我自己踩过最大的坑就是把自动化测试的目标定成“跑过去就行”结果每天都在维护失效的定位器和脏数据测试变成负担而不是保障。后来我把原则改成了“先让失败可诊断”截图、录屏、trace全部配好每个用例都能独立运行、稳定复现、快速定位再谈覆盖率。整个项目反而稳定多了。Playwright提供了一套很好的底座但怎么用好它还是要靠项目里一点一滴的经验累积。
返回列表