ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

双引擎测试机器人:Selenium与Playwright融合架构实战

双引擎测试机器人:Selenium与Playwright融合架构实战

1. 项目背景与设计思路:为什么我坚持双引擎而不是二选一

“网页自动化测试机器人”这个词听起来很高大上,做起来其实就是两件事:让脚本替你打开浏览器、点击、填表单、校验结果,然后把执行情况沉淀成报告。真正动手做的时候,第一个选择就卡住了——到底用 Selenium 还是 Playwright?我自己的实践倾向是:别急着站队,两个一起用。Selenium 陪老项目跑了十几年,WebDriver 标准覆盖面广,兼容性和社区生态目前仍然没人能替代;Playwright 是后起之秀,速度、稳定性、调试体验都在新一代工具里排在最前面。把这俩组合到一套测试体系里,既能兼容老系统,又能高效覆盖新技术栈,这就是本文想展开讲的核心思路。

这篇文章适合正在从零搭测试框架的团队、想优化现有自动化用例执行速度的测试开发,以及所有被“选型困难症”困住的人。接下来不会只讲概念,会直接给出我当时改造某个电商后台旧项目测试体系时用到的分层设计、核心代码和真实踩坑记录。

1.1 Selenium 的老成持重:老系统场景绕不开它

先说为什么不能把 Selenium 扔掉。很多企业内部系统的页面结构还停留在上古阶段:table 布局、嵌套三层 iframe、大量“非标准”的属性选择器、甚至部分老内核浏览器。Selenium 走的是 WebDriver 协议,通过一套标准化的命令去驱动浏览器,各家浏览器厂商都按这套协议实现,因此对老内核、企业定制浏览器的兼容性是实打实的优势。

我接手过一个订单管理后台的回归测试项目,页面里最核心的那张表格,点行里的按钮居然要先切进两层 iframe,而按钮没有任何 id 和 name,只能靠一段很长的 XPath 定位。Selenium 在这种场景下很稳定,几千条断言跑下来没有一次因为定位方式本身翻车。我当时也试过把它整体迁到 Playwright,不是说跑不了,而是调试成本明显偏高:老页面的事件触发机制和新工具的部分自动等待逻辑有冲突,需要额外写很多兜底逻辑。所以结论很清楚——老系统回归,Selenium 仍然是默认选项。

另一个现实问题是团队习惯。很多老测试团队写了好几年 Selenium 脚本,WebDriverWait、expected_conditions 这些东西已经成了肌肉记忆。强行全量切换框架,光培训成本和学习曲线就够喝一壶。保留 Selenium 侧的能力,让老成员继续复用经验,同时逐步引入 Playwright,团队转型的阻力会小很多。

1.2 Playwright 的快与稳:新页面交给它更省心

Playwright 和 Selenium 最大的区别在通信方式。Selenium 每次操作都要通过 WebDriver 协议转发一次命令,相当于隔着电话指挥浏览器干活;Playwright 走浏览器调试协议直接和内核对话,很多操作还可以批量执行,少了好几轮往返开销。再加上 Playwright 内置了 auto-wait,定位到元素后会自动等待它可操作,不需要像 Selenium 那样到处写显式等待,代码量少一截,执行速度也有明显提升。

实际跑一个“搜索→点击→断言”的标准流程,同样的等待条件,Playwright 的脚本通常比 Selenium 短三分之一左右,耗时也能降两到三成。如果页面请求少、交互简单,差距会小一些;一旦遇到大量异步渲染的现代化单页应用,Playwright 的优势会被放大得非常明显。

还有两个很实用的特性:上下文隔离和调试工具。默认每个浏览器上下文都是独立会话,测试用例之间天然隔离,互不干扰;自带的 trace 录制和 codegen 功能,能自动生成脚本原型、回放完整操作轨迹,排查问题的时候简直是救命级别。新项目、现代前端页面、需要高频并行执行的场景,我会默认选它。

1.3 混搭方案容易踩的三个坑

双引擎组合听着很理想,实际落地有几个坑会让你摔得很难看。

第一个坑是抽象层设计过重。有人一上来就把所有通用操作抽象成十几二十个方法,封装了滚动、拖拽、下拉、弹窗等一大堆东西。结果就是维护抽象层的成本比写用例还高,改了底层实现,抽象方法跟着调一晚上。我后来控制住了这个冲动,只抽象业务层真正关心的那七八个操作,比如打开页面、点击、输入、获取文本、等待可见、截图、关闭。

