行业资讯
📅 2026/8/4 16:48:50
Playwright自动化测试入门:从环境搭建到实战项目详解
1. 项目概述为什么是Playwright如果你正在寻找一个能帮你搞定网页自动化测试、数据抓取或者RPA机器人流程自动化的工具并且已经被Selenium的复杂环境配置、Puppeteer的单一浏览器支持或者Cypress的独特架构搞得有点头疼那么Playwright的出现很可能就是你的“柳暗花明又一村”。我最早接触它是在一个需要同时兼容Chrome、Firefox和Safari的跨浏览器测试项目里当时团队还在为不同浏览器的驱动和API差异而扯皮Playwright的“一站式”解决方案直接让我们把环境搭建时间从几天缩短到了几小时。简单来说Playwright是一个由微软开源的现代化浏览器自动化库。它最核心的吸引力在于其“多浏览器、单API”的设计理念。这意味着你只需要学习一套API就能控制ChromiumChrome/Edge、Firefox和WebKitSafari三大浏览器引擎。这不仅仅是“支持”那么简单Playwright团队为每个浏览器都维护了一个专门的“浏览器版本”确保API行为高度一致从根本上避免了“在Chrome上跑得好好的一到Firefox就报错”的经典难题。除了跨浏览器Playwright在易用性和功能强大性上找到了一个很好的平衡点。它内置了自动等待机制你不再需要写一堆time.sleep或者复杂的显式等待条件来应对页面加载它提供了强大的选择器引擎支持文本定位、CSS、XPath甚至React/Vue组件定位它还能轻松模拟移动设备、拦截网络请求、处理文件上传下载、录制操作视频……这些特性让它远远超出了传统“测试工具”的范畴成为了一个通用的浏览器自动化利器。所以这篇教程适合谁如果你是测试工程师想找一个更稳定、更强大的UI自动化框架如果你是开发或运维需要写脚本自动处理一些网页操作比如每日报表下载、系统状态巡检甚至如果你是数据分析师厌倦了手动点点点来收集数据那么跟着这篇教程走一遍你都能快速上手Playwright把它变成你手中的瑞士军刀。2. 环境搭建与核心概念解析2.1 一站式环境安装指南Playwright支持多种语言绑定包括JavaScript/TypeScriptNode.js、Python、Java和.NET。对于初学者我强烈推荐从Python或Node.js开始因为它们的生态活跃社区资源丰富且Playwright对它们的支持最为成熟。这里我以Python环境为例进行详细拆解Node.js的步骤逻辑类似。首先你需要一个Python环境建议3.7及以上。打开你的终端Windows用CMD或PowerShellMac/Linux用Terminal我们开始操作。第一步安装Playwright库使用pip命令安装Playwright的核心Python包。这里有个小技巧为了确保依赖清晰我习惯先创建一个虚拟环境但为了教程简洁我们直接进行全局安装如果你项目多还是建议用venv。pip install playwright这条命令会安装playwright这个Python库它提供了我们编写脚本时调用的所有API。第二步安装浏览器二进制文件Playwright的强大之处在于它自带浏览器无需你手动管理ChromeDriver或GeckoDriver。安装完库后需要安装它需要的浏览器内核。playwright install这条命令会下载Chromium、Firefox和WebKit的最新稳定版本到本地缓存中。这个过程可能会花费几分钟取决于你的网络。如果你想只安装特定的浏览器可以使用playwright install chromiumplaywright install firefoxplaywright install webkit注意playwright install命令下载的浏览器是Playwright专门优化过的版本与你自己在电脑上安装的Chrome或Edge是独立的。这样做保证了测试环境的纯净和一致性。如果你想使用系统已安装的浏览器不推荐用于自动化因为版本可能不一致需要更复杂的配置我们后续在高级篇会提到。第三步验证安装创建一个简单的Python脚本test_install.py来验证一切是否就绪。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 可以选择 ‘chromium‘, ‘firefox‘, ‘webkit‘ 中的任意一个 browser await p.chromium.launch(headlessFalse) # headlessFalse 表示打开可视化浏览器 page await browser.new_page() await page.goto(https://www.example.com) print(await page.title()) # 应该打印出 “Example Domain” await asyncio.sleep(2) # 等待2秒让你能看到浏览器窗口 await browser.close() asyncio.run(main())运行这个脚本python test_install.py如果看到一个浏览器窗口打开并访问了example.com同时终端打印出了“Example Domain”那么恭喜你Playwright环境已经成功搭建2.2 核心对象模型Browser, Context, Page理解Playwright的三个核心对象——Browser、Context和Page——是写出高效、稳定自动化脚本的关键。你可以把它们想象成一个层层递进的关系Browser浏览器对应一个浏览器进程实例。通过playwright.chromium.launch()这样的方法创建。一个Browser对象可以创建多个Context。类比就像你电脑上启动了一个Chrome程序。Context上下文相当于一个独立的浏览器会话Session。每个Context拥有独立的缓存、Cookie、本地存储等相互隔离。这在测试中非常有用比如你可以一个Context登录用户A另一个Context登录用户B互不干扰。类比Chrome浏览器中的“无痕模式”窗口或者不同的用户配置文件。Page页面对应一个浏览器标签页。我们绝大部分的操作如点击、输入、获取内容都是在Page对象上完成的。一个Context可以拥有多个Page。类比无痕窗口里的一个具体网页标签。一个典型的创建流程代码如下import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 1. 启动浏览器进程 browser await p.chromium.launch(headlessTrue) # 无头模式不显示UI # 2. 创建一个独立的上下文会话 context await browser.new_context() # 3. 在上下文中打开一个新页面标签页 page await context.new_page() # 现在你可以用 page 对象进行各种操作了 await page.goto(https://www.bing.com) # ... 你的自动化逻辑 # 4. 关闭资源顺序先关页面再关上下文最后关浏览器 await page.close() await context.close() await browser.close() asyncio.run(main())使用async with语句管理browser和context的生命周期是更优雅的方式它能确保即使发生异常资源也会被正确关闭。3. 元素定位与交互从入门到精通自动化脚本的核心就是“找到元素然后操作它”。Playwright提供了极其丰富和灵活的元素定位方式远超传统的ID、Class定位。3.1 八大定位器详解与实践Playwright的定位器Locator是核心抽象它代表一个随时可以用于操作点击、填充等的元素。创建定位器不会立即触发网络请求只有在执行操作如.click()时才会去实际查找元素这种“懒加载”机制提升了性能。1. 按文本内容定位最人性化的方式这是Playwright的一大亮点可以直接通过页面上看到的文本来定位元素。# 点击文本为“登录”的按钮 await page.locator(text登录).click() # 点击包含“提交”文本的元素模糊匹配 await page.locator(text提交).click() # 更精确的全文匹配 await page.locator(text确 定).click() # 注意引号内的空格也要完全匹配实操心得文本定位在测试表单、按钮时非常直观但要注意页面上文本的唯一性。如果“登录”按钮有多个它会默认匹配第一个。可以通过与其他定位器组合来缩小范围。2. CSS选择器定位Web开发者的首选与你在写网页样式时用的CSS选择器完全一致功能强大。# 通过ID await page.locator(#username).fill(myuser) # 通过Class await page.locator(.submit-btn).click() # 通过属性 await page.locator(input[typeemail]).fill(testexample.com) # 组合定位 await page.locator(div.header nav a.home).click()3. XPath定位复杂场景的利器当元素没有好的CSS特征时XPath可以通过层级关系精准定位。# 绝对路径脆弱不推荐 # await page.locator(/html/body/div[1]/form/input[1]).click() # 相对路径结合属性推荐 await page.locator(//button[idsubmit and typebutton]).click() # 使用文本内容 await page.locator(//a[contains(text(), 下一页)]).click()注意事项XPath虽然强大但表达式往往较长且依赖于DOM结构一旦页面结构微调就容易失效。应作为CSS和文本定位的补充而非首选。4. 按Role定位可访问性测试福音根据ARIA角色定位这对于测试组件库如Material-UI, Ant Design或确保网站可访问性非常有用。await page.locator(rolebutton[name搜索]).click() await page.locator(roletextbox[name用户名]).fill(admin)支持的角色包括button,checkbox,heading,link,textbox等。5. 按Label文字定位专门用于定位与表单标签label关联的输入框。# 找到label文字为“邮箱地址”的input await page.locator(label:has-text(邮箱地址)).locator(..).locator(input).fill(email) # Playwright 有更简洁的 API await page.get_by_label(邮箱地址).fill(email) # 更推荐6. 占位文本定位定位带有特定placeholder属性的输入框。await page.get_by_placeholder(请输入手机号).fill(13800138000)7. Title属性定位定位带有特定title属性的元素。await page.get_by_title(工具提示内容).click()8. Alt文本定位定位图片的alt属性。await page.get_by_alt_text(公司Logo).click()组合使用与最佳实践在实际项目中很少单独使用一种定位方式。组合使用能提高定位的精确性和健壮性。# 组合1CSS 文本 # 在某个特定的容器内找“保存”按钮 await page.locator(.modal-footer).locator(text保存).click() # 组合2使用 进行链式定位从父元素到子元素 await page.locator(nav.menu text产品介绍).click() # 最佳实践优先使用 get_by_* API # Playwright 推荐使用语义化的 get_by_* 系列方法它们更易读且稳定 await page.get_by_role(button, name提交).click() await page.get_by_label(密码).fill(secret) await page.get_by_placeholder(搜索...).fill(Playwright)get_by_*系列方法是Playwright更现代的API它们内部会智能地组合多种定位策略通常比直接写选择器字符串更可靠。3.2 元素操作与等待策略定位到元素后我们就可以与之交互了。Playwright的所有操作如click,fill,check都内置了自动等待它会等待元素被附加到DOM中。可见非隐藏非display:none非visibility:hidden。可交互如未被其他元素遮挡未禁用。稳定例如停止动画。这意味着在大多数情况下你不需要再写time.sleep或复杂的WebDriverWait。基础操作示例# 点击 await page.locator(#submit-btn).click() # 双击 await page.locator(#item).dblclick() # 悬停 await page.locator(.menu-item).hover() # 输入文本会先清空原有内容 await page.locator(#search-input).fill(关键字) # 模拟按键 await page.locator(#input).press(Enter) # 选择下拉框选项 await page.locator(select#country).select_option(label中国) # 按显示文本 await page.locator(select#country).select_option(valuecn) # 按value值 # 勾选/取消勾选复选框 await page.locator(#agree-terms).check() await page.locator(#newsletter).uncheck() # 上传文件 await page.locator(input[typefile]).set_input_files([path/to/file1.pdf, path/to/file2.jpg]) # 下载文件需要监听下载事件 async with page.expect_download() as download_info: await page.locator(a#download-link).click() download await download_info.value # 保存文件到指定路径 await download.save_as(/path/to/save/ download.suggested_filename)高级交互与等待有时自动等待不够你需要更精细的控制。# 1. 强制等待尽量避免是最后的手段 await page.wait_for_timeout(3000) # 等待3秒 # 2. 等待元素出现/可见/隐藏 await page.locator(.loading-spinner).wait_for(statehidden) # 等待加载动画消失 await page.locator(#success-message).wait_for(statevisible) # 等待成功提示出现 # 3. 等待导航完成在点击可能引发页面跳转的链接时非常有用 async with page.expect_navigation(): await page.locator(a#next-page).click() # 或者使用 wait_for_load_state await page.locator(a#next-page).click() await page.wait_for_load_state(networkidle) # 等待网络基本空闲 # 4. 等待特定条件成立 await page.wait_for_function(document.querySelector(.result).innerText.includes(完成))踩坑记录早期我习惯在每次操作后加time.sleep(2)结果脚本又慢又不可靠。切换到Playwright后我几乎不再使用强制等待。关键在于理解页面状态用wait_for、expect_navigation或等待元素状态来代替sleep。只有当页面有无法通过事件判断的固定延迟如一个第三方动画时才考虑用wait_for_timeout并加上注释说明原因。4. 高级特性与实战技巧掌握了基本操作Playwright的高级功能能让你应对更复杂的场景脚本也更健壮、高效。4.1 模拟真实设备与网络环境设备模拟Playwright可以轻松模拟手机、平板等设备的视口、User-Agent、触摸等特性。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 使用预定义的设备描述符 iphone_12 p.devices[iPhone 12] browser await p.chromium.launch(headlessFalse) # 创建上下文时传入设备参数 context await browser.new_context(**iphone_12) page await context.new_page() await page.goto(https://m.example.com) # 此时页面会以iPhone 12的尺寸和UA打开 await asyncio.sleep(3) await browser.close() asyncio.run(main())你可以在p.devices中找到大量预定义的设备如“iPhone 13”,“Pixel 5”,“iPad Pro 11”等。网络模拟可以模拟慢速网络或离线状态测试应用在弱网下的表现。# 获取上下文对象 context await browser.new_context() # 设置网络为“慢3G”模式 await context.set_offline(False) # 确保在线 # 更精细的控制需要用到 route 和 continue这里展示简单模拟 # 实际上更常用的是通过 page.route 来拦截和修改请求模拟各种网络场景4.2 拦截与修改网络请求这是Playwright最强大的功能之一可以用于屏蔽不必要的资源如图片、样式表以加速测试。模拟API响应进行前后端分离测试或故障注入。监听和分析网络流量。async def handle_route(route): # 获取请求对象 request route.request # 如果是图片请求直接中止节省带宽和时间 if request.resource_type image: await route.abort() # 如果是特定的API请求可以伪造响应 elif api/user/profile in request.url: await route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({name: Mock User, age: 30}) ) else: # 其他请求正常继续 await route.continue_() # 在页面上启用请求拦截 await page.route(**/*, handle_route) # “**/*” 匹配所有请求 await page.goto(https://your-app.com)实操心得在自动化测试中我经常用这个功能来abort图片和字体请求测试脚本的运行速度能提升30%以上。在做端到端测试时用它来fulfill满足后端尚未完成的API接口让前端测试可以提前进行。4.3 处理弹窗、框架与多页面弹窗Dialog处理alert,confirm,prompt。# 监听弹窗事件并在弹出前决定行为 page.on(dialog, lambda dialog: dialog.accept()) # 总是接受点击确定 # 或者更精确的控制 page.on(dialog, lambda dialog: dialog.accept(输入的文字) if dialog.type prompt else dialog.accept()) await page.locator(button#trigger-alert).click() # 触发弹窗框架iframe需要先定位到框架对象再在框架内操作。# 通过名称或URL定位框架 frame page.frame(namelogin-frame) # 通过name属性 # 或 frame page.frame(urlhttps://third-party.com/login) # 在框架内操作 await frame.locator(#username).fill(user) await frame.locator(#password).fill(pass) await frame.locator(button).click()多页面多标签页# 监听新页面打开事件例如点击了 target_blank 的链接 async with page.context.expect_page() as new_page_info: await page.locator(a[target_blank]).click() new_page await new_page_info.value # 现在可以在新标签页上操作了 await new_page.bring_to_front() # 切换到该页 title await new_page.title() # ... 操作完成后记得关闭 await new_page.close()4.4 录制与调试生成代码与问题排查代码录制器Playwright自带一个强大的GUI工具——Playwright Inspector可以录制你的操作并生成代码。设置环境变量打开录制模式# Windows (PowerShell) $env:PWDEBUG1 # Linux/Mac export PWDEBUG1运行你的脚本。此时浏览器会以“有头”模式打开并弹出一个Inspector窗口。在Inspector中点击“Record”按钮然后在浏览器中的操作会被实时转换成代码显示在Inspector中。你可以暂停、复制这些代码。技巧对于完全陌生的网站先用录制功能快速生成操作骨架然后再进行代码优化和封装效率极高。调试技巧慢动作模式在launch时设置slow_mo参数单位毫秒让所有操作以慢速执行方便观察。browser await p.chromium.launch(headlessFalse, slow_mo500) # 每个操作间隔500ms截图与录屏在脚本关键点或失败时保存证据。await page.screenshot(pathscreenshot.png, full_pageTrue) # 全屏截图 await page.video.save_as(video.webm) # 保存录屏需要在context创建时启用录屏控制台输出执行页面中的JavaScript并获取结果。dimensions await page.evaluate(() { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight } }) print(dimensions)5. 项目实战构建一个健壮的自动化测试脚本让我们综合运用以上知识编写一个模拟用户登录某网站并检查登录后首页元素是否正常的完整脚本。我们将注重健壮性和可维护性。import asyncio import pytest # 这里引入pytest展示如何与测试框架结合 from playwright.async_api import async_playwright, expect # 注意引入了 expect 断言库 class TestWebsiteLogin: 网站登录功能测试类 pytest.fixture(scopeclass) async def browser(self): 启动浏览器作为类级别的fixture async with async_playwright() as p: # 启动浏览器建议测试时使用 headlessTrue 以提高速度 # 调试时可设为 False browser await p.chromium.launch(headlessTrue, args[--disable-blink-featuresAutomationControlled]) # 隐藏自动化特征 yield browser await browser.close() pytest.fixture async def context(self, browser): 为每个测试用例创建独立的上下文隔离Cookie和缓存 context await browser.new_context( viewport{width: 1920, height: 1080}, ignore_https_errorsTrue # 忽略HTTPS证书错误测试环境可能用自签名证书 ) # 可以在这里设置全局超时 context.set_default_timeout(30000) # 30秒 yield context await context.close() pytest.fixture async def page(self, context): 为每个测试用例创建新页面 page await context.new_page() yield page await page.close() pytest.mark.asyncio async def test_successful_login_and_homepage_validation(self, page): 测试用例成功登录并验证首页关键元素 # 1. 导航到登录页 await page.goto(https://demo.testfire.net/login.jsp) # 2. 使用更健壮的定位方式填写表单 # 使用 get_by_label 定位即使前端ID/Class变了也能工作 await page.get_by_label(Username).fill(admin) await page.get_by_label(Password).fill(admin) # 3. 点击登录按钮并等待导航完成登录通常会跳转 async with page.expect_navigation(): await page.get_by_role(button, nameLogin).click() # 4. 断言登录成功验证跳转后的URL或页面元素 # 方法1验证URL包含特定路径 assert /bank/main.jsp in page.url # 方法2使用Playwright内置的断言库更推荐有更好的错误信息 await expect(page).to_have_url(https://demo.testfire.net/bank/main.jsp) # 5. 验证首页关键元素存在且可见 # 检查欢迎文本 await expect(page.locator(textWelcome to Altoro Mutual)).to_be_visible() # 检查账户概览区域 await expect(page.get_by_role(heading, nameAccount Overview)).to_be_visible() # 检查主导航菜单 menu_items [Account History, Transfer Funds, Contact Us] for item in menu_items: await expect(page.get_by_role(link, nameitem)).to_be_visible() # 6. 可选执行一些登录后的操作如查看账户摘要 await page.get_by_role(link, nameAccount History).click() await expect(page.locator(h1)).to_contain_text(Account History) # 可以在这里添加更多页面状态的断言 print(登录及首页验证测试通过) pytest.mark.asyncio async def test_login_with_invalid_credentials(self, page): 测试用例使用错误凭证登录应显示错误信息 await page.goto(https://demo.testfire.net/login.jsp) await page.get_by_label(Username).fill(wronguser) await page.get_by_label(Password).fill(wrongpass) await page.get_by_role(button, nameLogin).click() # 验证错误提示信息出现 # 注意这里需要根据实际网站的错误提示文本来定位 error_msg_locator page.locator(textLogin Failed) await expect(error_msg_locator).to_be_visible() # 也可以检查错误信息的详细内容 await expect(error_msg_locator).to_contain_text(Invalid username or password) print(无效登录测试通过) # 如果不使用pytest也可以直接运行 async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) context await browser.new_context() page await context.new_page() test TestWebsiteLogin() # 由于是实例方法需要稍作调整这里仅示意 # 实际项目中建议使用pytest组织测试用例 await test.test_successful_login_and_homepage_validation(page) await browser.close() if __name__ __main__: asyncio.run(main())脚本设计要点解析结构清晰使用Pytest框架和fixture来管理浏览器、上下文和页面的生命周期使测试用例更简洁资源管理更安全。定位策略健壮优先使用get_by_role、get_by_label等语义化定位器它们比基于CSS选择器的定位更能抵抗前端UI的变化。智能等待使用expect().to_be_visible()等断言它们内部包含了等待逻辑代替了硬编码的sleep。明确断言不仅检查元素是否存在还检查其内容、属性确保功能正确。错误处理真实的测试脚本还应包含try...except块来捕获异常并配合截图功能在失败时保存现场证据便于排查。6. 常见问题排查与性能优化即使有了强大的工具在实际编写和运行脚本时你依然会遇到各种问题。这里我总结了一些高频问题和优化技巧。6.1 高频问题速查表问题现象可能原因解决方案TimeoutError: Timeout 30000ms exceeded1. 元素定位器找不到匹配项。2. 页面加载太慢或网络阻塞。3. 元素在iframe或shadow DOM内。1. 使用Playwright Inspector检查定位器是否正确。2. 增加默认超时context.set_default_timeout(60000)。3. 使用page.frame(...)定位iframe或locator.shadow_root处理Shadow DOM。Element is not attached to the DOM操作的元素已被从页面移除如页面刷新、动态更新。1. 确保操作前元素稳定存在可使用page.wait_for_selector等待。2. 使用locator.first或重新定位。Target closed尝试操作的页面或浏览器已被关闭。检查代码逻辑确保在page.close()或browser.close()后没有再进行操作。使用async with语句管理生命周期。点击/输入无效1. 元素被遮挡弹窗、其他元素。2. 元素是disabled状态。3. 需要先触发其他事件如hover。1. 使用page.screenshot查看当前页面状态。2. 检查元素属性await page.is_enabled(selector)。3. 先执行element.hover()。脚本在无头模式下失败有头模式成功1. 视口大小不同导致布局差异。2. 某些动画或懒加载在无头模式下行为不同。1. 在创建context时固定视口viewport{width: 1920, height: 1080}。2. 尝试添加await page.wait_for_load_state(networkidle)。无法处理文件下载下载触发方式特殊如通过JavaScript。1. 确保正确监听download事件。2. 有些下载需要模拟点击后等待可以尝试page.wait_for_event(download)。被网站检测为自动化脚本网站通过检测navigator.webdriver等属性。1. 启动浏览器时添加参数args[--disable-blink-featuresAutomationControlled]。2. 使用browser.new_context时传入user_agent伪装成普通浏览器。6.2 性能优化与最佳实践复用Browser和Context创建Browser和Context的开销很大。在测试套件中尽量在setup_class或全局fixture中创建一次Browser为每个测试用例创建新的Context和Page。这比每个用例都开闭浏览器快一个数量级。并行执行Playwright原生支持并行测试。结合Pytest的-n参数需要pytest-xdist或Playwright自己的测试运行器可以大幅缩短测试总时间。确保每个并行进程使用独立的User Data Dir或Context。拦截无用请求如前所述使用page.route拦截并中止对图片、字体、样式表如果不需要测试UI甚至某些第三方分析脚本的请求能显著提升页面加载速度和测试执行速度。使用无头模式Headless在CI/CD管道或不需要观察UI的测试中务必使用headlessTrue。这是最简单的性能提升方式。优化定位器避免使用过于复杂或冗长的XPath。优先使用get_by_*系列API。对于需要多次操作的元素将定位器存储在变量中避免重复解析选择器。# 好 submit_btn page.get_by_role(button, name提交) await submit_btn.click() await expect(submit_btn).to_be_enabled() # 不好 await page.get_by_role(button, name提交).click() await expect(page.get_by_role(button, name提交)).to_be_enabled()合理设置超时全局超时set_default_timeout不宜过短如小于10秒以免网络波动导致失败。但对于某些已知很快的操作可以在局部使用更短的超时。清理测试数据对于会修改后端数据的测试如创建订单一定要在测试后或teardown阶段进行清理避免测试数据污染影响后续测试。我个人在大型项目中实践下来通过“复用Browser 并行 拦截请求”这三板斧通常能将一个原本需要30分钟的端到端测试套件缩短到5分钟以内。关键在于理解你的测试场景有针对性地应用这些优化策略而不是生搬硬套。