从 Selenium 迁移到 Playwright:升级你的测试框架实战手册
如果你已经在使用 Selenium 进行 Web 自动化测试,可能会注意到近年来 Playwright 的崛起。这不是简单的替代关系,而是一次测试能力的全面升级。我在去年带领团队完成从 Selenium 到 Playwright 的迁移后,测试执行速度提升了40%,代码维护成本降低了30%。更重要的是,那些曾经令人头疼的等待问题、不稳定性问题,都得到了显著改善。
核心差异:不仅仅是语法变化
在开始迁移前,理解两个框架的本质差异至关重要:
Selenium 像是一个翻译官——它将你的指令翻译成不同浏览器的原生API调用,中间经过WebDriver协议。这个额外的抽象层虽然带来了广泛的浏览器支持,但也增加了复杂性和不稳定性。
Playwright 则像是直接与浏览器对话——它通过DevTools协议直接控制浏览器,支持Chromium、Firefox和WebKit三大引擎,提供了更一致、更可靠的执行环境。
迁移准备:三步走策略
第一步:环境评估与规划
在开始编码迁移之前,花时间评估当前测试状态:
- 统计现有测试用例数量和复杂度
- 识别重度依赖 Selenium 特性的部分
- 规划迁移顺序(建议从简单的测试开始)
第二步:环境搭建
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 卸载旧依赖 pip uninstall selenium # 安装 Playwright pip install playwright pytest-playwright # 安装浏览器 playwright install chromium firefox webkit </pre>
第三步:基础配置迁移
将原来的 Selenium 配置转换为 Playwright 的配置文件:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 原来的 Selenium 配置 # from selenium import webdriver # options = webdriver.ChromeOptions() # options.add_argument('--headless') # driver = webdriver.Chrome(options=options) # Playwright 配置 import asyncio from playwright.async_api import async_playwright asyncdef create_browser(): asyncwith async_playwright() as p: browser = await p.chromium.launch(headless=False) context = await browser.new_context() page = await context.new_page() return browser, page </pre>
核心API迁移对照表
浏览器初始化
Selenium方式:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Chrome() wait = WebDriverWait(driver, 10) </pre>
Playwright方式:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">from playwright.sync_api import Page, expect # 自动等待内置于大多数操作中 page.wait_for_timeout(1000) # 尽量避免使用,利用自动等待 </pre>
元素定位与操作
点击元素:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Selenium element = driver.find_element(By.ID, "submit-btn") element.click() # Playwright (更简洁) page.click("[#submit](javascript:;)-btn") # 或者使用更明确的选择器 page.get_by_role("button", name="提交").click() </pre>
输入文本:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Selenium search_box = driver.find_element(By.NAME, "q") search_box.send_keys("Playwright 教程") # Playwright page.fill("input[name='q']", "Playwright 教程") # 或者 page.get_by_placeholder("搜索...").fill("Playwright 教程") </pre>
等待策略迁移
这是迁移中最需要调整思维的地方:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Selenium 显式等待 element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "dynamic-element")) ) # Playwright 自动等待(推荐) page.wait_for_selector("[#dynamic](javascript:;)-element", state="visible") # 或者更优雅的: expect(page.locator("[#dynamic](javascript:;)-element")).to_be_visible() </pre>
处理弹窗和对话框
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Selenium(需要配置期望) alert = driver.switch_to.alert alert.accept() # Playwright(监听对话框) page.on("dialog", lambda dialog: dialog.accept()) page.click("[#trigger](javascript:;)-alert") </pre>
高级特性迁移指南
1. 页面切换管理
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Playwright 的上下文管理更清晰 async with await browser.new_context() as context: page = await context.new_page() await page.goto("https://example.com") # 打开新标签页 async with await context.new_page() as new_page: await new_page.goto("https://another-site.com") </pre>
2. 网络请求拦截
Playwright 的网络请求控制能力强大得多:
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 拦截特定请求 await page.route("**/api/*", lambda route: route.fulfill( status=200, content_type="application/json", body='{"success": true}' )) # 修改请求头 await page.route("**/*", lambda route: route.continue_(headers={ **route.request.headers, "X-Custom-Header": "value" })) </pre>
3. 文件上传下载
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 文件上传(不再需要复杂的 send_keys) async with page.expect_file_chooser() as fc_info: await page.click("[#upload](javascript:;)-button") file_chooser = await fc_info.value await file_chooser.set_files("myfile.pdf") # 下载文件 async with page.expect_download() as download_info: await page.click("[#download](javascript:;)-button") download = await download_info.value await download.save_as("downloaded_file.pdf") </pre>
迁移常见问题与解决方案
问题1:自定义等待条件怎么办?
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 原来的 Selenium 自定义等待 def element_has_class(locator, class_name): def _predicate(driver): element = driver.find_element(*locator) return class_name in element.get_attribute("class") return _predicate # Playwright 解决方案 async def wait_for_class(page, selector, class_name, timeout=10000): await page.wait_for_function( """([selector, className]) => { const el = document.querySelector(selector); return el && el.classList.contains(className); }""", [selector, class_name], timeout=timeout ) </pre>
问题2:如何处理 Shadow DOM?
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Playwright 原生支持 Shadow DOM shadow_host = page.locator("[#shadow](javascript:;)-host") shadow_root = shadow_host.element_handle().evaluate_handle("el => el.shadowRoot") shadow_element = shadow_root.query_selector(".inner-element") </pre>
问题3:并行测试怎么处理?
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># Playwright 的 BrowserContext 是天然隔离的 import pytest @pytest.fixture(scope="function") async def page(browser): context = await browser.new_context() page = await context.new_page() yield page await context.close() # 可以在不同的上下文中并行执行测试 </pre>
性能优化技巧
- 重用浏览器实例:创建多个上下文而不是多个浏览器
- 合理使用 headless 模式:CI/CD 环境中使用 headless
- 视频录制选择性开启:只在失败的测试中录制视频
-
优化截图策略:只在需要时截图,使用
full_page=True参数控制
<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: transparent; margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;"># 配置示例 browser = await chromium.launch( headless=True, args=['--disable-dev-shm-usage'] ) context = await browser.new_context( viewport={'width': 1920, 'height': 1080}, record_video_dir='videos/' if config.record_video else None ) </pre>
迁移检查清单
- 更新依赖项和配置文件
- 重写浏览器初始化逻辑
- 更新元素定位器(优先使用角色和文本定位器)
- 替换显式等待为 Playwright 的自动等待模式
- 更新文件操作相关代码
- 重写弹窗和对话框处理逻辑
- 更新测试报告集成
- 配置 CI/CD 流水线支持
- 建立性能基准并对比
迁移后的收益
完成迁移后,你将会发现:
- 稳定性显著提升:减少了约60%的 flaky tests
- 执行速度加快:平均测试执行时间减少30-50%
- 代码更简洁:代码行数通常减少40%
- 调试更方便:内置的追踪和截图功能强大
- 跨浏览器测试更可靠:真正的多浏览器支持
开始你的迁移之旅
迁移不是一夜之间完成的。建议采取渐进式迁移策略:先从简单的测试用例开始,积累经验后再处理复杂的测试。建立迁移的节奏——比如每周迁移10%的测试用例,同时保持原有 Selenium 测试的正常运行。
记住,迁移不仅仅是技术替换,更是测试理念的升级。Playwright 提供的不仅是新的 API,更是更现代化、更可靠的测试方法论。开始迁移吧,你会发现自己不仅获得了更好的工具,也成为了更好的自动化测试工程师。
最好的迁移时间是一年前,其次是现在。