去年,我们团队面临一个艰难抉择:继续维护已经使用了五年的Selenium测试套件,还是迁移到当时刚崭露头角的Playwright。我们的测试套件包含了近2000个端到端测试,每天运行在多个浏览器上,但维护成本越来越高。最终我们决定迁移,而这个过程让我深刻理解了两种工具的差异。
为什么考虑迁移?
先说个真实经历。我们有一个测试,在Chrome 89上运行良好,但Chrome 90发布后突然开始随机失败。排查了两天,发现是Selenium的点击行为与浏览器更新后的默认行为有细微差异。这种“浏览器更新导致测试失效”的情况,在Selenium时代我们每个月都会遇到几次。
核心差异一览
架构设计的代际差距
Selenium基于WebDriver协议,需要为每个浏览器安装对应的驱动。这就像你需要为每个品牌的电视准备不同的遥控器。而Playwright直接通过DevTools协议与浏览器通信,更像是内置了万能遥控器。
# Selenium的典型启动代码 from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service('/path/to/chromedriver') driver = webdriver.Chrome(service=service) # 还需要处理版本兼容性问题 # Playwright则简单得多 from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() # 无需管理驱动版本
等待机制的彻底革新
这是迁移中最需要适应的一点。Selenium的显式等待经常让我们写出这样的代码:
# Selenium风格的等待 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "dynamic-element")) ) # 还有隐式等待、强制等待……容易混乱
Playwright采用了更智能的自动等待:
# Playwright自动等待元素可操作 page.click("#submit-button") # 自动等待直到元素可点击 page.fill("#username", "test") # 自动等待输入框可编辑 # 需要自定义等待时也更直观 page.wait_for_selector(".success-message", state="visible", timeout=10000)
迁移的四个阶段
第一阶段:并行运行(1-2周)
不要直接替换,而是先让两者共存。我们在CI流水线中同时运行两套测试:
# 你的CI配置文件可能类似这样 test_suite: parallel: -name:"Selenium Legacy Tests" command:"pytest selenium_tests/" -name:"Playwright New Tests" command:"pytest playwright_tests/" -name:"Comparison Tests" command:"python compare_results.py"# 对比关键路径测试结果
这个阶段的目标是建立信心。我们挑选了10个核心业务流程测试,用Playwright重写,然后对比两者的执行结果和稳定性。
第二阶段:选择器迁移策略
选择器是迁移中最耗时的部分。我们的经验是:
优先迁移CSS选择器
# Selenium的选择器方式多样,不统一 driver.find_element(By.ID, "login-btn") driver.find_element(By.CSS_SELECTOR, ".login-button") driver.find_element(By.XPATH, "//button[contains(@class, 'login')]") # Playwright推荐使用更稳定的定位方式 page.locator("button:has-text('登录')") # 文本定位 page.locator("[data-testid='login-submit']") # 测试专用属性 page.locator(".login-form >> button.primary") # 链式选择
创建选择器映射表,这是我们的实际做法:
SELECTOR_MAPPING = { "login_button": { "selenium": ("id", "loginBtn"), "playwright": "button:has-text('登录')", "fallback": "[data-test-id='login-button']" }, # ... 其他元素映射 } def get_locator(page, element_name): """统一的元素定位方法""" mapping = SELECTOR_MAPPING[element_name] return page.locator(mapping["playwright"])
第三阶段:处理框架差异
页面对象模式的重构
Selenium的页面对象通常这样写:
# Selenium风格的Page Object class LoginPage: def __init__(self, driver): self.driver = driver self.username = (By.ID, "username") self.password = (By.ID, "password") def login(self, user, pwd): self.driver.find_element(*self.username).send_keys(user) self.driver.find_element(*self.password).send_keys(pwd) # 还需要处理各种等待和异常
Playwright的页面对象更简洁:
# Playwright风格的页面对象 class LoginPage: def __init__(self, page): self.page = page self.username = page.locator("#username") self.password = page.locator("#password") self.submit = page.locator("button[type='submit']") async def login(self, user, pwd): await self.username.fill(user) await self.password.fill(pwd) await self.submit.click() # Playwright会自动等待导航完成
截图和录像的改进
Playwright的截图能力强大得多:
# 不仅仅是全屏截图 await page.screenshot(path="screenshot.png", full_page=True) await element.screenshot(path="element.png") # 元素级别截图 await page.screenshot(path="mobile.png", viewport={"width": 375, "height": 667}) # 视频录制(Selenium很难实现) context = await browser.new_context(record_video_dir="videos/")
第四阶段:分批替换和优化
我们采用“新测试用Playwright,旧测试逐步迁移”的策略:
- 按优先级迁移:先迁移最不稳定、维护成本最高的测试
- 保持接口兼容:创建适配层,减少迁移影响
- 性能对比:监控迁移前后的执行时间和稳定性
迁移中的常见坑
1. 异步处理的思维转换
这是最大的思维转变。Selenium主要是同步的,而Playwright默认异步:
# 错误:混合使用同步和异步 def test_login(): page.click("#button") # 错误!需要await # 正确:统一异步风格 async def test_login(page): await page.click("#button") # 或者使用同步API def test_login_sync(page): page.click("#button") # 同步API
2. 网络拦截的差异
Playwright的网络拦截更强大,但用法不同:
# Selenium处理网络请求很有限 # 通常需要配合其他工具 # Playwright可以直接拦截和修改 await page.route("**/api/login", lambda route: route.fulfill( status=200, body=json.dumps({"success": True}) )) # 或者监听所有请求 page.on("request", lambda request: print(f">> {request.method} {request.url}"))
3. 多浏览器测试的简化
# Selenium需要为每个浏览器配置不同驱动 # Playwright一行代码切换浏览器 @pytest.mark.parametrize("browser_type", ["chromium", "firefox", "webkit"]) def test_multi_browser(browser_type): with sync_playwright() as p: browser = getattr(p, browser_type).launch() # 相同代码在不同浏览器运行
迁移后的收益
迁移完成三个月后,我们的数据对比:
- 执行时间:从平均45分钟减少到18分钟
- 稳定性:随机失败率从12%降低到2%以下
- 维护成本:每周修复测试的时间从15小时减少到3小时
- 代码量:测试代码减少约40%
你应该迁移吗?
根据我们的经验,以下情况建议迁移:
✅ 你的测试套件超过100个用例
✅ 需要测试现代Web功能(PWA、WebSocket等)
✅ 对执行速度和稳定性有更高要求
✅ 团队愿意学习新的测试模式
以下情况可以暂缓:
⏸️ 项目即将结束维护
⏸️ 团队对Selenium非常熟悉且当前稳定
⏸️ 主要测试遗留系统,Playwright支持有限
开始你的迁移
如果决定迁移,我的建议是:
- 先从一个小的、独立的模块开始
- 建立对比基准,确保功能对等
- 培训团队成员,特别是异步编程概念
- 逐步推进,不要试图一次性完成
迁移的过程就像给行驶中的汽车换轮胎——需要谨慎,但一旦完成,你会感受到显著的性能提升。我们从Selenium到Playwright的迁移花了四个月,虽然过程中有些阵痛,但回头看,这是去年我们做的最正确的技术决策之一。
记住,工具只是手段,保证软件质量才是目的。选择最适合你当前和未来需求的工具,然后优雅地完成过渡。