第二个坑是用例代码直接绑死底层 API。如果用例里直接写driver.find_element(...),或者直接调page.locator(...),那这套用例就没法和另一套引擎切换。要切换引擎,就必须保证用例层只依赖统一的抽象接口,这是双引擎方案能成立的命门。

第三个坑是运行配置随意。有人给用例的 fixture 里硬编码了 selenium,或者环境变量写得含糊不清,跑着跑着就不知道当前用例到底走的哪个引擎。我自己的做法是把引擎选择项做成 pytest 的命令行参数,每条用例、每轮执行用什么引擎,一眼就能看出来。

2. 测试机器人整体架构与引擎调度

想清楚“为什么用两个工具”之后,接下来要解决的是“怎么把它们揉进一套体系”。双引擎方案最怕的就是两套代码、两种风格、两批人维护,最后变成维护噩梦。这块是架构设计的关键环节,我的思路是:分层隔离,统一接口,把 Selenium 和 Playwright 当成可插拔的驱动,真正面向用例的只有一个稳定的“浏览器操作接口”。

2.1 分层设计:用例不该知道浏览器是谁

整个测试机器人从下往上分四层。最底层叫基础设施层,负责日志、截图、配置读取、报告输出,这些和页面操作没有直接关系。倒数第二层是驱动适配层,上面提到的 SeleniumDriver 和 PlaywrightDriver 都属于这一层,它们实现同一套 BaseDriver 接口,但内部的差异完全可以藏在方法内部。再往上是业务操作层,封装具体的业务动作,比如用户登录、商品搜索、下单支付,这段代码会被多个用例复用。最顶层是用例层,只按照业务场景组织步骤和断言,完全不感知底层是哪个引擎。

这四层关系对应到目录结构,大概长这样:

auto_robot/ ├── infra/ # 基础设施:日志、配置、报告 │ ├── config.py │ ├── logger.py │ └── reporter.py ├── drivers/ # 驱动适配层 │ ├── base_driver.py │ ├── selenium_driver.py │ └── playwright_driver.py ├── pages/ # 业务操作层:页面对象 │ ├── login_page.py │ ├── order_page.py │ └── dashboard_page.py ├── tests/ # 用例层 │ ├── conftest.py │ ├── test_login.py │ └── test_order_flow.py └── run.py # 汇总执行入口

这种分层的收益很明显。用例层和业务操作层根本不知道 Selenium 或者 Playwright 的存在,它们只知道自己拿到的对象有click、fill、wait_visible这些方法。哪天想换引擎、加新引擎(比如将来的某个新工具),只写一个新的驱动类就行,业务层和用例层不用改一行代码。

2.2 统一引擎调度:一套用例,两套驱动

引擎调度的核心是抽象接口加上工厂方法。BaseDriver 定义了所有驱动子类必须实现的方法:

# drivers/base_driver.py class BaseDriver: def open(self, url: str): raise NotImplementedError def click(self, selector: str): raise NotImplementedError def fill(self, selector: str, text: str): raise NotImplementedError def get_text(self, selector: str) -> str: raise NotImplementedError def wait_visible(self, selector: str, timeout: float): raise NotImplementedError def screenshot(self, path: str): raise NotImplementedError def close(self): raise NotImplementedError

这个接口只暴露业务层高频使用的操作,不追求大而全。接口里的selector统一使用 CSS 选择器,因为 CSS 选择器对两种引擎都是天然支持的。如果有少数老页面必须用 XPath,可以通过xpath=前缀区分,但接口本身不因此增加复杂度。

工厂方法负责依据用户传入的 engine 参数创建对应驱动:

# drivers/engine_factory.py def create_driver(engine: str = "selenium", headless: bool = True) -> BaseDriver: if engine == "selenium": from drivers.selenium_driver import SeleniumDriver return SeleniumDriver(headless=headless) elif engine == "playwright": from drivers.playwright_driver import PlaywrightDriver return PlaywrightDriver(headless=headless) else: raise ValueError(f"Unknown engine: {engine}")

这里故意用局部 import,而不是文件顶部统一引入。原因很实际:如果同时导入两个框架,Selenium 那边的兼容问题和 Playwright 那边的初始化开销会被无条件加载,哪怕你根本不用另一个引擎。局部导入可以做到用谁加载谁,启动效率和出错面都有保障。

2.3 报告与日志:机器人必须可观测

