news 2026/9/10 2:50:35

Selenium Python 测试指南:基于 pytest 与 Bazel 的浏览器自动化测试编写与运行实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Selenium Python 测试指南:基于 pytest 与 Bazel 的浏览器自动化测试编写与运行实践

Selenium Python 测试指南:基于 pytest 与 Bazel 的浏览器自动化测试编写与运行实践

【免费下载链接】seleniumA browser automation framework and ecosystem.项目地址: https://gitcode.com/GitHub_Trending/se/selenium

Selenium 官方 Python 测试套件是一套以 pytest 为核心、以 Bazel 为构建引擎的跨浏览器测试体系。本文以仓库中的 py/TESTING.md 为主干,完整梳理其测试框架约定、Bazel 目标矩阵、原生 pytest 运行方式、浏览器级跳过(xfail)标记、Driver 生命周期管理、核心 Fixtures 与目录组织规范,并结合 py/conftest.py、py/BUILD.bazel 等源码实现,讲清每条规则背后的真实机制。读完本文,你将能够在新浏览器行为、新 WebDriver API 或 BiDi 协议特性落地时,按照官方同款规范编写测试、精准定位目标并高效运行调试。

测试框架总览

Selenium Python 测试套件的技术选型非常统一:

  • 测试框架使用pytest,断言直接使用 pytest 标准的assert语句,不引入额外断言库;
  • 测试所需的 HTML 页面统一存放在common/src/web/目录下(如javascriptPage.htmlalerts.htmlclick_tests/*.html等,均由测试用 Web 服务器动态提供);
  • 通过pagesfixture 加载测试页面,核心 API 是pages.load("pageName.html")
  • 每个测试函数的浏览器实例由driverfixture 提供,并由 pytest 按所选浏览器自动参数化。

一个最小的官方风格测试如下(与文档示例一致):

import pytest from selenium.webdriver.common.by import By def test_element_is_displayed(driver, pages): pages.load("javascriptPage.html") element = driver.find_element(By.ID, "displayed") assert element.is_displayed() is True @pytest.mark.xfail_safari(reason="Safari doesn't support this") def test_something_safari_fails(driver, pages): # Expected to fail on Safari pass

从这个例子可以拆出三条核心约定:测试函数签名中直接注入driverpages;页面加载统一走pages.load而不是硬编码 URL;对特定浏览器的已知失败用xfail_<browser>标记声明,而不是在测试体内写平台分支。

从源码看测试的运行基石

driverfixture:按浏览器参数化的真实驱动

driverfixture 定义在 py/conftest.py。它并非简单地yield webdriver.Chrome(),而是封装了一个Driver类(py/conftest.py),承担了以下职责:

  • 浏览器参数化:当命令行指定--driver chrome firefox时,pytest 通过pytest_generate_tests(py/conftest.py)对driverfixture 做indirect参数化,同一测试会在每个浏览器上各跑一遍;
  • 平台可用性检查:Safari 仅在 Darwin、IE 仅在 Windows 上运行,否则直接pytest.skipis_platform_valid,py/conftest.py);
  • 启动重试:本地驱动启动失败时最多重试 3 次、间隔 1 秒(DRIVER_START_RETRIES/DRIVER_START_INTERVAL,py/conftest.py);
  • 会话级回收stop_driver这个 session 级 autouse fixture 会在整个测试会话结束时统一quit(),避免驱动进程泄漏(py/conftest.py)。

pagesfixture 与webserverfixture

pagesfixture(py/conftest.py)本质是对webserverfixture 的薄封装:

class Pages: def url(self, name, localhost=False): return webserver.where_is(name, localhost) def load(self, name): driver.get(self.url(name))
  • pages.load("page.html"):驱动浏览器访问该页面;
  • pages.url("page.html"):只返回完整 URL,适合需要先构造 URL 再使用的场景。

底层的webserver是 session 级 autouse fixture(py/conftest.py),启动一个由 py/test/selenium/webdriver/common/webserver.py 实现的SimpleWebServer。它基于ThreadingHTTPServer,默认监听localhost:8000(端口被占用时自动 +1 重试),以common/src/web/为静态根目录,并提供echo_headersecho_bodyecho_jsonset_cookiebasic-auth(用户名postman/ 密码password)等测试专用端点,以及page/<n>动态页面生成能力。

使用 Bazel 运行测试

Bazel 会为每个浏览器生成独立的测试目标,且默认并行执行。仓库通过 py/BUILD.bazel 中的py_test_suite宏批量生成这些目标。

常用目标一览

bazel test //py/... # All tests bazel test //py:unit # Unit tests (no browser) bazel test //py:test-chrome # Chrome browser tests bazel test //py:test-firefox # Firefox browser tests bazel test //py:test-chrome-common # Common (cross-browser) tests with Chrome

目标命名遵循固定模式:

目标含义
//py:unit单元测试,不启动浏览器,对应 py/BUILD.bazel 中的unit套件
//py:test-<browser>某浏览器的完整聚合套件(common + actions + 各 feature 子套件),见 py/BUILD.bazel
//py:test-<browser>-common跨浏览器公共测试(test/selenium/webdriver/common/**+ support + 该浏览器私有测试),见 py/BUILD.bazel
//py:test-<browser>-actions仅 Actions API 相关测试(interactions_tests.pyw3c_interaction_tests.py等)
//py:test-<browser>-<feature>按功能拆分的小型子套件,如fedcmtimeoutsvirtual-authapi-requestalertsrenderedprint,见 py/BUILD.bazel
//py:test-<browser>-bidiBiDi 协议测试(仅 chrome / edge / firefox 支持)

运行单个测试文件与单个测试

单文件目标名的规则是test/<path>/<file>-<browser>[-variant]。命名由 py/private/suite.bzl 中的py_test_suite宏生成:去掉_tests.py后缀后拼接浏览器后缀。例如:

# 单个测试文件(Chrome) bazel test //py:test/selenium/webdriver/common/window_tests-chrome # 发现精确目标名 bazel query //py:all | grep window_tests

在单文件目标内用-k表达式精确过滤某个测试函数(BiDi 变体目标同理):

bazel test //py:test/selenium/webdriver/common/bidi/browsing_context_tests-chrome-bidi \ --test_arg=-k \ --test_arg=test_get_tree_with_child

bazel query //py/...可以列出py/下全部可用目标,是排查目标名拼写问题的首选手段。

BiDi 与远程(Grid)目标

# 以 BiDi 协议运行 bazel test //py:test-chrome-bidi # 针对 Grid 服务器运行(仅 chrome 和 firefox)。 # 套件会自行启动 Selenium standalone server,并通过 webdriver.Remote 与之通信。 bazel test //py:test-chrome-remote # classic protocol bazel test //py:test-chrome-remote-bidi # BiDi over Grid's websocket proxy bazel test //py:test-remote # every classic remote suite bazel test //py:test-remote-bidi # every BiDi remote suite

这些聚合目标的组成在 py/BUILD.bazel 中定义:test-remote聚合 chrome/firefox 的-remote-common-remote-actions与各-remote-<feature>子套件;而test-remote-bidi特意独立成 job,不与经典 remote 套件合并,以便在 BiDi over Grid 的 WebSocket 代理仍属新能力时单独观察其稳定性(py/BUILD.bazel)。

常用过滤与调试参数

# 按标签过滤测试 bazel test //py/... --test_tag_filters=chrome # 失败重试与输出控制 bazel test //py/... --flaky_test_attempts=3 bazel test //py/... --test_output=all bazel test //py/... --test_output=streamed # Live output for debugging # 无头模式 bazel test //py:test-chrome --headless

说明:--headless在 Bazel 目标层面透传给测试进程,最终由 conftest 读取并给浏览器 Options 添加--headless(Chrome/Edge)或-headless(Firefox)参数,见 py/conftest.py。

不使用 Bazel:直接用 pytest 运行

在本地开发迭代时,也可以绕过 Bazel 直接用 pytest 运行,前提是完成环境准备。

环境准备

# 1. 安装依赖(使用锁定文件保证可复现) pip install -r py/requirements_lock.txt # 2. 构建生成文件并拷贝到本地源码树 ./go py:local_dev

第二步会生成 DevTools 协议绑定、BiDi 模块等构建期生成物(对应 py/BUILD.bazel 中的generate_bidigenerate_devtools等规则),确保from selenium.webdriver.common.bidi import ...等导入可用。

运行命令

# 运行某目录下全部测试 pytest py/test/selenium/webdriver/chrome/ --driver chrome # 运行某个测试文件 pytest py/test/selenium/webdriver/common/window_tests.py # 运行单个测试函数 pytest py/test/selenium/webdriver/common/window_tests.py::test_should_get_the_size_of_the_current_window # 组合 pytest 选项 pytest py/test/selenium/webdriver/chrome/ --driver chrome --headless -v

命令行选项(来自 conftest)

所有--xxx选项都在 py/conftest.py 的pytest_addoption中注册,可按需组合:

选项说明
--driver <DRIVER>要运行的驱动,可选chromeedgefirefoxiesafariwebkitgtkwpewebkit,可多次传入实现多浏览器参数化
--browser <BROWSER>--driver的别名
--browser-binary <PATH>指定浏览器可执行文件位置
--driver-binary <PATH>指定 driver(服务)可执行文件位置
--browser-args <ARGS>启动浏览器时附加的参数(按空格拆分)
--headless以无头模式运行
--use-lan-ip测试服务器改用局域网 IP 而非 localhost(SimpleWebServer会以 LAN IP 监听)
--bidi启用 BiDi 协议支持
--remote针对远程 Grid 服务器运行

另外,当传入--driver时,pytest 会自动忽略与所选浏览器无关的测试目录(pytest_ignore_collect,py/conftest.py),例如只跑 Chrome 时不会收集 firefox 目录下的用例。

BiDi 与远程的 pytest 用法

Note:运行 BiDi 测试请使用--bidi标志。启用后 conftest 会给浏览器 Options 设置web_socket_url = True并将unhandled_prompt_behavior设为ignore(见 py/conftest.py)。

要针对 Grid 服务器运行,请追加--remote。它会启动一个 Selenium standalone server,并通过webdriver.Remote运行测试,因此需要先构建 Grid jar:bazel build //java/src/org/openqa/selenium/grid:selenium_server_deploy.jar--bidi --remote可组合使用:Grid 会把webSocketUrlcapability 改写为自身的/session/<id>/se/bidi端点,并将该 socket 代理转发到对应的 node。

从源码看,--remote路径由serverfixture(py/conftest.py)承载:它自动定位selenium_server_deploy.jar(优先 runfiles,其次bazel-bin),用空闲端口启动 Java Grid 服务器,并通过webdriver.Remote(command_executor=...)创建驱动;当同时给出--driver-binary--browser-binary时,还会生成--driver-configuration参数将驱动与浏览器固定到 Grid node,从而跳过 Selenium Manager 的自动探测(_pinned_grid_args,py/conftest.py)。

跳过测试:浏览器级 xfail 标记体系

跨浏览器测试中"这个用例在某个浏览器上必然失败"是常态,Selenium 为此定义了按浏览器区分的 pytest 标记。每个标记都接受可选的reasonrun参数:run=False表示完全跳过该测试,而不是"运行并期望失败"。

Marker使用场景
@pytest.mark.xfail_chrome预期在 Chrome 上失败
@pytest.mark.xfail_firefox预期在 Firefox 上失败
@pytest.mark.xfail_safari预期在 Safari 上失败
@pytest.mark.xfail_edge预期在 Edge 上失败
@pytest.mark.xfail_ie预期在 IE 上失败
@pytest.mark.xfail_remote预期在 Remote WebDriver 下失败
@pytest.mark.xfail_chrome(reason="Not implemented yet") @pytest.mark.xfail_firefox(reason="https://bugzilla.mozilla.org/123") def test_something(driver, pages): pass @pytest.mark.xfail_safari(run=False) # Skip entirely instead of xfail def test_skip_safari(driver, pages): pass

这些标记的注册信息位于 py/pyproject.toml,并在运行时由 conftest 的_apply_xfail_markers(py/conftest.py)按当前驱动名动态生效:它会查找xfail_<driver>标记;远程模式下若没有则回退到xfail_remote;标记带condition参数且求值为假时该次运行不生效;run=False时直接pytest.skip而非 xfail。真实用例可参考 py/test/selenium/webdriver/common/window_tests.py,该测试对 Chrome/Edge/Firefox/Remote 分别声明了失败原因(如 geckodriver 的已知 issue)。

Driver 生命周期管理

部分测试需要精细控制驱动实例的创建与销毁时机,通过以下两个标记实现:

Marker使用场景
@pytest.mark.no_driver_after_test测试结束后销毁 driver,强制下一个测试重新创建
@pytest.mark.needs_fresh_driver重启 driver 以保证测试隔离

两者在driverfixture 中的行为不同(py/conftest.py):

  • needs_fresh_driver:主要用于 BiDi 测试。普通 BiDi 测试默认复用同一个 driver(会话内共享,仅在窗口失效时重启,见ensure_valid_window),而带此标记的测试会在结束时stop_driver(),下一条用例拿到全新实例;
  • no_driver_after_test:任意模式下,测试结束后调用stop_driver()并将全局 driver 引用置空,实现"一次性 driver"语义;
  • 此外,pytest_exception_interact(py/conftest.py)会在测试失败时主动销毁 driver,避免故障状态被后续用例复用。

核心 Fixtures

Selenium Python 测试大量使用 pytest fixtures 来简化 setup/teardown。模块私有的 fixture 直接定义在使用它的测试文件里;跨模块共享的 fixture 集中在py/conftest.py

Fixture说明
driverWebDriver 实例,按浏览器自动参数化
pages加载测试页:pages.load("page.html")pages.url("page.html")
webserver测试 HTTP 服务器引用(session 级 autouse)
clean_driver不带参数化的全新 driver 类引用
clean_options全新的浏览器 Options 实例
headless浏览器是否以无头模式启动;用于断言无头浏览器不建模的行为(如窗口焦点——无头 Chromium 会把焦点永远报告给当前窗口)

基于这些基础 fixture,conftest 还提供了一批用途更专一的 fixture:

  • clean_service:返回当前驱动对应的Service类实例(可注入executable_path);
  • firefox_options/chromium_options:仅在目标浏览器匹配时返回干净的 Options(Firefox 需--driver firefox,Chromium 系列需--driver chrome|edge),否则自动 skip;
  • edge_service:直接返回 Edge 的Service类;
  • proxy_server:动态创建可自定义响应内容的 HTTP 代理服务器,测试结束后自动关闭(py/conftest.py);
  • driver_executable:解析--driver-binary指定的可执行文件路径(含 Bazel runfiles 路径解析)。

测试组织与命名规范

测试目录按"单元 / 集成"和"浏览器归属"双重维度组织:

py/test/ ├── unit/ # Unit tests (no browser) │ └── selenium/webdriver/ └── selenium/webdriver/ # Integration tests ├── common/ # Cross-browser tests ├── chrome/ ├── firefox/ ├── safari/ └── remote/

要点:

  • common/下是与浏览器无关的跨浏览器测试(window_tests.pyvisibility_tests.pyalerts_tests.py等),任何浏览器目标都会运行它们;
  • chrome/firefox/safari/等目录存放浏览器私有行为测试;
  • remote/下的测试需要--remote标志才会运行,否则 conftest 会直接 skip(_skip_unless_remote,py/conftest.py);
  • BiDi 协议测试集中在common/bidi/(如browsing_context_tests.pynetwork_tests.pyscript_tests.pyprotocol_tests.py等)与common/_bidi/,仅在*-bidi目标中收集。

测试文件一律以_tests.py结尾(如visibility_tests.py)。该约定同时被两处消费:Bazel 侧的 py/private/suite.bzl 用_is_test识别测试文件并自动生成目标;pytest 侧的 py/pyproject.toml 通过python_files = ["test_*.py", "*_test.py", "*_tests.py"]匹配。

构建文件:自动发现与免维护

新增测试通常不需要修改任何 Bazel 构建文件

  • py/private/suite.bzl 中的py_test_suite宏会对传入的srcs逐一过滤,凡匹配test_**_tests.py的文件都会自动生成pytest_test目标(底层封装见 py/private/pytest.bzl,它生成一个调用pytest.main()的 runner 并装配 runfiles);
  • 只要测试文件位于已被现有py_test_suite覆盖的目录(如py/test/selenium/webdriver/common/),就会自动纳入test-<browser>-common等套件。

唯一的例外是涉及特殊依赖的测试:例如alerts_tests.pywebdriverwait_tests.py依赖common_alert库、print_pdf_tests.py依赖common_print_page_options,因此它们在 py/BUILD.bazel 中被显式归入独立的 feature 子套件(FEATURE_TESTS),以控制依赖图的最小化。

小结:从编写到运行的完整路径

  1. 编写:在py/test/selenium/webdriver/<common|chrome|firefox|...>/下新建xxx_tests.py,测试函数注入driverpages,用 pytest 原生断言;
  2. 标记:按需添加xfail_<browser>no_driver_after_testneeds_fresh_driver等标记,声明平台预期与管理驱动生命周期;
  3. 定位:用bazel query //py:all | grep <关键词>找到精确目标,或直接bazel test //py:test-<browser>-common跑整套;
  4. 调试--test_output=streamed看实时输出,--flaky_test_attempts=3容忍偶发失败,--test_arg=-k <表达式>精确定位单个用例;
  5. 本地快跑pip install -r py/requirements_lock.txt && ./go py:local_dev后用pytest ... --driver chrome --headless直接迭代。

这套体系既保证了跨浏览器覆盖的广度(common 套件 + 浏览器私有套件),也通过 BiDi 目标、remote 目标和 feature 子套件实现了按协议、按拓扑、按依赖的细粒度编排,是研究或贡献 Selenium Python 绑定测试时应当遵循的标准范式。

【免费下载链接】seleniumA browser automation framework and ecosystem.项目地址: https://gitcode.com/GitHub_Trending/se/selenium

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:49:34

从反射到Source Generator:FUI框架装配逻辑优化实录

先说个场景&#xff0c;你就知道这个题目值不值得看下去了&#xff1a;上个季度我把 FUI 框架里的组件装配逻辑从“启动时扫程序集 反射建表”整套搬到了编译期 Source Generator 生成注册代码。搬完之后&#xff0c;最直观的体验是 IDE 里 CtrlF5 一按&#xff0c;页面秒开&a…

作者头像 李华
网站建设 2026/9/10 2:46:49

CANN/ge实验参数配置

试验参数 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 2:46:20

本体建模与知识图谱构建实战:从OWL到SPARQL的工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华