自动化机器人跑起来之后,最怕的不是“报错”,而是报错之后无从下手。我见过太多测试结果是红了一大片,但没有任何截图、没有日志上下文、没有失败现场记录的情况。这种损失远比用例失败本身更痛。所以可观测性是整个架构里优先级很高的环节。

日志方面我按用例编号建目录,每个用例执行期间产生的所有输出落到独立文件里,内容包括操作步骤、耗时、元素选择器、警告信息。截图方面,Selenium 侧用内存截图加上磁盘落盘,Playwright 侧用它的 screenshot API,正常情况下每个关键步骤存一张,失败时强制再来一张现场图。报告方面用 pytest 生态现有的 HTML 报告插件,把 pass、fail、耗时、截图路径汇总到一张页面里,方便后续人工浏览。

为了统一给用例加日志和耗时统计,可以写一个装饰器:

# infra/trace_decorator.py import time import logging logger = logging.getLogger("auto_robot") def case_log(func): def wrapper(*args, **kwargs): start = time.time() try: result = func(*args, **kwargs) logger.info("[PASS] %s 耗时 %.2fs", func.__name__, time.time() - start) return result except Exception as e: logger.error("[FAIL] %s 耗时 %.2fs 错误: %s", func.__name__, time.time() - start, e) raise return wrapper

装饰器本身不复杂,但它能保证每个用例的执行耗时和结果类型都进入日志体系。后期统计机器人每天跑了多少条、平均耗时、失败率,直接用这份日志就能做出初步报表,不需要重新改用例代码。

3. 从零搭建可运行的自动化机器人

架构聊清楚以后,必须落到能跑起来的代码上。这一章我会把最小可运行版本的关键代码完整过一遍,包括环境准备、Selenium 驱动、Playwright 驱动和统一执行入口。照着这段走,基本一两个小时就能把骨架拉起来,剩下的工作是往业务层补页面操作。

3.1 环境准备与项目目录

先装依赖。Python 版本建议在 3.9 以上,虚拟环境提前建好,避免不同项目的包互相污染。

python -m venv .venv source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate pip install selenium playwright pytest pytest-xdist pytest-html pytest-rerunfailures

装完后执行一次playwright install chromium,会把 Playwright 自带的 Chromium 内核浏览器下载到本地,这样 Playwright 侧就不需要额外再准备浏览器驱动。Selenium 侧用的是系统里的浏览器,Selenium 4 以上版本带有自动驱动的机制,能自动匹配下载对应的驱动文件,但后续在断网的内网环境里跑,还是得提前手动准备好驱动文件,这个后面会在问题章节单独讲。

准备一个配置文件infra/config.py,集中管理测试环境地址、默认超时、截图目录位置。这个文件很基础但很重要,避免以后到处散落硬编码的 URL 和超时时间。

# infra/config.py import os BASE_URL = os.getenv("ROBOT_BASE_URL", "http://localhost:8080") SCREENSHOT_DIR = os.getenv("ROBOT_SCREENSHOT_DIR", "./artifacts/screenshots") DEFAULT_TIMEOUT = float(os.getenv("ROBOT_TIMEOUT", "15"))

3.2 Selenium 侧的关键实现

SeleniumDriver 类的重点不在语法,而在于每个方法是否稳定。先看初始化部分:

# drivers/selenium_driver.py from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class SeleniumDriver(BaseDriver): def __init__(self, headless=True): options = webdriver.ChromeOptions() if headless: options.add_argument("--headless=new") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") self.driver = webdriver.Chrome(options=options) self.driver.implicitly_wait(2) def open(self, url): self.driver.get(url)

四个启动参数里有两个很容易被忽略。--no-sandbox是运行在容器环境里时必需的,很多 CI 容器默认没有沙箱能力,不加这个直接报错;--disable-dev-shm-usage是解决容器内存 tmpfs 过小的问题,不加的话页面一多就可能崩。--headless=new则是新版 Chromium 内核推荐的无头模式,旧的--headless参数在部分版本里已经进入废弃流程。

接着是关键的操作方法:

def click(self, selector): element = self.wait_visible(selector) element.click() def fill(self, selector, text): element = self.wait_visible(selector) element.clear() element.send_keys(text) def wait_visible(self, selector, timeout=15): return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located((By.CSS_SELECTOR, selector)) ) def screenshot(self, path): self.driver.save_screenshot(path) def close(self): self.driver.quit()

这里所有定位都走wait_visible而不是直接find_element,是我坚持的一个习惯。直接找元素经常撞上页面还在加载、元素还没渲染出来的时候,WebDriverWait 会把等待逻辑统一收敛。隐式等待我设成了 2 秒,作为底层的兜底;显式等待才是定位的主要手段。两者混用虽然在某些极端场景下会有叠加耗时,但从稳定性优先的角度,这种“双保险”带来的成本完全可以接受。

3.3 Playwright 侧的关键实现

PlaywrightDriver 的结构和 SeleniumDriver 对齐,但内部风格完全不同。Playwright 自带智能等待,代码更短:

# drivers/playwright_driver.py from playwright.sync_api import sync_playwright class PlaywrightDriver(BaseDriver): def __init__(self, headless=True): self._pw = sync_playwright().start() self.browser = self._pw.chromium.launch(headless=headless) self.context = self.browser.new_context() self.page = self.context.new_page() def open(self, url): self.page.goto(url, wait_until="domcontentloaded") def click(self, selector): self.page.locator(selector).click() def fill(self, selector, text): self.page.locator(selector).fill(text) def wait_visible(self, selector, timeout=15): return self.page.locator(selector).first.wait_for( state="visible", timeout=timeout * 1000 ) def screenshot(self, path): self.page.screenshot(path=path) def close(self): self.context.close() self.browser.close() self._pw.stop()

goto方法里的wait_until="domcontentloaded"是我刻意选的。默认值是load,会等所有图片、脚本、样式表都加载完,碰到慢资源要白等好几秒;domcontentloaded只等 DOM 结构就绪,对测试场景足够用,能省不少时间。如果想更激进,甚至可以设置成commit,不等页面完整渲染就开始操作,但稳定性会下降,我一般不推荐默认用它。

context这个对象值得多说一句。它相当于一个独立的浏览器会话,和另一个 context 之间完全不共享 cookie、存储和登录态。这是一笔隐形的并发财富:同一个浏览器进程里可以并行开多个 context 跑不同测试用例,互不干扰。后面并发章节会再用到它。

3.4 统一入口:一键跑完所有用例

有了两个驱动实现后,最后一步是把引擎选择交到命令行。在tests/conftest.py里注册一个--engine参数,再提供一个 fixture 负责创建和回收 driver:

# tests/conftest.py import pytest from drivers.engine_factory import create_driver def pytest_addoption(parser): parser.addoption("--engine", action="store", default="selenium", help="选择执行引擎:selenium 或 playwright") parser.addoption("--headless", action="store_true", default=True) @pytest.fixture(scope="function") def browser(pytestconfig): engine = pytestconfig.getoption("--engine") headless = pytestconfig.getoption("--headless") driver = create_driver(engine, headless=headless) yield driver driver.close()

用例文件的写法就可以做到完全不关心引擎:

# tests/test_login.py from infra.config import BASE_URL def test_login(browser): browser.open(f"{BASE_URL}/login") browser.fill("#username", "tester01") browser.fill("#password", "pass123") browser.click("#login_button") assert browser.wait_visible("#dashboard", timeout=10) is not None

跑起来也很简单:

# 用 Selenium 执行 pytest -v --engine=selenium tests/test_login.py # 用 Playwright 执行 pytest -v --engine=playwright tests/test_login.py

这一步做完,双引擎骨架就活了。你会发现切换引擎的对用例层是透明的,真正写用例的人完全不需要关心底层是哪个工具。这就达到了架构设计的目标。

4. 并发提速与 CI 流水线整合

单条用例跑起来只是第一步,测试机器人的真正价值在回归场景里才能体现。当用例数量从几十条涨到几百条,串行执行的时间会让人怀疑人生。这一章讲两件事:怎么安全地并发执行,怎么把整套东西接进持续集成流水线,让机器人每天自动跑。

4.1 并发执行:如何避免浏览器实例互相踩踏

Playwright 的 context 隔离机制让并发有了很好的基础,但执行层仍然要小心。我推荐用 pytest-xdist 做多进程并发,它天然能让测试用例分布到多个 worker 上,每个 worker 独立跑自己的浏览器实例。

pytest -n 4 --engine=playwright tests/

-n 4表示启 4 个并发进程,每个进程会跑自己的 driver fixture,所以 SeleniumDriver 和 PlaywrightDriver 内部的 driver 根本不会跨进程共享。线程安全的那些问题在这里基本不存在。

但有两个问题必须提前预防。第一是机器资源,单个浏览器实例几十个进程很常见,4 个并发加上系统其他进程,内存很容易吃紧。我在本地机器上最多开到 8,CI 里反而会压到 4,因为容器的资源配置通常更紧。第二是测试数据隔离,并发跑用例时,如果两个用例同时登录同一个账号,很容易互相挤掉线。我的做法是给关键业务用例准备一组测试账号池,每个 worker 从池子里拿自己的账号,用完归还。这样并发稳定性立刻上了一个台阶。

4.2 CI 流水线中的执行策略

接入流水线时,我习惯把整套执行放在容器里,保证环境干净可复现。一次典型的流水线任务包含这几步:拉取代码、安装依赖、启动被测服务(或者使用远程测试环境)、执行 pytest、收集测试报告和产物。

stages: - test test-job: stage: test script: - pip install -r requirements.txt - playwright install chromium --with-deps - pytest -n 4 --engine=playwright --html=report.html --self-contained-html - pytest -n 2 --engine=selenium --html=report-selenium.html --self-contained-html artifacts: when: always paths: - report.html - report-selenium.html - artifacts/

两个引擎在同一轮流水线里都跑最关键的那批用例,是比较稳妥的策略。Playwright 负责主要回归,Selenium 负责老页面冒烟。这里有个经验:artifacts里的路径如果漏掉了artifacts/截图目录,等出问题想翻现场图时会发现啥都没留下。所以when: always和截图目录归档这两项,一定要在流水线配置里保留。

另外依赖缓存值得做。pip install每次全量下载其实很浪费时间,流水线支持缓存的话可以把~/.cache/pip和 Playwright 的浏览器目录缓存起来,整个执行时间能压缩下来不少。

4.3 稳定性三板斧:自动重试、失败截图、跟踪记录

自动化测试最让人头疼的是“这次红、下次绿”的偶发失败。网络抖动、服务重启、第三方资源响应慢都可能触发。盲目追求一次全绿不现实,合理的方式是给用例加上有限度的自动重试。

pytest -n 4 --reruns 2 --reruns-delay 1 --engine=playwright tests/

--reruns 2表示单个用例失败后再跑两次,--reruns-delay 1表示每次重试间隔一秒。我特别提醒一句:重试机制只用来抵抗偶发不稳定,不能用它掩盖真实功能缺陷。如果一个用例重试了两次仍然失败,那基本可以断定被测功能真的有问题,必须立即进入缺陷流程。

失败截图这件事,无论 Selenium 还是 Playwright 都必须做成强约束。Selenium 侧可以在 pytest 的 fixture 里监听用例失败事件,取当前 driver 状态存图;Playwright 侧除了截图,还可以启动 trace 录制,把整个页面的 DOM 变化、网络请求、控制台日志全部记录下来。出问题查 trace 比猜原因高效太多了:

# Playwright 侧开启追踪 self.context.tracing.start(screenshots=True, snapshots=True) # 用例结束后保存 self.context.tracing.stop(path=f"artifacts/trace/{case_name}.zip")

trace 文件可以回放出完整的操作链路,元素在哪个时刻变成了什么状态、请求在哪个节点失败,一目了然。这个能力是 Playwright 相比 Selenium 很有竞争力的一个点,强烈建议用起来。

5. 常见问题排查实录

双引擎方案跑了一段时间,陆陆续续会遇到一些有共性的坑。这一章把最典型的几类问题整理成速查表,并且针对每个问题给出我排查时的思路,方便你遇到类似情况时少走弯路。

5.1 驱动版本与浏览器不匹配

Selenium 最常见的翻车现场就是驱动和浏览器版本对不上。明明昨天还跑得好好的,今天浏览器偷偷更新了,驱动还在老版本,Session 创建直接报错。Selenium 4 的驱动自动管理可以省点事,但它需要能访问驱动下载站点;一旦测试环境在隔离内网,自动管理就失效了,只能手动把对应版本的驱动文件放到指定路径。

我的习惯是:锁死浏览器版本,不和 CI 环境共用同一个浏览器自动更新策略。部署浏览器时固定版本号,驱动文件按版本归档到项目的 third_party 目录,启动 Selenium 时明确配置驱动路径。Playwright 侧则没有这个烦恼,因为它用的是自己下载并锁定的浏览器版本,和系统浏览器互不干扰。这也是双引擎架构里的一个隐性优势——两个框架各管各的浏览器,不会互相拖累。

5.2 元素定位失败:从等待到选择器

定位失败是自动化测试里最不稀奇的问题。很多新手一上来就怀疑是选择器写得不对,但实际先要排除的是等待问题。我遇到过好几次“页面元素已存在但被遮住点不了”的情况,表象是定位失败,实际是那个按钮上面飘着一个 loading 遮罩层,等三五秒遮罩消失就好了。

在 Playwright 里可以用click(force=True)强制点击,但我不推荐一上来就 force。更重要的是搞清楚为什么会被遮住,是动画未结束、弹窗未关闭、还是布局稳定性问题。排查顺序建议是:先看控制台有没有报错,再截图确认元素位置,最后检查页面是不是有覆盖层。都排除了,才考虑改选择器。CSS 选择器优先,XPath 只用于老页面的兜底,这是两套引擎都舒服的选择器策略。

下面是几张常见问题的速查表:

异常现象常见原因排查与解决
Session 创建报错WebDriver 版本与浏览器不匹配锁定浏览器版本,匹配驱动
元素定位超时页面还没渲染完,或选择器失效先用短超时试运行,确认是否存在遮罩层
点击后无反应存在覆盖层或事件未绑定截图观察页面状态,检查控制台报错
并发时偶发登录失败测试账号互踢使用账号池隔离数据
iframe 里定位不到没有切入对应 frameSelenium 用 switch_to.frame,Playwright 用 frame_locator

5.3 性能与稳定的平衡点

追求快没错,但过度压缩等待时间是稳定性的隐形杀手。有人为了让用例跑得快,把全局超时设成 3 秒,结果页面稍微慢一点就开始红,重试又进一步拖慢整体时间,最后反而更慢。正确的做法是分场景配置超时:核心流程的等待可以放宽到 15 到 20 秒,边缘校验环节可以压缩到 5 秒。

网络请求方面,Playwright 可以做资源拦截,把图片、字体、统计脚本这些不影响断言结果的请求直接屏蔽:

def block_unused_resources(route): if route.request.resource_type in ("image", "font", "media"): route.abort() else: route.continue_() page.route("**/*", block_unused_resources)

这个操作能把整条用例耗时降下来不少。Selenium 没有这么方便的内置拦截,所以这类优化我基本只在 Playwright 侧做,也是我建议新页面优先用 Playwright 的一个现实原因。

5.4 登录态与多窗口这类特殊场景

自动化测试里登录态反复失效是特别讨厌的。每次用例都要走一遍登录流程,一个任务跑下来光登录就花掉几分钟。两个引擎侧都有自己的解决思路。

Playwright 的storage_state可以直接复用登录态:

# 首次登录后 context.storage_state(path="state.json") # 后续用例直接加载 context = browser.new_context(storage_state="state.json") page = context.new_page()

Selenium 侧没有这么一行式的方案,但可以通过把 cookie 保存下来再注入的方式达到同样效果:

# 登录成功后导出 cookie cookies = driver.get_cookies() json.dump(cookies, open("cookies.json", "w")) # 下次使用前导入 for cookie in cookies: driver.add_cookie(cookie)

多窗口场景也要做适配。Selenium 切窗口要操作window_handles,Playwright 的处理更直接,一个新的标签页就是一个独立的page对象,直接拿context.pages[-1]就能操作。这两套思路完全不同,好在都封装在各自的 Driver 类里,业务层不会感知到。

6. 经验沉淀与最后的建议

双引擎机器人跑了一段时间,我自己最大的体会有三点。第一,抽象接口一定要小而稳,宁可少几个方法,也别把底层功能全部暴露出来,接口一旦复杂,维护成本会吃掉自动化带来的收益。第二,日志和截图不是可有可无的附属品,而是机器人的眼睛;出问题找不到现场,再强的定位技巧都是空转。第三,切换引擎要顺滑,千万不要搞两套风格完全不同的用例写法,统一接口能让团队在两种引擎之间无缝切换,这才是双引擎方案最有价值的地方。

实际操作中我还有一个习惯:每周固定留一个晚上做全量回归加代码审查,跑完的日志、trace、截图全部归档,比对前后几周的数据,看失败趋势在哪个模块集中。这个做法看起来很普通,但它帮我发现过好几个界面上不明显但实际已经退化的功能点,比等用户报障再排查要主动得多。

如果你是刚开始搭这套体系,我的建议很直接:第一周只跑通一条最小用例,用 Selenium 和 Playwright 各跑一遍;第二周扩充到三个核心流程,把日志和截图做扎实;第三周再接 CI 和并发。节奏不用快,每步都要稳定落地。自动化测试这行,慢就是快,稳定压倒一切。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进