Java-Selenium-WebDriver-实用手册-全-

Java Selenium WebDriver 实用手册(全)

原文:zh.annas-archive.org/md5/35b7bb6327cca70dfdbf1a17bd553748

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Selenium 是一个开源的大型项目,它可以实现对 Web 浏览器的自动化。Selenium 项目的核心组件是 Selenium WebDriver,一个用于以编程方式控制浏览器(如 Chrome、Firefox、Edge、Safari 或 Opera)的库。Selenium WebDriver 提供了跨浏览器的应用程序编程接口(API),支持多种编程语言(官方支持 Java、JavaScript、Python、C#或 Ruby)。

尽管我们可以使用 Selenium WebDriver 实现与浏览器自动化相关的多种目的,但它的主要用途是为 Web 应用程序验证实现端到端测试。全球数千家组织和测试人员现在都在使用 Selenium,它是端到端测试的主要解决方案之一,支持一个价值数百万美元的行业。

谁应该阅读本书

本书全面总结了 Selenium WebDriver 版本 4 的主要功能,使用 Java 作为语言绑定。它回顾了自动化 Web 导航、浏览器操作、Web 元素交互、用户模拟、自动化驱动程序管理、页面对象模型(POM)设计模式、使用远程和云基础设施、与 Docker 和第三方工具集成等主要方面。

本书的主要受众包括不同级别的 Java 程序员(从初学者到高级),比如开发人员、测试人员、质量保证工程师等。因此,您需要对 Java 语言和面向对象编程有基本的了解。最终目标是全面了解 Selenium WebDriver 的主要方面,以便使用您选择的不同测试框架(例如 JUnit 或 TestNG)在 Java 中创建端到端测试。

为什么写这本书

测试自动化是一种利用自动化工具控制测试执行的软件测试技术。它可以提高效率和效果,同时确保软件系统的整体质量。在这个领域,Selenium WebDriver 是开发面向 Web 应用程序的端到端测试的事实标准库。本书是迄今为止对 Selenium 4 的第一次完整评估。

本书采用了一种学以致用的方法。为此,我们通过可立即执行的测试示例来回顾 Selenium WebDriver 的主要功能。这些示例在 GitHub 的一个开源存储库中公开可用(https://github.com/bonigarcia/selenium-webdriver-java)。为了完整起见,此存储库包含每个测试示例在不同的嵌入式测试框架中的不同风味:JUnit 4、JUnit 5(单独或与 Selenium-Jupiter 结合使用)和 TestNG。

阅读本书

本书内容分为 3 部分和 10 章节:

第一部分,介绍

第一部分提供了关于 Selenium、测试自动化和项目设置的技术背景。这部分比较理论,由两章组成:

  • 第一章,“Selenium 概述”,介绍了 Selenium 项目的核心组件(WebDriver、Grid 和 IDE)及其生态系统(即围绕 Selenium 的工具和技术)。此外,本章还回顾了与 Selenium 相关的端到端测试原则。

  • 第二章,“测试准备”,解释了如何设置包含使用 Selenium WebDriver API 的端到端测试的 Java 项目(Maven 和 Gradle)。然后,您将学习如何使用不同的测试框架(JUnit 4、JUnit 5(单独或与 Selenium-Jupiter 结合)、TestNG)开发您的第一个 WebDriver 测试。

第二部分,Selenium WebDriver API

第 II 部分提供了对 Selenium WebDriver API 的实用洞察。本部分以示例库中可用的测试为指导,并包括以下章节:

  • 第三章,“WebDriver 基础”,描述了用于自动化与 Web 应用程序交互的 Selenium WebDriver API 的主要方面。因此,本章还回顾了几种定位和等待 Web 元素的策略。此外,您将了解如何在浏览器中模拟用户操作(即使用键盘和鼠标进行的自动化交互)。

  • 第四章,“与浏览器无关的功能”,回顾了 Selenium WebDriver API 在不同浏览器中可互操作的方面。因此,本章展示了如何执行 JavaScript、创建事件监听器、管理窗口、制作屏幕截图、处理 Shadow DOM、操作 Cookie、访问浏览器历史或 Web 存储,以及与窗口、标签和 iframe 等元素交互。

  • 第五章,“特定于浏览器的操作”,解释了 Selenium WebDriver API 特定于特定浏览器的方面。这些特性组包括浏览器功能(选项、参数、偏好设置等)、Chrome 开发者工具协议(CDP)、地理位置功能、基本和 Web 身份验证、将页面打印为 PDF 或 WebDriver BiDi API。

  • 第六章,“远程 WebDriver”,描述了如何使用 Selenium WebDriver API 来控制远程浏览器。然后,您将学习如何设置和使用 Selenium Grid 版本 4。最后,您将了解如何在云提供商(如 Sauce Labs、BrowserStack 或 CrossBrowserTesting 等)和 Docker 容器中使用高级基础设施进行 Selenium 测试。

第三部分,高级概念

第 III 部分专注于在不同领域和用例中利用 Selenium WebDriver API。本部分包括以下章节:

  • 第七章,“页面对象模型(POM)”,介绍了 POM,这是与 Selenium WebDriver 一起使用的流行设计模式。该模式允许用户使用面向对象的类来建模网页,以便于测试维护和减少代码重复。

  • 第八章,“测试框架的细节”,回顾了与 Selenium WebDriver 一起使用的单元测试框架的几个特定功能,这些功能允许改进整个测试过程的不同方面。为此,本章首先解释了如何进行跨浏览器测试(即,使用参数化测试和测试模板重用相同的测试逻辑以验证使用不同浏览器的 Web 应用程序)。

  • 第九章,“第三方集成”,回顾了您可以使用的不同技术来增强您的 Selenium WebDriver 测试,例如报告工具、测试数据生成和其他框架(例如 Cucumber 或 Spring)。此外,本章描述了如何使用外部库与 Selenium 结合使用来实现特定用例,例如文件下载或非功能性测试(例如负载、安全性或可访问性)。

  • 第十章,“超越 Selenium”,介绍了与 Selenium 相关的几个自动化框架:Appium(用于移动测试)和 REST Assured(用于测试 REST Web 服务)。最后,我们回顾了一些与 Selenium WebDriver 最相关的当前替代方案,例如 Cypress、WebDriverIO、TestCafe、Puppeteer 或 Playwright。

本书中使用的约定

本书使用了以下印刷约定:

斜体

指示新术语、网址、电子邮件地址、文件名和文件扩展名。

等宽

用于程序列表,以及在段落内引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

等宽粗体

显示用户应该逐字输入的命令或其他文本。

等宽斜体

显示应替换为用户提供的值或由上下文确定的值的文本。

提示

这个元素表示提示或建议。

注意

这个元素表示一般注释。

警告

这个元素表示警告或注意。

使用代码示例

可以在 https://github.com/bonigarcia/selenium-webdriver-java 上下载代码示例。如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至 bookquestions@oreilly.com

这本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们以获得许可。例如,编写使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍中的示例代码需要许可。引用本书并引用示例代码回答问题无需许可。将本书的大量示例代码整合到您产品的文档中需要许可。

我们感激,但通常不需要,署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Hands-On Selenium WebDriver with Java 作者 Boni García(O’Reilly)。版权所有 2022 Boni García,978-1-098-11000-0。”

如果您认为您对代码示例的使用超出了公平使用或上述授权范围,请随时通过permissions@oreilly.com联系我们。

致谢

首先,我要感谢 O’Reilly 团队使这本书成为现实。他们在这段旅程的每个阶段都提供了卓越的编辑支持。

我也想感谢这本书的技术审阅人员。他们宝贵的反馈和专业建议显著提高了书籍的质量:Diego Molina(Sauce Labs 的高级软件工程师,Selenium 项目的技术负责人),Filippo Ricca(热那亚大学计算机科学副教授),Andrea Stocco(瑞士意大利大学软件研究所的博士后研究员),Ivan Krutov(Aerokube 的软件开发人员)和 Daniel Hinojosa(独立顾问、程序员、讲师、演讲者和作者)——非常感谢你们。

最后,我要感谢 Simon Stewart 的贡献(WebDriver 的创造者,直到 2021 年担任 Selenium 项目负责人)。非常感谢你,Simon,为这本书写序言并提供关于内容的宝贵反馈。但更重要的是,我要感谢你在这些年中领导 Selenium 项目的工作。你对自动化测试社区的贡献已经成为软件历史的一部分。

第一部分:介绍

Selenium 是一个开源的综合项目,由三个核心组件组成:WebDriver、Grid 和 IDE。Selenium 提供了高级能力,用于浏览器自动化,实践者通常用于为 Web 应用实施端到端测试。本书的第一部分全面介绍了 Selenium 项目及其生态系统。此外,它还提供了软件测试理论的入门,重点放在其对 Selenium WebDriver 的实际应用上。最后,你将了解如何使用 Maven 或 Gradle 设置项目来开发 WebDriver 测试。为了全面起见,我涵盖了不同的选择,关于用于嵌入对 Selenium WebDriver API 调用的单元测试框架,即 JUnit 4、JUnit 5(单独或通过 Selenium-Jupiter 扩展)和 TestNG。

第一章:Selenium 入门

Selenium 是一个由一组库和工具组成的开源套件,允许自动化 web 浏览器。我们可以将 Selenium 视为一个以三个核心组件为中心的项目:WebDriver、Grid 和 IDE(集成开发环境)。Selenium WebDriver 是一个允许以编程方式驱动浏览器的库。因此,我们可以使用 Selenium WebDriver 自动化地浏览网站并与网页交互(例如点击链接、填写表单等),就像真实用户一样。 Selenium WebDriver 的主要用途是自动化测试 web 应用程序。 Selenium 的其他用途包括自动化基于 web 的管理任务或网络抓取(自动化的 web 数据提取)。

本章全面介绍了 Selenium 的核心组件:WebDriver、Grid 和 IDE。然后,它回顾了 Selenium 生态系统,即其周围的其他工具和技术。最后,它分析了与 Selenium 相关的软件测试基础。

Selenium 核心组件

Jason Huggins 和 Paul Hammant 在 Thoughtworks 工作期间于 2004 年创建了 Selenium。他们选择了“Selenium”这个名字作为 Hewlett-Packard 开发的现有测试框架“Mercury”的对应物。这个名称很重要,因为化学元素硒以减少汞的毒性而闻名。

Selenium 的最初版本(今天称为 Selenium Core)是一个 JavaScript 库,模拟用户在 web 应用程序中的操作。 Selenium Core 解释所谓的 Selenese 命令来执行这些任务。这些命令被编码为由三部分组成的 HTML 表:command(在 web 浏览器中执行的操作,如打开 URL 或点击链接)、target(标识 web 元素的定位器,如给定组件的属性)和 value(可选数据,如输入到 web 表单字段的文本)。

Huggins 和 Hammant 在 Selenium Core 中增加了一个脚本层,创建了一个名为 Selenium Remote Control(RC)的新项目。Selenium RC 遵循客户端-服务器架构。客户端使用绑定语言(如 Java 或 JavaScript)通过 HTTP 发送 Selenese 命令到一个名为 Selenium RC Server 的中间代理。这个服务器根据需求启动 Web 浏览器,在网站中注入 Selenium Core 库,并将来自客户端的请求代理到 Selenium Core。此外,Selenium RC Server 将目标网站伪装成注入的 Selenium Core 库的相同本地 URL,以避免同源策略的问题。这种方法在当时是浏览器自动化的一个变革,但它有显著的限制。首先,由于 JavaScript 是支持自动化的基础技术,一些动作是不允许的,因为 JavaScript 不允许它们 - 例如,上传和下载文件或处理弹出窗口和对话框等。此外,Selenium RC 引入了相当大的开销,影响了其性能。

与此同时,Simon Stewart 在 2007 年创建了项目 WebDriver。从功能角度来看,WebDriver 和 Selenium RC 是等效的,即两个项目都允许程序员使用编程语言模拟 Web 用户。然而,WebDriver 使用每个浏览器的原生支持来执行自动化,因此,其功能和性能远远优于 RC。2009 年,在 Jason Huggins 和 Simon Stewart 在 Google 测试自动化大会上的会议之后,他们决定将 Selenium 和 WebDriver 合并成一个单一项目。这个新项目被称为 Selenium WebDriver 或 Selenium 2。这个新项目使用了基于 HTTP 的通信协议,结合了浏览器上的原生自动化支持。这种方法仍然是 Selenium 3(2016 年发布)和 Selenium 4(2021 年发布)的基础。现在我们将 Selenium RC 和 Core 称为“Selenium 1”,并且鼓励使用 Selenium WebDriver。本书重点介绍迄今为止最新版本的 Selenium WebDriver,即版本 4。

提示

附录 A 总结了随 Selenium 4 发布的新特性。本附录还包含了从 Selenium 3 升级到 4 的迁移指南。

今天,Selenium 是一个知名的自动化套件,由三个子项目组成:WebDriver、Grid 和 IDE。以下小节介绍了每个子项目的主要特点。

Selenium WebDriver

Selenium WebDriver 是一个允许自动控制网页浏览器的库。出于这个目的,它提供了不同语言绑定的跨平台 API。Selenium WebDriver 官方支持的编程语言包括 Java、JavaScript、Python、Ruby 和 C#。在内部,Selenium WebDriver 使用每个浏览器实现的本机支持来进行自动化过程。因此,我们需要在使用 Selenium WebDriver API 的脚本和浏览器之间放置一个称为 driver 的组件。表格 1-1 总结了 Selenium WebDriver 官方支持的浏览器和驱动程序。

注意

术语 Selenium 广泛用于指代用于浏览器自动化的库。由于这个术语也是总体项目的名称,我在本书中使用 Selenium 来标识由三个组件组成的浏览器自动化套件,即 Selenium WebDriver(库)、Selenium Grid(基础设施)和 Selenium IDE(工具)。

表格 1-1. Selenium WebDriver 支持的浏览器和驱动程序

浏览器 驱动程序 操作系统 维护者 下载
Chrome/Chromium chromedriver Windows/macOS/Linux Google https://chromedriver.chromium.org
Edge msedgedriver Windows/macOS/Linux 微软 https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver
Firefox geckodriver Windows/macOS/Linux Mozilla https://github.com/mozilla/geckodriver
Opera operadriver Windows/macOS/Linux Opera Software AS https://github.com/operasoftware/operachromiumdriver
Internet Explorer IEDriverServer Windows Selenium 项目 https://www.selenium.dev/downloads
Safari safaridriver macOS Apple 内建

驱动程序(例如 chromedriver、geckodriver 等)是平台相关的二进制文件,用于接收来自 WebDriver 脚本的命令,并将其转换为特定于某种浏览器的语言。在 Selenium WebDriver 的首个发布版(即 Selenium 2)中,这些命令(也称为 Selenium 协议)是通过 HTTP(即所谓的 JSON Wire Protocol)传输的 JSON 消息。如今,这种通信(仍然是 JSON over HTTP)遵循一个名为 W3C WebDriver 的标准规范。截至 Selenium 4,该规范是首选的 Selenium 协议。

图 1-1 总结了我们迄今所见的 Selenium WebDriver 的基本架构。可以看到,这个架构有三层。首先,我们有一个使用 Selenium WebDriver API 的脚本(Java、JavaScript、Python、Ruby 或 C#)。这个脚本将 W3C WebDriver 命令发送到第二层,其中包含驱动程序。本图展示了使用 chromedriver(控制 Chrome)和 geckodriver(控制 Firefox)的具体情况。最后,第三层包含了 Web 浏览器。在 Chrome 的情况下,本机浏览器遵循 DevTools Protocol。DevTools 是针对基于 Blink 渲染引擎的浏览器(如 Chrome、Chromium、Edge 或 Opera)的一组开发者工具。DevTools Protocol 基于 JSON-RPC 消息,并允许检查、调试和分析这些浏览器。在 Firefox 中,本机自动化支持使用 Marionette 协议。Marionette 是一个基于 JSON 的远程协议,允许对基于 Gecko 引擎的 Web 浏览器(如 Firefox)进行仪器化和控制。

hosw 0101

图 1-1. Selenium WebDriver 架构

总体来说,Selenium WebDriver 允许以编程方式控制网页浏览器,如同用户一样操作。为此,Selenium WebDriver API 提供了广泛的功能,用于浏览网页、与网页元素交互或模拟用户操作等。目标应用程序是基于 web 的,如静态网站、动态 Web 应用程序、单页面应用程序(SPA)、具有 Web 界面的复杂企业系统等。

Selenium Grid

Selenium 家族的第二个项目是 Selenium Grid。Philippe Hanrigou 在 2008 年开始开发该项目。Selenium Grid 是一组网络主机,为 Selenium WebDriver 提供浏览器基础设施。这种基础设施使得可以在多个操作系统上(并行)执行 Selenium WebDriver 脚本,使用不同类型和版本的远程浏览器。

图 1-2 展示了 Selenium Grid 的基本架构。可以看到,一组节点提供了 Selenium 脚本使用的浏览器。这些节点可以使用不同的操作系统(如我们在 表 1-1 中看到的)以及安装了各种浏览器。这个 Grid 的中心入口点是 Hub(也称为 Selenium 服务器)。这个服务器端组件负责跟踪节点并代理 Selenium 脚本的请求。与 Selenium WebDriver 类似,W3C WebDriver 规范是这些脚本与 Hub 之间通信的标准协议。

hosw 0102

图 1-2. Selenium Grid 汇集节点架构

Grid 中的中心-节点架构自 Selenium 2 版本以来就已经可用。这种架构在 Selenium 3 和 4 中也存在。然而,如果向中心发送的请求数量很大,这种集中式架构可能会导致性能瓶颈。Selenium 4 提供了完全分布式的 Selenium Grid 变体,以避免这个问题。此架构实现了先进的负载均衡机制,以避免任何组件的过载。

Tip

第 6 章描述了如何按照经典方法设置 Selenium Grid(基于中心和一组节点)。本章还涵盖了独立模式(即在同一台机器上托管中心和节点)以及完全分布式架构。

Selenium IDE

Selenium IDE 是 Selenium 套件的最后一个核心组件。Shinya Kasatani 在 2006 年创建了这个项目。Selenium IDE 是一个实现所谓的记录和回放(R&P)自动化技术的工具。顾名思义,这项技术分为两步。首先,在 Selenium IDE 中,记录部分捕捉用户与浏览器的交互,将这些动作编码为 Selenium 命令。其次,使用生成的 Selenium 脚本自动执行浏览器会话(回放)。

Selenium IDE 的早期版本是一个嵌入 Selenium Core 来录制、编辑和回放 Selenium 脚本的 Firefox 插件。这些早期版本是 XPI 模块(即用于创建 Mozilla 扩展的技术)。从 2017 年发布的版本 55 开始,Firefox 将对插件的支持迁移到W3C 浏览器扩展规范。因此,Selenium IDE 被停用,并且一段时间内无法使用。Selenium 团队根据浏览器扩展建议重新编写了 Selenium IDE,以解决这个问题。由此,我们现在可以在 Chrome、Edge 和 Firefox 等多个浏览器中使用 Selenium IDE。

图 1-3 展示了新版 Selenium IDE GUI(图形用户界面)。

使用此 GUI,用户可以记录与浏览器的交互并编辑和执行生成的脚本。Selenium IDE 将每个交互编码为不同部分:命令(即在浏览器中执行的动作)、目标(即 Web 元素的定位器)和值(即处理的数据)。我们还可以选择包括命令的描述。图 1-3 展示了这些步骤的一个记录示例:

  1. 打开网站(https://bonigarcia.dev/selenium-webdriver-java)。在本书的其余部分中,我们将使用此网站作为实践站点。

  2. 点击带有“GitHub”文本的链接。结果,导航移动到示例存储库源代码。

  3. 断言网页上存在书名(Hands-On Selenium WebDriver with Java)。

  4. 关闭浏览器。

hosw 0103

图 1-3. Selenium IDE 显示了录制脚本的示例

一旦我们在 Selenium IDE 中创建了脚本,我们就可以将此脚本导出为 Selenium WebDriver 测试。例如,图 1-4 展示了如何将所示示例转换为 JUnit 测试用例。最后,我们可以将项目保存在本地计算机上。此示例的结果项目可在 示例 GitHub 存储库 中找到。

在撰写本文时,Selenium 项目正在将 Selenium IDE 移植到 Electron。Electron 是一个基于 Chromium 和 Node.js 的开源框架,允许进行桌面应用程序开发。

hosw 0104

图 1-4. 将 Selenium IDE 脚本导出为 JUnit 测试用例

Selenium 生态系统

软件生态系统是与共同技术背景支持下的共享市场进行交互的元素的集合。在 Selenium 的情况下,其生态系统涉及官方核心项目和其他相关项目、库和参与者。本节将审查 Selenium 生态系统,分为以下几类:语言绑定、驱动程序管理器、框架、浏览器基础设施和社区。

语言绑定

正如我们所知,Selenium 项目为 Selenium WebDriver 维护了各种语言绑定:Java、JavaScript、Python、Ruby 和 C#。然而,也有其他语言可用。表 1-2 总结了社区维护的 Selenium WebDriver 的这些语言绑定。

表 1-2. Selenium WebDriver 的非官方语言绑定

名称 语言 许可证 维护者 网站
hs-webdriver Haskell BSD-3-Clause Adam Curtis https://github.com/kallisti-dev/hs-webdriver
php-webdriver PHP MIT Facebook、社区 https://github.com/php-webdriver/php-webdriver
RSelenium R AGPLv3 rOpenSci https://github.com/ropensci/RSelenium
Selenium Go MIT Miki Tebeka https://github.com/tebeka/selenium
Selenium-Remote-Driver Perl Apache 2.0 George S. Baugh https://github.com/teodesian/Selenium-Remote-Driver
webdriver.dart Dart Apache 2.0 Google https://github.com/google/webdriver.dart
wd JavaScript Apache 2.0 Adam Christian https://github.com/admc/wd

驱动程序管理器

驱动程序是使用 Selenium WebDriver 原生控制网络浏览器所必需的组件(参见 Figure 1-1)。因此,在使用 Selenium WebDriver API 之前,我们需要管理这些驱动程序。驱动程序管理是指下载、设置和维护适合特定浏览器的正确驱动程序的过程。驱动程序管理过程中的常见步骤包括:

1. 下载

每个浏览器都有自己的驱动程序。例如,我们使用 chromedriver 来控制 Chrome 或 geckodriver 来控制 Firefox(参见 Table 1-1)。驱动程序是特定于平台的二进制文件。因此,我们需要为特定操作系统(通常是 Windows、macOS 或 Linux)下载适当的驱动程序。此外,我们需要考虑驱动程序的版本,因为驱动程序发布与特定浏览器版本(或范围)兼容。例如,要使用 Chrome 91.x,我们需要下载 chromedriver 91.0.4472.19. 我们通常可以在驱动程序文档或发布说明中找到浏览器驱动程序的兼容性信息。

2. 设置

一旦我们有了合适的驱动程序,我们需要在我们的 Selenium WebDriver 脚本中使其可用。

3. 维护

现代网络浏览器(例如 Chrome,Firefox 或 Edge)会自动静默升级,无需提示用户。因此,关于 Selenium WebDriver,我们需要及时维护浏览器驱动程序版本的兼容性,以适应这些所谓的evergreen浏览器。

正如您所见,驱动程序的维护过程可能耗时。此外,它可能会给 Selenium WebDriver 用户带来问题(例如,由于自动浏览器升级后的浏览器驱动程序不兼容而导致的测试失败)。因此,所谓的驱动程序管理器旨在在一定程度上自动化驱动程序管理过程。Table 1-3 总结了不同语言绑定的可用驱动程序管理器。

Table 1-3. Selenium WebDriver 的驱动程序管理器

Name Language License Maintainer Website
WebDriverManager Java Apache 2.0 Boni García https://github.com/bonigarcia/webdrivermanager
webdriver-manager JavaScript MIT Google https://www.npmjs.com/package/webdriver-manager
webdriver-manager Python Apache 2.0 Serhii Pirohov https://pypi.org/project/webdriver-manager
WebDriverManager.Net C# MIT Aliaksandr Rasolka https://github.com/rosolko/WebDriverManager.Net
webdrivers Ruby MIT Titus Fortner https://github.com/titusfortner/webdrivers
提示

在本书中,我推荐使用 WebDriverManager,因为它自动化了整个驱动程序维护过程(即下载、设置和维护)。有关自动化和手动驱动程序管理的更多信息,请参见 附录 B。

定位器工具

Selenium WebDriver API 提供了不同的定位 Web 元素的方法(参见 第三章):通过属性(id、name 或 class)、通过链接文本(完整或部分)、通过标签名、通过 CSS(层叠样式表)选择器或通过 XML Path Language(XPath)。具体的工具可以帮助识别和生成这些定位器。表格 1-4 展示了其中一些工具。

表格 1-4. 定位器工具概述

名称 类型 许可证 维护者 网站
Chrome DevTools 内置浏览器工具 专有免费软件,基于开源 Google https://developer.chrome.com/docs/devtools
Firefox Developer Tools 内置浏览器工具 MPL 2.0 Mozilla https://developer.mozilla.org/en-US/docs/Tools
Cropath 浏览器扩展 免费软件 AutonomIQ https://autonomiq.io/deviq-chropath.html
SelectorsHub 浏览器扩展 免费软件 Sanjay Kumar https://selectorshub.com
POM Builder 浏览器扩展 免费软件 LogiGear Corporation https://pombuilder.com

框架

在软件工程中,框架是一组用作软件开发的概念和技术基础和支持的库和工具。Selenium 是包装、增强或补充其默认功能的框架的基础。表格 1-5 包含了基于 Selenium 的这些框架和库。

表格 1-5. 基于 Selenium 的测试框架和库

名称 语言 描述 许可证 维护者 网站
CodeceptJS JavaScript 将浏览器交互建模为用户视角的简单步骤的多后端测试框架 MIT Michael Bodnarchuk https://codecept.io
FluentSelenium Java Selenium WebDriver 的流畅 API Apache 2.0 Paul Hammant https://github.com/SeleniumHQ/fluent-selenium
FluentLenium Java 网站和移动自动化框架,用于创建可读性强、可重用的 WebDriver 测试 Apache 2.0 FluentLenium 团队 https://fluentlenium.com
Healenium Java 使用机器学习算法分析 Web 和移动 Web 元素,改善 Selenium 测试稳定性的库 Apache 2.0 Anna Chernyshova 和 Dmitriy Gumeniuk https://healenium.io
Helium Python 基于 Selenium WebDriver 的高级 API MIT Michael Herrmann https://github.com/mherrmann/selenium-python-helium
QAF (QMetry Automation Framework) Java 用于 Web 和移动应用程序的测试自动化平台 MIT Chirag Jayswal https://qmetry.github.io/qaf
Lightning Java 轻量级的 Selenium WebDriver Java 客户端 Apache 2.0 FluentLenium https://github.com/aerokube/lightning-java
Nerodia Python Watir Ruby gem 的 Python 移植版 MIT Lucas Tierney https://nerodia.readthedocs.io
Robot Framework Python, Java, .NET 等 基于可读测试用例的通用自动化框架 Apache 2.0 Robot Framework Foundation https://robotframework.org
Selenide 团队 Java Selenium WebDriver 的流畅、简洁 API MIT Selenide 团队 https://selenide.org
SeleniumBase Python 基于 WebDriver 和 pytest 的浏览器自动化框架 MIT Michael Mintz https://seleniumbase.io
Watir (Web Application Testing in Ruby) Ruby 基于 WebDriver 的 Ruby gem 库,用于自动化 Web 浏览器 MIT Titus Fortner http://watir.com
WebDriverIO JavaScript 基于 WebDriver 和 Appium 的测试自动化框架 MIT Christian Bromann https://webdriver.io
Nightwatch.js JavaScript 基于 W3C WebDriver 的集成端到端测试框架 MIT Andrei Rusu https://nightwatchjs.org
Applitools Java, JavaScript, C#, Ruby, PHP, Python 用于视觉用户界面回归和 A/B 测试的测试自动化框架。它为 Selenium、Appium 等提供 SDK 商业 Applitools 团队 https://applitools.com
Katalon Studio Java, Groovy 利用 Selenium WebDriver、Appium 和云提供商的测试自动化平台 商业 Katalon 团队 https://www.katalon.com
TestProject Java, C#, Python 构建在 Selenium 和 Appium 之上的 Web 和移动应用测试自动化平台 商业 TestProject 团队 https://testproject.io

浏览器基础设施

我们可以使用 Selenium WebDriver 来控制安装在运行 WebDriver 脚本的机器上的本地浏览器。此外,Selenium WebDriver 还可以驱动远程 Web 浏览器(即在其他主机上执行的浏览器)。在这种情况下,我们可以使用 Selenium Grid 来支持远程浏览器基础设施。然而,这种基础设施的创建和维护可能具有挑战性。

或者,我们可以使用 云服务提供商 来外包支持浏览器基础设施的责任。在 Selenium 生态系统中,云服务提供商是为自动化测试提供托管服务的公司或产品。这些公司通常为 Web 和移动测试提供商业解决方案。云服务提供商的用户可以请求各种类型、版本和操作系统的按需浏览器。此外,这些提供商通常还提供其他服务,以简化测试和监控活动,例如访问会话录像或分析能力等。目前 Selenium 最相关的云服务提供商包括 Sauce LabsBrowserStackLambdaTestCrossBrowserTestingMoon CloudTestingBotPerfectoTestinium

另一个我们可以使用来支持 Selenium 浏览器基础设施的解决方案是 Docker。Docker 是一种开源软件技术,允许用户将应用程序打包和运行为轻量级、可移植的容器。Docker 平台有两个主要组件:Docker 引擎,用于创建和运行容器,以及 Docker Hub,用于分发 Docker 镜像的云服务。在 Selenium 领域,我们可以使用 Docker 来打包和执行容器化的浏览器。表 1-6 总结了在 Selenium 生态系统中使用 Docker 的相关项目。

表 1-6. Selenium 的 Docker 资源

名称 描述 许可证 维护者 网站
docker-selenium Selenium Grid 的官方 Docker 镜像 Apache 2.0 Selenium 项目 https://github.com/seleniumhq/docker-selenium
Selenoid 用 Go 语言轻量级实现的 Selenium Hub,在 Docker 中运行浏览器(镜像可在 Docker Hub 上找到) Apache 2.0 Aerokube https://aerokube.com/selenoid
Moon 使用 Docker 和 Kubernetes 的企业级 Selenium 集群 商业 Aerokube https://aerokube.com/moon
Callisto 开源的 Kubernetes 本地实现的 Selenium Grid MIT Aerokube https://github.com/wrike/callisto

社区

由于软件开发的协作性质,需要许多参与者的组织和互动。在开源领域,我们可以通过社区的相关性来衡量项目的成功。Selenium 得到了全球许多不同参与者的大力支持。表 1-7 总结了几个分组资源,包括官方文档、开发、支持和活动。

表 1-7. Selenium 社区资源

类别 描述 网站
官方文档 用户指南 https://www.selenium.dev/documentation
博客 https://www.selenium.dev/blog
Wiki https://github.com/seleniumhq/selenium/wiki
生态系统 https://www.selenium.dev/ecosystem
开发 源代码 https://github.com/seleniumhq/selenium
问题 https://github.com/seleniumhq/selenium/issues
治理 https://www.selenium.dev/project
支持 用户组 https://groups.google.com/group/selenium-users
Slack https://seleniumhq.slack.com
IRC https://webchat.freenode.net/#selenium
StackOverflow https://stackoverflow.com/questions/tagged/selenium
Reddit https://www.reddit.com/r/selenium
活动 会议 https://www.selenium.dev/categories/conference
Meetups https://www.meetup.com/topics/selenium

软件测试基础

软件测试(或简称测试)包括对称为被测系统(SUT)的软件的动态评估,通过一组有限的测试用例(或简称测试)对其进行评估,并对其做出裁决。测试意味着使用特定的输入值执行 SUT,以评估结果或期望行为。

乍一看,我们可以区分软件测试的两个单独类别:手动和自动化。一方面,在手动测试中,一个人(通常是软件工程师或最终用户)评估 SUT。另一方面,在自动化测试中,我们使用特定的软件工具开发测试并控制它们对 SUT 的执行。自动化测试允许在 SUT 中早期检测缺陷(通常称为错误),同时提供大量额外的好处(例如成本节省、快速反馈、测试覆盖率或可重复使用性等)。在某些情况下,手动测试也可以是一种有价值的方法,例如探索性测试(即人工测试人员自由地调查和评估 SUT)。

注意

此部分提供的众多测试形式没有统一的分类标准。这些概念正如软件工程一样,处于持续演变和辩论之中。可以将其视为适用于大量项目的提议。

测试级别

根据 SUT 的规模不同,我们可以定义不同的测试级别。这些级别确定了软件团队在测试工作中划分的几个类别。在本书中,我提出使用堆叠布局来表示不同的级别(见图 1-5)。这个结构的较低级别代表了用于验证软件小片段(称为单元)的测试。随着堆栈的上升,我们在其中找到其他层级(例如集成系统等),其中 SUT 集成了越来越多的组件。

hosw 0105

图 1-5. 不同测试级别的堆栈表示

此堆栈的最低级别是单元测试。在这个级别,我们评估软件的各个单元。一个单元是特定的可观察行为元素。例如,单元通常是面向对象编程中的方法或类,以及函数式编程中的函数。单元测试旨在验证每个单元的预期行为。由于每个测试在隔离环境中执行少量代码,自动化单元测试通常运行非常快速。为了实现这种隔离,我们可以使用测试替身,即替换给定单元的依赖组件的软件片段。例如,在面向对象编程中,一种流行的测试替身类型是模拟对象。模拟对象使用一些程序化的行为来模仿实际对象。

图 1-5 中的下一个级别是集成测试。在这个级别,不同的单元组合在一起以创建复合组件。集成测试旨在评估涉及单元之间的交互并暴露其接口中的缺陷。

然后,在系统测试端到端(E2E)级别,我们测试整个软件系统。我们需要部署系统测试对象(SUT)并验证其高级功能来执行这些级别的测试。系统/端到端测试与集成测试的区别在于前者涉及所有系统组件和最终用户(通常是模拟的)。换句话说,系统和端到端测试通过用户界面(UI)评估 SUT。该 UI 可以是图形化的(GUI)或非图形化的(例如基于文本或其他类型)。

图 1-6 展示了系统测试与端到端测试之间的区别。如您所见,端到端测试涉及软件系统及其依赖子系统(例如数据库或外部服务)。而系统测试仅包括软件系统,这些外部依赖通常是模拟的。

hosw 0106

图 1-6. 测试不同级别的基于组件的表示

验收测试是所呈现堆栈的顶层。在这个级别,最终用户参与测试过程。验收测试的目标是决定软件系统是否符合最终用户的期望。如图 1-6 所示,与端到端测试类似,验收测试验证整个系统及其依赖项。因此,验收测试也使用 UI 来执行 SUT 验证。

提示

Selenium WebDriver 的主要目的是实施端到端测试。尽管如此,我们可以使用 WebDriver 来进行系统测试,当模拟网站调用的后端时。此外,我们可以将 Selenium WebDriver 与行为驱动开发(BDD)工具结合使用,以实施验收测试(请参阅第九章)。

测试类型

根据设计测试用例的策略,我们可以实施不同类型的测试。两种主要的测试类型是:

功能测试(也称为行为或闭箱测试

评估软件片段是否符合预期行为(即其功能需求)。

结构测试(也称为透明箱测试

确定程序代码结构是否存在错误。为此,测试人员应了解软件片段的内部逻辑。

这些测试类型的区别在于,功能测试是基于责任的,而结构测试是基于实现的。这两种类型可以在任何测试级别(单元、集成、系统、端到端或验收)进行。然而,结构测试通常在单元或集成级别进行,因为这些级别能够更直接地控制代码执行流程。

警告

黑盒测试白盒测试分别是功能测试和结构测试的另外两个名称。然而,由于科技行业正努力采用更具包容性的术语,而非潜在有害的语言,因此不推荐使用这些称号。

功能测试有不同的类型。例如:

UI 测试(当 UI 是图形界面时称为GUI 测试

评估应用的视觉元素是否符合预期功能。请注意,UI 测试与系统和端到端测试级别不同,因为前者测试界面本身,而后者通过 UI 评估整个系统。

负面测试

在意外条件下评估 SUT(例如预期异常)。此术语是常规功能测试(有时称为正面测试)的对应项,其中我们评估 SUT 是否按预期行为(即其快乐路径)。

跨浏览器测试

这是针对 Web 应用的。它旨在验证不同 Web 浏览器(类型、版本或操作系统)中的网站和应用的兼容性。

第三种杂项测试类型,非功能测试,包括评估软件系统的质量属性(即其非功能需求)的测试策略。非功能测试的常见方法包括但不限于:

性能测试

评估软件系统的不同指标,例如响应时间、稳定性、可靠性或可扩展性。性能测试的目标不是查找错误,而是查找系统瓶颈。性能测试有两种常见的子类型:

负载测试

通过模拟多个并发用户增加系统的使用量,以验证其是否可以在定义的边界内运行。

压力测试

超越系统的操作能力来进行系统练习,以确定系统崩溃的实际极限。

安全测试

试图评估安全性关注点,例如机密性(信息保护披露)、认证(确保用户身份)或授权(确定用户权利和特权)等。

可用性测试

评估软件应用的用户友好程度。这种评估也称为用户体验(UX)测试。可用性测试的一个子类型是:

A/B 测试

比较同一应用的不同变体,以确定哪一个对其最终用户更有效。

可访问性测试

评估系统是否可供残障人士使用。

提示

我们主要使用 Selenium WebDriver 来实施功能测试(即与 Web 应用程序 UI 进行交互,以评估应用程序行为)。不太可能使用 WebDriver 来实施结构测试。此外,虽然这不是其主要用途,但我们可以使用 WebDriver 来实施非功能性测试,例如负载、安全性、可访问性或本地化(评估特定区域设置)测试(参见 第九章)。

测试方法论

软件开发生命周期 是在软件工程中创建软件系统所需的活动、动作和任务的集合。软件工程师在整体开发生命周期中设计和实施测试用例的时刻取决于具体的开发过程(例如迭代式、瀑布式或敏捷式等)。最相关的两种测试方法是:

测试驱动开发(TDD)

TDD 是一种方法论,我们在实际软件设计和实施之前设计和实施测试。在 21 世纪初期,随着极限编程(XP)等敏捷软件开发方法的兴起,TDD 受到欢迎。在 TDD 中,开发人员首先为给定特性编写(最初失败的)自动化测试。然后,开发人员创建一段代码来通过该测试。最后,开发人员重构代码以实现或改善可读性和可维护性。

测试末位开发(TLD)

TLD 是一种方法论,我们在实施 SUT 后设计和实施测试。这种做法在传统的软件开发流程中很典型,如瀑布式(顺序式)、增量式(多瀑布式)、螺旋式(风险导向的多瀑布式)或 Rational Unified Process (RUP)。

另一种相关的测试方法是 行为驱动开发(BDD)。BDD 是从 TDD 演变而来的一种测试实践,因此我们在软件开发生命周期的早期阶段设计测试。为此,最终用户与开发团队进行对话(通常是项目负责人、经理或分析师)。这些对话正式化了对期望行为和软件系统的共同理解。因此,我们根据一个或多个 场景 创建验收测试,遵循 给定-当-则 的结构:

给定

场景开始时的初始背景

触发场景的事件

那么

预期结果

提示

TLD 是一种常见的实践,用于实现 Selenium WebDriver。换句话说,开发人员/测试人员在 SUT 可用之前不实施 WebDriver 测试。然而,也可以采用不同的方法。例如,使用 WebDriver 结合 Cucumber 时,BDD 是一种常见的方法(参见 第九章)。

与测试方法论领域密切相关的是持续集成(CI)的概念。CI 是一种软件开发实践,软件项目的成员持续构建、测试和集成他们的工作。Grady Booch 于 1991 年首次提出了 CI 这个术语。现在它是创建软件的一种流行策略。

如图 1-7 所示,CI 有三个独立的阶段。首先,我们使用一个源代码存储库,这是一个存储和共享软件项目源代码的托管设施。我们通常使用版本控制系统(VCS)来管理此存储库。VCS 是一个跟踪源代码、谁做了每个更改以及何时做的工具(有时称为补丁)。

hosw 0107

图 1-7. CI 通用流程

Git,最初由 Linus Torvalds 开发,是当今首选的版本控制系统。其他选择包括并发版本系统(CVS)或 Subversion(SVN)。在 Git 之上,一些代码托管平台(例如 GitHub、GitLab 或 Bitbucket)提供了协作云存储库托管服务,用于开发、共享和维护软件。

开发者在本地环境中同步一个本地存储库(或简称repo)的副本。然后,他们使用该本地副本进行编码工作,将新的更改提交到远程存储库(通常是每天)。CI 的基本思想是每次提交都会触发对新更改的软件构建和测试。用于评估补丁是否破坏构建的测试套件称为回归测试。回归套件可以包含不同类型的测试,包括单元测试、集成测试、端到端测试等。

当测试数量过大而无法进行回归测试时,我们通常只选择整个套件中的一部分相关测试。有不同的策略来选择这些测试,例如冒烟测试(即确保关键功能的测试)或合理性测试(即评估基本功能的测试)。最后,我们可以将完整的套件作为计划任务执行(通常是每夜)。

我们需要使用一个名为构建服务器的服务器端基础设施来实现 CI 流水线。当回归测试失败时,构建服务器通常会向原始开发者报告问题。表 1-8 提供了几个构建服务器的摘要。

表 1-8. 构建服务器

名称 描述 许可证 维护者 网站
Bamboo 与 Jira(问题跟踪器)和 Bitbucket 轻松使用 商业版 Atlassian https://www.atlassian.com/software/bamboo
GitHub Actions GitHub 中集成的构建服务器 公共存储库免费 微软 https://github.com/features/actions
GitLab CI/CD GitLab 中集成的构建服务器 公共存储库免费 GitLab https://docs.gitlab.com/ee/ci
Jenkins 开源自动化服务器 MIT Jenkins 团队 https://www.jenkins.io
提示

我使用一个 GitHub 存储库(https://github.com/bonigarcia/selenium-webdriver-java)来发布和维护本书中提供的测试示例。GitHub Actions 是此存储库的构建服务器(见第二章)。

我们可以通过两种方式扩展典型的 CI 流水线(见图 1-8):

持续交付(CD)

在 CI 之后,构建服务器将发布版本部署到一个暂存环境(即用于测试目的的生产环境副本),并执行自动化验收测试(如果有)。

持续部署

构建服务器将软件发布到生产环境作为最后一步。

hosw 0108

图 1-8. 持续集成、交付和部署流水线

靠近 CI,术语 DevOps(开发与运维)已经获得了推广。DevOps 是一种软件方法论,促进了软件项目中不同团队(包括开发者、测试人员、质量保证、运维等)之间的沟通和协作,以高效开发和交付软件。

测试自动化工具

我们需要使用一些工具来有效地实施、执行和控制自动化测试。其中最相关的测试工具类别之一是单元测试框架。单元测试家族的原始框架(也称为xUnit)是 SmalltalkUnit(或 SUnit)。SUnit 是由 Kent Beck 于 1999 年为 Smalltalk 语言创建的单元测试框架。Erich Gamma 将 SUnit 移植到 Java,创建了 JUnit。从那时起,JUnit 变得非常流行,激发了其他单元测试框架的开发。表 1-9 总结了不同语言中最相关的单元测试框架。

表 1-9. 单元测试框架

名称 语言 描述 许可证 维护者 网站
JUnit Java xUnit 家族的参考实现 EPL JUnit 团队 https://junit.org
TestNG Java 受 JUnit 和 NUnit 启发,包括额外功能 Apache 2.0 Cedric Beust https://testng.org
Mocha JavaScript 用于 Node.js 和浏览器的测试框架 MIT OpenJS Foundation https://mochajs.org
Jest JavaScript 着重于 Web 应用程序的简易性 MIT Facebiij https://jestjs.io
Karma JavaScript 允许在 Web 浏览器中执行 JavaScript 测试 MIT Karma 团队 https://karma-runner.github.io
NUnit .Net 适用于所有.Net 语言(C#、Visual Basic 和 F#)的单元测试框架 MIT .NET Foundation https://nunit.org
unittest Python Python 2.1 起作为标准库包含的单元测试框架 PSF 许可证 Python 软件基金会 https://docs.python.org/library/unittest.html
minitest Ruby Ruby 的完整测试工具套件 MIT Seattle Ruby Brigade https://github.com/settlers/minitest

xUnit 家族的一个重要共同特征是测试结构,由四个阶段组成(见 图 1-9):

设置

测试用例初始化 SUT 以展示预期行为。

练习

测试用例与 SUT 交互。因此,测试从 SUT 获取结果。

验证

测试用例决定从 SUT 获取的结果是否符合预期。为此,测试包含一个或多个断言。断言(或谓词)是检查预期条件是否为真的布尔值函数。执行断言生成测试结论(通常是通过或失败)。

拆卸

测试用例将 SUT 恢复到初始状态。

hosw 0109

图 1-9. 单元测试通用结构
提示

我们可以与其他库或实用工具结合使用单元测试框架来实现任何测试类型。例如,如 第二章 所述,我们使用 JUnit 和 TestNG 嵌入调用 Selenium WebDriver API,为 Web 应用实现端到端测试。

设置和拆卸阶段在单元测试用例中是可选的。虽然它不是严格强制的,但强烈建议进行验证。即使单元测试框架包括实现断言的能力,通常也会集成第三方 断言库。这些库旨在通过提供丰富的流畅断言集来改善测试代码的可读性。此外,这些库还提供增强的错误消息,帮助测试人员理解失败的原因。Java 的一些最相关的断言库总结在 表 1-10 中。

表 1-10. Java 的断言库

名称 描述 许可证 维护者 网站
AssertJ Java 的流畅断言库 Apache 2.0 AssertJ 团队 https://assertj.github.io/doc
Hamcrest 创建灵活断言的 Java 匹配器库 BSD Hamcrest 团队 http://hamcrest.org
Truth 用于 Java 和 Android 的流畅断言 Apache 2.0 Google https://truth.dev

如图 1-9 所示(见 图 1-9),SUT 通常可以查询另一个组件,称为 依赖组件(DOC)。在某些情况下(例如单元或系统测试级别),我们可能希望将 SUT 与 DOC(们)隔离开来。我们可以找到各种模拟库来实现这种隔离。

表 1-11 显示了 Java 的一些模拟库的综合摘要。

表 1-11. Java 的模拟库

名称 级别 描述 许可证 维护人员 网站
EasyMock 单元 允许使用 Java 注释对对象进行单元测试的模拟对象 Apache EasyMock 团队 https://easymock.org
Mockito 单元 用于模拟创建和验证的 Java 模拟库 MIT Mockito 团队 https://site.mockito.org
JMockit 集成 允许 Java EE 和基于 Spring 的应用进行容器外集成测试 开源 JMockit 团队 https://jmockit.github.io
MockServer 系统 用于通过 HTTP 或 HTTPS 与 Java 客户端集成的任何系统的模拟库 Apache 2.0 James Bloom https://www.mock-server.com
WireMock 系统 用于模拟基于 HTTP 的服务的工具 Apache 2.0 Tom Akehurst https://wiremock.org

我们在本节分析的最后一类测试工具是 BDD,这是一种创建验收测试的开发过程。实现这种方法有很多替代方案。例如,表 1-12 显示了相关 BDD 框架的简要摘要。

表 1-12. BDD 框架

名称 语言 描述 许可证 维护人员 网站
Cucumber Ruby, Java, JavaScript, Python 用于创建遵循 BDD 方法的自动验收测试的测试框架 MIT SmartBear Software https://cucumber.io
FitNesse Java 独立的协作 wiki 和验收测试框架 CPL FitNesse 团队 http://fitnesse.org
JBehave Java, Groovy, Kotlin, Ruby, Scala 适用于所有 JVM 语言的 BDD 框架 BSD-3-Clause JBehave 团队 https://jbehave.org
Jasmine JavaScript 用于 JavaScript 的 BDD 框架 MIT Jasmine 团队 https://jasmine.github.io
Capybara Ruby 模拟用户故事场景的基于 Web 的验收测试框架 MIT Thomas Walpole https://teamcapybara.github.io/capybara
Serenity BDD Java, Javascript 自动验收测试库 Apache 2.0 Serenity BDD 团队 https://serenity-bdd.info

总结与展望

自 2004 年起,Selenium 已经发展了很长一段路程。许多从业者认为它是开发 Web 应用端到端测试的事实标准解决方案,并被全球数千个项目使用。在本章中,您已经看到了 Selenium 项目的基础(由 WebDriver、Grid 和 IDE 组成)。此外,Selenium 拥有一个丰富的生态系统和活跃的社区。WebDriver 是 Selenium 项目的核心,它是一个提供 API 以编程方式控制不同 Web 浏览器(例如 Chrome、Firefox、Edge 等)的库。Table 1-13 包含了 Selenium WebDriver 的主要和次要用途的全面概述。

表 1-13. Selenium WebDriver 的主要和次要用途

| | 主要 | 次要(其他用途) |
| --- | --- |
| 目的 | 自动化测试 | Web 页面抓取、基于 Web 的管理任务 |
| 测试级别 | 端到端测试 | 系统测试(模拟后端调用)验收测试(例如与 Cucumber 一起使用) |

| 测试类型 | 功能测试(确保预期行为)跨浏览器测试(不同 Web 浏览器的兼容性) |

回归测试(确保每次提交后的构建在 CI 中) | 非功能性测试(例如负载、安全性、可访问性或本地化) |

测试方法论 TLD(在系统可用时实施测试) BDD(在早期开发阶段定义用户场景)

在接下来的章节中,您将了解如何使用 Maven 或 Gradle 作为构建工具设置 Java 项目。该项目将包含用于 Web 应用的端到端测试,使用 JUnit 和 TestNG 作为单元测试框架,并调用 Selenium WebDriver API。此外,您将学习如何使用基本测试案例(Selenium WebDriver 版本的经典hello world)来控制不同的 Web 浏览器(例如 Chrome、Firefox 或 Edge)。

第二章:测试准备

这一章旨在使用 Selenium WebDriver 和 Java 语言实现你的第一个端到端测试。为此,我们首先回顾了技术要求,包括先前的知识、硬件和软件。其次,本章概述了设置包含 Selenium WebDriver 测试的 Java 项目的概述。您可以使用类似 Maven 或 Gradle 的构建工具来简化项目设置。最后,您将学习使用 Selenium WebDriver 实现基本的端到端测试,即hello world测试。我们将使用不同的 Web 浏览器(如 Chrome、Edge 或 Firefox)和单元测试框架(JUnit 和 TestNG)以多种风格实现此测试。请记住,本书中的每个代码示例都可以在开源 GitHub 存储库中找到。因此,您可以重用此存储库的内容和配置作为自己测试的基础。

要求

使用 Java 启动 Selenium WebDriver 的第一个要求是理解 Java 语言和面向对象编程。不必成为专家,但需要基本的知识。然后,您可以在任何主流操作系统上使用 Selenium WebDriver:Windows、Linux 或 macOS。因此,您可以选择您喜欢的计算机类型。原则上,在内存、CPU、硬盘等方面对硬件没有特定要求,因此任何中档计算机都可以胜任。

Java 虚拟机

接下来,您需要在计算机上安装 Java 虚拟机(JVM)。有两种类型的 JVM 分发版。第一种选择是 Java 运行时环境(JRE),其中包括 JVM 和 Java 标准 API。第二种选择是 Java 开发工具包(JDK),它是 JRE 加上 Java 的软件开发工具包(例如javac编译器和其他工具)。由于我们是在 Java 中开发,我建议使用 JDK(尽管一些集成开发环境也包含了 Java 的 SDK)。对于 Java 版本,我建议至少使用 JDK 8,因为它是在我写作时期通常受到许多 Java 项目支持的长期支持版本。

文本编辑器或集成开发环境(IDE)

要编写我们的 Java 测试,我们需要一个文本编辑器或 IDE。IDE 提供了优秀的开发体验,因为它们具有完整的环境(用于编码、运行、调试、自动完成等)。尽管如此,你可以使用任何你喜欢的文本编辑器,结合命令行工具(用于运行、调试等)来获得类似的实践。总体而言,选择使用哪种工具取决于个人偏好。一些流行的文本编辑器的替代方案包括 Sublime TextAtomNotepad++Vim 等。IDE 包括 EclipseIntelliJ IDEANetBeansVisual Studio Code

浏览器和驱动程序

使用 Selenium WebDriver 进行自动化的一个最初的方法是使用本地浏览器。我考虑在本书中使用以下浏览器:Chrome、Edge 和 Firefox。我称它们为 主要浏览器,原因有几点。首先,它们在全球范围内非常流行,因为我们使用 Selenium WebDriver 测试 Web 应用程序时,希望使用与潜在用户相同的浏览器。其次,这些浏览器是 常绿 的(即它们自动升级)。第三,这些浏览器适用于主要操作系统:Windows、Linux 和 macOS(不像 Safari,它也是一款流行的浏览器,但仅在 macOS 上可用)。最后,这些浏览器在 GitHub 仓库使用的持续集成(CI)环境中可用(即 GitHub Actions)。

控制 web 浏览器使用 Selenium WebDriver 的最后要求是驱动程序二进制文件:chromedriver(用于 Chrome)、msedgedriver(用于 Edge)和 geckodriver(用于 Firefox)。如 第一章 所述,驱动程序管理涉及三个步骤:下载、设置和维护。为了避免该章节中解释的潜在问题,我强烈建议使用 WebDriverManager 自动化此过程。

提示

附录 B 提供了由 WebDriverManager 执行的自动驱动程序管理过程的详细信息。此外,以防出于某些原因你需要手动执行驱动程序管理,本附录也解释了如何进行。

构建工具

另一个重要组成部分是 构建工具。构建工具是用于从源代码自动创建可执行应用程序的软件实用程序。这些工具在依赖管理、编译、打包、测试执行和部署方面简化了项目管理。总体而言,构建工具是自动化软件项目开发的便捷方式,无论是在构建服务器(如 GitHub Actions)还是开发者机器上。因此,我强烈建议使用构建工具来设置项目。本书中涵盖的替代方案包括:

Maven

一个由 Apache 软件基金会维护的开源构建自动化工具。它主要用于 Java 项目,尽管也支持其他语言,如 C#、Ruby 或 Scala。

Gradle

另一个用于软件开发的开源构建自动化工具。它支持 Java 和其他语言,如 Kotlin、Groovy、Scala、C/C++ 或 JavaScript。

推荐的版本是 Maven 3+ 和 Gradle 6+。为了完整起见,在示例仓库中我同时使用了两种构建工具。再次强调,最终选择使用哪一种取决于你的偏好。

注意

如果打算使用 IDE 进行开发和运行测试,那么构建工具并非必需。不过,我建议你至少在计算机上安装其中一种工具,以复制通常在构建服务器上使用的环境(例如 Jenkins、GitHub Actions 等)。

可选软件

除了已经解释的软件外,还有一些其他程序可以帮助你更好地利用本书。首先,你可以使用 Git 进行源代码管理。由于本书中的测试示例可在 GitHub 上获得,你可以使用 Git 进行分支(或克隆)和更新此仓库。

第二个可选工具是 Docker。在本书中,我展示了如何使用 Docker 来执行容器化的浏览器(见 第六章)。因此,我强烈建议你在计算机上安装 Docker Engine(适用于 Linux、macOS 和 Windows 10)。

最后,如果需要,你可以使用不同的网页浏览器。除了主流浏览器(Chrome、Edge 和 Firefox)外,还可以使用其他浏览器与 Selenium WebDriver 结合使用,如 macOS 中的 Safari,或任何操作系统中的 OperaChromium,以及 HtmlUnit(一个无界面浏览器,即无 GUI 浏览器)。

项目设置

你可以在 GitHub 仓库 中找到本书的所有代码示例。该仓库是开源的,使用 Apache 2.0 许可发布。该仓库有多个目的。首先,将所有示例集中在一个站点中非常方便。其次,你可以使用它的设置(Maven 或 Gradle)作为你项目的骨架。

提示

下面的子节描述了创建包含 Selenium WebDriver 测试的 Java 项目的一般要求。附录 C 提供了关于示例仓库配置的低级细节。

项目布局

项目布局是用于存储软件项目的不同资产(例如源代码、二进制文件、静态资源等)的目录结构。Maven 和 Gradle 在 Java 项目中使用等效的布局。我们可以使用这一布局在两种构建工具中执行示例仓库,多亏了这一点。

如图 Figure 2-1 所示,以下一组文件夹(称为脚手架文件夹)在两个构建工具中完全相同:

src/main/java

应用程序源代码(即 Java 文件)

src/main/resources

应用程序资源文件(即属性、配置文件等)

src/test/java

测试源代码(即用于测试的 Java 文件)

src/test/resources

测试资源文件(即用于测试的附加资产)

hosw 0201

图 2-1. Maven 和 Gradle 中的项目布局

项目布局的其余部分在两个构建工具中不同。第一个区别是配置文件。一方面,该文件在 Maven 中是唯一的,称为pom.xml(项目对象模型)。另一方面,在 Gradle 中有两个文件用于配置,称为settings.gradlebuild.gradle。Maven 和 Gradle 之间的第二个区别是输出文件夹。在两种情况下,构建工具都创建了此文件夹以保存生成的构建(即编译后的类、生成的打包文件等)。该文件夹在 Maven 中称为target,在 Gradle 中称为build。最后,Gradle 包含一组文件夹和文件,用于所谓的 Gradle 包装器。这个包装器是一个脚本文件(对 Unix-like 系统称为gradlew,对 Windows 系统称为gradlew.bat),提供以下好处:

  • 在本地机器上构建项目而无需安装 Gradle

  • 需要使用给定版本(可以与本地安装的 Gradle 实例不同)

  • 通过更改包装器工件(在gradle/wrapper文件夹中)轻松升级到新版本

从版本 4 开始,Maven 采用了使用mvnw脚本的包装概念。

注意

本书范围不包括解释 Maven 和 Gradle 提供的所有功能。然而,您可以在附录 C 中找到有关它们的构建生命周期和典型命令的更多信息。如需进一步了解,请阅读官方的MavenGradle文档。

依赖项

软件项目的依赖项是所需的库或插件。构建工具除了其他功能外,还能够自动管理项目依赖关系。为此,我们需要在项目配置文件中指定这些依赖关系的坐标(请参阅 Maven 和 Gradle 的具体子部分以获取详细信息)。Java 项目的坐标是一组三个标签,唯一标识该项目(例如,库、插件等),即:

groupId

创建项目的组织、公司、个人等。

artifactId

用于标识项目的唯一名称。

version

项目的特定版本。默认情况下,建议您使用每个发布的最新版本。

本节解释了我在示例库中使用的 Java 依赖项。首先,当然,我们需要 Selenium WebDriver 来进行浏览器自动化。这个依赖项是唯一强制性的。然后,我建议使用额外的依赖项用于自动化驱动程序管理实用程序、单元测试框架、流畅断言和日志记录。本节的其余部分解释了每个实用程序的动机和基本使用方法。

Selenium WebDriver

Selenium WebDriver 最相关的概念之一是 WebDriver 层次结构,它是一组用于控制不同网页浏览器的类集合。正如您在图 2-2 中所见,该层次结构遵循面向对象的编程范式。在顶部,我们找到 WebDriver 接口,它是整个结构的父接口。层次结构的下部对应于驱动单个浏览器的 Java 类。例如,我们需要使用 ChromeDriver 类的实例来控制本地的 Chrome 浏览器。表 2-1 展示了 WebDriver 层次结构主要类及其对应的目标浏览器的全面摘要。

表 2-1. WebDriver 层次结构描述

浏览器

|

org.openqa.selenium.chrome

|

ChromeDriver
Chrome

|

org.openqa.selenium.edge

|

EdgeDriver
Edge

|

org.openqa.selenium.firefox

|

FirefoxDriver
Firefox

|

org.openqa.selenium.safari

|

SafariDriver
Safari

|

org.openqa.selenium.opera

|

OperaDriver
Opera

|

org.openqa.selenium.ie

|

InternetExplorerDriver
Internet Explorer

|

org.openqa.selenium.remote

|

RemoteWebDriver
远程浏览器(参见第六章)

hosw 0202

图 2-2. WebDriver 对象的层次结构

自动化驱动程序管理

在实例化 WebDriver 层次结构对象之前,解析相应的驱动程序是强制性的。例如,要使用 ChromeDriver 控制 Chrome,我们首先需要在本地机器上安装这个浏览器。其次,我们需要管理 chromedriver。为了避免手动驱动程序管理可能出现的问题(参见第一章),建议完全自动化驱动程序管理过程(下载、设置和维护)。关于 Java,推荐实现是 WebDriverManager,这是一个 Selenium WebDriver 辅助库,允许自动化驱动程序管理。本节解释了如何将 WebDriverManager 作为 Java 依赖项使用。

一旦我们的项目解析了 WebDriverManager 的依赖项(请参见附录 C 获取配置详细信息),我们就可以使用 WebDriverManager API 来管理驱动程序。该 API 提供了一组单例(称为管理器),用于下载、设置和维护驱动程序。这些单例可通过 WebDriverManager 类访问。例如,我们需要调用 chromedriver() 方法来管理 Chrome 所需的驱动程序,即 chromedriver,如下所示:

WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();

表格 2-2 总结了所有支持的浏览器的基本 WebDriverManager 调用。除了这些基本调用(即方法 setup())外,WebDriverManager 还公开了一个流畅的 API 用于高级配置。有关 WebDriverManager 方法论、配置能力及其他用途的更多详细信息,请参阅 附录 B,例如作为一个命令行接口工具(从 shell 中)、作为服务器(使用类似 REST 的 API)、作为代理(使用 Java 仪器化)或作为 Docker 容器。

表格 2-2. WebDriverManager 基本调用

WebDriverManager 基本调用 浏览器 驱动程序

|

WebDriverManager.chromedriver().setup();
Chrome chromedriver

|

WebDriverManager.edgedriver().setup();
Edge msedgedriver

|

WebDriverManager.firefoxdriver().setup();
Firefox geckodriver

|

WebDriverManager.operadriver().setup();
Opera operadriver

|

WebDriverManager.chromiumdriver().setup();
Chromium chromedriver

|

WebDriverManager.iedriver().setup();
Internet Explorer IEDriverServer

单元测试框架

如 第 1 章 中所解释的,单元测试框架是创建不同类型测试的基础。本书将教您如何使用 Selenium WebDriver 为 Web 应用程序实现端到端测试。因此,我建议将 Selenium WebDriver 调用嵌入到使用特定单元测试框架创建的测试中。我推荐的备选方案之一是:JUnit 4、JUnit 5(单独或与 Selenium-Jupiter 结合,后者是 Selenium WebDriver 的扩展)或 TestNG。以下子章节提供了有关这些替代方案的更多详细信息。我的建议是专注于您喜欢的单元测试框架和构建工具,以继续练习本书其余部分中呈现的示例。

JUnit 4

JUnit 是由 Erich Gamma 和 Kent Beck 于 1999 年创建的 Java 单元测试框架。它被认为是在 Java 中开发测试的事实标准框架。在 JUnit 中,测试 是一个用于测试的 Java 类中的方法。在 JUnit 4 中,Java 注解是开发 JUnit 测试的构建块。JUnit 4 的基本注解是 @Test,因为它允许标识包含测试逻辑的方法(即用于执行和验证软件的代码)。此外,还有其他注解用于标识用于设置(即测试前发生的事情)和拆卸(即测试后发生的事情)的方法。

  • @BeforeClass 在所有测试之前执行一次。

  • @Before 在每个测试之前执行。

  • @After 在每个测试之后执行。

  • @BeforeClass 在所有测试之前执行一次。

图 2-3 展示了 JUnit 4 的基本测试生命周期的图形表示。

hosw 0203

图 2-3. JUnit 4 测试生命周期

JUnit 5

由于 JUnit 4 存在一些限制(如单体架构或不可能组合 JUnit 运行器),JUnit 团队在 2017 年发布了一个新的主要版本(即 JUnit 5)。JUnit 在版本 5 中进行了完全重新设计,采用了由三个组件组成的模块化架构(见 Figure 2-4)。第一个组件是 JUnit Platform,是整个框架的基础。JUnit Platform 的目标是双重的:

  • 它通过 test launcher API 允许在 JVM 中发现和执行测试(顺序或并行)。这个 API 通常被构建工具和 IDE 等程序化客户端使用。

  • 它定义了在 JUnit 平台上运行测试的 test engine API。这个 API 通常被提供测试模型的框架所使用。

hosw 0204

Figure 2-4. JUnit 5 架构

多亏了测试引擎 API,第三方测试框架可以在 JUnit 5 之上执行测试。一些已实现用于 JUnit 5 的测试引擎的现有测试框架的示例包括 TestNGCucumberSpock。此外,JUnit 5 还提供了测试引擎 API 的两个开箱即用的实现。这些引擎是 JUnit 5 架构的其余两个组件,即

Vintage

提供与传统 JUnit 测试(即版本 3 和 4)的向后兼容的测试引擎。

Jupiter

提供一个新的编程和扩展模型的测试引擎

Jupiter 是 JUnit 5 的一个重要组成部分,它提供了全新的 API,使用强大的编程模型来开发测试。这个编程模型的一些特性包括参数化测试、并行执行、标记和过滤、有序测试、重复和嵌套测试,以及丰富的禁用(忽略)测试的能力。

与 JUnit 4 类似,Jupiter 也使用 Java 注解来声明测试用例。例如,用于标识带有测试逻辑方法的注解仍然是 @Test。在 Jupiter 中,其他基本测试生命周期注解的名称与 JUnit 4 有些不同:@BeforeAll@BeforeEach@AfterEach@AfterAll。如你所见在 Figure 2-5 中,这些注解每一个都遵循了 JUnit 4 相同的工作流程。

hosw 0205

Figure 2-5. JUnit 5 测试生命周期

因此,使用 Selenium WebDriver 和 WebDriverManager 的 Jupiter 测试结构在 JUnit 4 和 JUnit 5 中非常相似。除了设置和拆卸注解名称的变化外,在 Jupiter 编程模型中,测试方法(及其生命周期)不需要在 JUnit 4 中是 public 的。

Tip

本书将教会你如何使用 Selenium WebDriver 进行端到端测试的 Jupiter 基础知识。请查看下一节中关于基于 JUnit 5 的完整测试的hello world示例。详细信息请查看 JUnit 5 文档

JUnit 5 与 Selenium-Jupiter

Jupiter 的扩展模型允许向默认编程模型添加自定义功能。为此,Jupiter 提供了一个 API,开发人员可以扩展(使用称为扩展点的接口)以提供自定义功能。这些扩展点的类别包括:

测试生命周期回调

在测试生命周期的不同时刻包含自定义逻辑

参数解析

实现依赖注入(例如,在测试方法或构造函数中注入参数)

测试模板

根据给定的上下文重复测试

条件测试执行

根据自定义条件启用或禁用测试

异常处理

在测试及其生命周期中管理 Java 异常

测试实例

创建和处理测试类实例

拦截调用

拦截调用测试代码(并决定这些调用是否继续)

作为 Jupiter 开发者,你可以实现自定义扩展或使用现有的扩展。表 2-3 展示了一些 Jupiter 扩展的示例。

表 2-3. Jupiter 扩展

名称 描述 许可证 维护者 网站
JUnit Pioneer Jupiter 的扩展包 EPL 2.0 JUnit Pioneer 团队 https://junit-pioneer.org
rerunner-jupiter 用于重新运行失败的 Jupiter 测试的扩展 Apache 2.0 Artem Sokovets https://github.com/artsok/rerunner-jupiter
MockitoExtension Jupiter 扩展,用于初始化模拟对象和处理存根 MIT Mockito 团队 https://github.com/mockito/mockito
QuickPerf 用于评估一些性能相关属性的库 Apache 2.0 QuickPerf 团队 https://github.com/quick-perf/quickperf
Selenium-Jupiter Selenium WebDriver 的 Jupiter 扩展 Apache 2.0 Boni García https://bonigarcia.dev/selenium-jupiter
SpringExtension Spring 框架的 Jupiter 扩展 Apache 2.0 Pivotal Software https://spring.io/projects/spring-framework

在本书的背景下,Selenium-Jupiter 是一个非常有吸引力的选择,因为它可以无缝地在 Jupiter 测试中使用 Selenium WebDriver。Selenium-Jupiter 的基础如下(请参见下一节关于基于 Selenium-Jupiter 的hello world测试):

减少测试用例中的样板代码

由于 Jupiter 编程模型提供的参数解析功能,Selenium-Jupiter 允许声明 WebDriver 层次结构的对象(例如,ChromeDriverFirefoxDriver 等)以控制来自测试的 Web 浏览器作为构造函数或测试参数。

通过 WebDriverManager 自动化驱动程序管理

由于扩展模型提供的测试生命周期回调,对于 Selenium-Jupiter 用户来说,使用 WebDriverManager 是完全透明的。

高级端到端测试功能

例如,这包括与 Docker 的无缝集成、测试模板(用于跨浏览器测试)或故障排除和监控功能(例如,会话录制或可配置截图)。

TestNG

本书中我使用的最后一个单元测试框架是 TestNG。 TestNG 提供的一些更显著的特性包括并行测试执行、测试优先级、使用自定义注解进行数据驱动测试以及创建详细的 HTML 报告。

与 JUnit 4 和 Jupiter 一样,TestNG 也使用 Java 注解声明测试及其生命周期(即每个测试之前和之后发生的事情)。 再次,注解 @Test 用于指定测试方法。 然后,它提供了注解 @BeforeClass@BeforeMethod 来指定测试设置,并使用 @AfterMethod@AfterClass 进行拆卸(参见 图 2-6)。 另外,TestNG 允许使用以下术语对包含在 Java 类中的测试进行分组:

  • Suite 包含一个或多个 tests

  • Test 包含一个或多个 classes

  • Class 是一个带有测试方法的 Java 类,例如,用 @Test 注解。

按照这种表示法,并如 图 2-6 所示,TestNG 提供了额外的注解来在套件和测试之前和之后执行自定义逻辑。

hosw 0206

图 2-6. TestNG 测试生命周期

流畅断言

如 第一章 中介绍的,有不同的断言库。 这些库通常提供丰富的流畅断言和全面的错误消息。 在这些备选方案中,我在示例库中使用 AssertJ 库。 原因有两个。 首先,我们可以选择在 IDE 中使用静态方法 assertThat 后(通常在静态方法后按 Ctrl + 空格可用)快速断言数据的可用方法。 图 2-7 显示了使用 IDE(本例中为 Eclipse)检查此方法的示例。

hosw 0207

图 2-7. 使用 Eclipse 手动检查 AssertJ 中可用的断言方法

与其他选项相比,AssertJ 的第二个优势是它允许使用点符号进行断言链。 由此,我们可以连接几个条件以创建更可读的断言,例如:

assertThat(1 + 1).isGreaterThan(1).isLessThan(3);

记录

最后,我建议使用日志记录库来跟踪您的 Java 代码。如您所知,日志记录是程序员在软件执行时跟踪事件的一种简单方式。通常通过将文本消息写入文件或标准输出来执行日志记录,并且它允许您跟踪程序并诊断问题。今天,使用特定库来有效进行日志记录是很普遍的。这些库提供不同的好处,例如消息的粒度级别(例如调试,警告或错误),时间戳或配置能力。

Hello World

我们已经准备好将本章中解释的所有部分结合起来,实现我们的第一个端到端测试。正如您可能知道的那样,一个hello world程序是许多编程语言用来说明基本语法的简单代码片段。示例 2-1 展示了 Selenium WebDriver 版本的这个经典hello world

提示

以下示例使用 JUnit 5 作为单元测试框架来嵌入调用 Selenium WebDriver 的代码。请记住,您可以在示例存储库中找到其他版本(即 JUnit 4,带有 Selenium-Jupiter 的 JUnit 5 和 TestNG)。

示例 2-1。使用 Chrome 和 JUnit 5 的 Hello World
class HelloWorldChromeJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    private WebDriver driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeAll
    static void setupClass() {
        WebDriverManager.chromedriver().setup(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @BeforeEach
    void setup() {
        driver = new ChromeDriver(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

    @Test
    void test() {
        // Exercise
        String sutUrl = "https://bonigarcia.dev/selenium-webdriver-java/";
        driver.get(sutUrl); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        String title = driver.getTitle(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        log.debug("The title of {} is {}", sutUrl, title); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)

        // Verify
        assertThat(title).isEqualTo("Hands-On Selenium WebDriver with Java"); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    }

    @AfterEach
    void teardown() {
        driver.quit(); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
    }

}

1

我们使用WebDriver接口声明 Java 属性。我们在测试中使用这个变量来控制 Selenium WebDriver 的 Web 浏览器。

2

在这个类的所有测试的设置中(即仅执行一次),我们调用 WebDriverManager 来管理所需的驱动程序。在这个例子中,因为我们使用 Chrome 作为浏览器,所以我们需要解决 chromedriver。

3

在测试设置中(每个测试方法执行一次),我们实例化WebDriver对象来控制 Chrome。换句话说,我们创建了一个ChromeDriver类型的对象。

4

测试逻辑通过driver变量使用 Selenium WebDriver API。首先,测试执行 System Under Test(SUT)。为此,我们使用我们的webdriver变量的get()方法打开练习站点(在本例中代表 Chrome 浏览器)。

5

我们使用getTitle()方法获取网页标题。

6

出于调试目的,我们使用DEBUG级别记录该标题。

7

测试的最后部分包含一个 AssertJ 断言。在这种情况下,我们验证网页标题是否符合预期。

8

在每个测试结束时,我们需要关闭浏览器。为此,我们可以调用driver对象的quit()方法(有关如何关闭WebDriver对象的更多信息,请参见第三章)。

你可以以不同的方式执行此测试。我建议获取示例仓库的本地副本。你可以使用 GitHub 网站下载源代码的完整副本。或者,你可以使用 Git 在 shell 中克隆存储库,如下所示:

git clone https://github.com/bonigarcia/selenium-webdriver-java

接下来,你可以使用 Maven 或 Gradle(如附录 C 中所述)在 shell 中运行测试。此外,你还可以将克隆的 Maven/Gradle 项目导入到 IDE 中。IDE 提供了内置功能,可以从其 GUI 中执行测试。例如,图 2-8 展示了在 Eclipse 中执行前一个hello world测试的屏幕截图(在此情况下,使用命令 Run → Run As → JUnit Test)。请注意,在集成控制台(图片底部)中,第一行跟踪是由 WebDriverManager 解析的驱动器分辨率。然后,浏览器通过 chromedriver 启动,最后,我们可以看到测试跟踪(具体来说,是网页标题)。

hosw 0208

图 2-8. 在 Eclipse 中执行 Selenium WebDriver 的hello world的屏幕截图

使用 JUnit 4 和 TestNG 的hello world版本几乎与 JUnit 5 相同,但使用不同的测试生命周期注解(例如,JUnit 4 的@Before代替 JUnit 5 的@BeforeEach等)。关于 JUnit 5 加 Selenium-Jupiter,代码更加紧凑。示例 2-2 展示了这个hello world版本。正如你所看到的,无需声明设置和拆卸。我们只需要将想要的WebDriver对象声明为测试参数(在本例中为 FirefoxDriver),Selenium-Jupiter 会处理驱动程序管理(也包括 WebDriverManager)、对象实例化和浏览器处理。

示例 2-2. 使用 Firefox 和 Selenium-Jupiter 的 Hello world
@ExtendWith(SeleniumJupiter.class)
class HelloWorldFirefoxSelJupTest {

    @Test
    void test(FirefoxDriver driver) {
        // Same test logic than other "hello world" tests
    }

}

使用其他浏览器

除了本书中称为主要浏览器(即 Chrome、Edge 和 Firefox)之外,示例仓库还包含使用其他浏览器的hello world测试:Opera、Chromium、Safari 和 HtmlUnitDriver(用于 HtmlUnit 无头浏览器的 Selenium WebDriver 兼容驱动程序)。这些测试包含在此存储库的helloworld_otherbrowsers包中,与原始的hello world版本略有不同。例如,示例 2-3 展示了使用 Opera 的 JUnit 5 类设置的hello world测试。由于这个浏览器可能在运行测试的机器上不可用(例如,在 GitHub Actions 中不可用 Opera),我使用假设在运行时有条件地禁用测试。

示例 2-3. 使用 Opera 和 JUnit 5 的类设置
@BeforeAll
static void setupClass() {
    Optional<Path> browserPath = WebDriverManager.operadriver()
            .getBrowserPath(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assumeThat(browserPath).isPresent(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    WebDriverManager.operadriver().setup();
}

1

我们使用 WebDriverManager 来定位浏览器路径。

2

如果此路径不存在,则假设系统中未安装浏览器,因此测试被跳过(使用 AssertJ 假设)。

通常情况下,您可以在示例存储库中使用其他单元测试框架找到此测试。JUnit 5 和 TestNG 版本使用与前面代码片段相同的等效测试设置。然而,使用 JUnit 5 和 Selenium-Jupiter 时有所不同。正如您在 示例 2-4 中所见,Selenium-Jupiter 通过使用自定义注解(称为 EnabledIfBrowserAvailable)简化了依赖于浏览器可用性(本例中为 Safari)的测试假设逻辑。

示例 2-4. 使用 Safari 和 JUnit 5 加上 Selenium-Jupiter 的 Hello World
@EnabledIfBrowserAvailable(SAFARI)
@ExtendWith(SeleniumJupiter.class)
class HelloWorldSafariSelJupTest {

    @Test
    void test(SafariDriver driver) {
        // Same test logic than other "hello world" tests
    }

}

要使用 Selenium WebDriver 控制 Safari,我们需要手动配置 Safari 以授权远程自动化。为此,首先通过单击 Safari 菜单选项 Safari → 首选项 → 高级选项卡来显示开发菜单。然后,启用“显示开发菜单”复选框。之后,“开发”菜单应该会显示出来。最后,单击“允许远程自动化”选项(参见 图 2-9)。

hosw 0209

图 2-9. 在 macOS 上启用 Safari 远程自动化

总结与展望

本章提供了使用 Selenium WebDriver 和 Java 开发 Web 应用端到端测试的基础知识。您需要做出的第一个重要决定是决定在哪个单元测试框架中嵌入 Selenium WebDriver 调用以实现这些测试。为了多样性和完整性,本书提出了四个选项:JUnit 4、JUnit 5、JUnit 5 加上 Selenium-Jupiter 和 TestNG。它们在基本的 Selenium WebDriver 测试方面都是等效的。对于更高级的用法,第八章 将涵盖每个测试框架的特定功能,这些功能可能对 WebDriver 测试(例如,用于跨浏览器测试的参数化测试)很重要。另一个您应该考虑的决定是选择构建工具。在本书中,我提出了两个选项:Maven 和 Gradle。再次强调,对于标准的开发实践,这两者都是类似的。

本书的第二部分专注于 Selenium WebDriver API,并将在下一部分开始介绍。要开始学习,请参阅 第三章,该章节涵盖了 Selenium WebDriver API 的基本概念,包括 WebDriver 对象、Web 元素定位、用户模拟操作(键盘和鼠标动作)以及等待策略。和往常一样,本章将通过 GitHub 托管的代码示例进行指导。

第二部分。Selenium WebDriver API

Selenium WebDriver 是一个开源库,允许以编程方式控制 web 浏览器(例如 Chrome、Edge 或 Firefox 等),就像真实用户一样操作。它提供了一个跨浏览器的 API,您可以使用它来为 web 应用程序实施端到端的测试。本书的这一部分详细总结了 Selenium WebDriver API。接下来的章节旨在非常实用。因此,我将使用 GitHub 上示例仓库中可用的现成测试来解释 Selenium WebDriver API 的每个特性。

第三章:WebDriver 基础知识

本章介绍了 Selenium WebDriver API 的基本方面。为此,我们首先回顾了创建WebDriver层次结构实例的不同方法(例如,ChromeDriverEdgeDriverFirefoxDriver等)。此外,我们还探讨了这些对象中可用的主要方法。其中,在网页中定位不同元素至关重要。因此,您将了解可能的定位器,即在网页中查找元素的策略(在 Selenium WebDriver API 中称为WebElement),例如按标签名称,链接文本,HTML 属性(标识符,名称或类),CSS 选择器或 XPath。本章还涵盖了 Selenium WebDriver API 的另一个关键方面,即模拟用户操作(即使用键盘和鼠标自动与网页进行交互)。本章的最后部分介绍了等待网页元素的能力。由于 Web 应用程序的动态和异步性质,此功能至关重要。

基本 WebDriver 使用

本节涵盖了与WebDriver对象相关的三个基本方面。首先,我们回顾了创建它们的不同方法。其次,我们研究了它们的基本操作。最后,我们分析了处理这些对象的不同方式(通常在测试结束时,用于关闭浏览器)。

WebDriver 创建

正如在第二章中介绍的,要在 Java 中使用 Selenium WebDriver 控制浏览器,第一步是创建WebDriver实例。因此,在使用 Chrome 时,我们需要创建一个ChromeDriver对象,在 Edge 时需要使用EdgeDriver,在 Firefox 时需要使用FirefoxDriver等等。创建这些类型实例的基本方法是在 Java 中使用new运算符。例如,我们可以按照以下方式创建一个ChromeDriver对象:

WebDriver driver = new ChromeDriver();

使用new运算符创建WebDriver实例是完全正确的,您可以在测试中使用它。然而,值得审查其他可能性,因为根据创建这些对象的特定用例,这些替代方案可能提供额外的好处。这些替代方案是 WebDriver 和 WebDriverManager 构建器。

WebDriver 构建器

Selenium WebDriver API 提供了一个遵循构建者模式的内置方法,用于创建WebDriver实例。通过RemoteWebDriver类的静态方法builder()可以访问此功能,并提供一个流畅的 API 来创建WebDriver对象。表 3-1 介绍了此构建器的可用方法。示例 3-1 展示了使用 WebDriver 构建器的测试框架。

表 3-1. WebDriver 构建器方法

方法 描述

|

oneOf(Capabilities options)
特定于浏览器的功能

|

addAlternative(Capabilities options)
可选的特定于浏览器的功能(参见第五章)

|

addMetadata(String key, Object value)
添加自定义元数据,通常用于在云提供商中请求额外功能(请参阅第六章)

|

setCapability(String capabilityName,
    Object value)
各个浏览器特定的能力(见第五章)

|

address(String uri)
address(URL url)
address(URI uri)
设置远程服务器的地址(见第六章)

|

config(ClientConfig config)
在使用远程服务器时的特定配置,如连接超时或代理设置

|

withDriverService(DriverService service)
本地驱动器的特定配置(例如,chromedriver 的文件位置、使用的端口、超时或参数)

|

build()
建造者模式中的最后一个方法,用于创建WebDriver实例
Tip

第五章解释了关于浏览器特定能力(例如ChromeOptions)的详细信息。在这一点上,我们仅使用这些类来选择浏览器类型(例如,Chrome 的ChromeOptions,Edge 的EdgeOptions,或 Firefox 的FirefoxOptions)。

示例 3-1. 使用 WebDriver 建造者建立的测试框架
class WebDriverBuilderJupiterTest {

    WebDriver driver;

    @BeforeAll
    static void setupClass() {
        WebDriverManager.chromedriver().setup(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @BeforeEach
    void setup() {
        driver = RemoteWebDriver.builder().oneOf(new ChromeOptions()).build(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void test() {
        // TODO: use variable "driver" to call the Selenium WebDriver API
    }

}

1

通常,在实际WebDriver实例化之前,我们使用 WebDriverManager 解析所需的驱动程序(例如此示例中的 chromedriver)。

2

我们使用 WebDriver 建造者创建WebDriver实例。由于在这个测试中我们想要使用 Chrome,因此我们使用一个ChromeOptions对象作为 capabilities 参数(使用oneOf()方法)。

从功能角度来看,这个例子与第二章中呈现的常规hello world测试的工作方式相同。然而,WebDriver 建造者 API 可以轻松地允许指定不同的行为。考虑以下代码片段作为示例。此代码更改设置方法并创建一个SafariDriver实例。假设在这种情况下(通常情况下,当测试未在 macOS 上执行时,因此系统中不可用 Safari 时),我们使用 Chrome 作为替代浏览器。

@BeforeEach
void setup() {
    driver = RemoteWebDriver.builder().oneOf(new SafariOptions())
            .addAlternative(new ChromeOptions()).build();
}

WebDriverManager 建造者

另一个创建WebDriver对象的可能性是使用 WebDriverManager。除了解决驱动程序外,从版本 5 开始,WebDriverManager 还提供了WebDriver建造者实用程序。示例 3-2 展示了使用这个建造者的测试框架。

示例 3-2. 使用 WebDriverManager 建立的测试框架
class WdmBuilderJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void test() {
        // TODO: use variable "driver" to call the Selenium WebDriver API
    }

}

1

WebDriverManager 解决所需的驱动程序(在本例中为 chromedriver)并在单行代码中创建正确的WebDriver类型的实例(在本例中为ChromeDriver)。

此方法具有不同的优势。首先,它可以减少冗长的测试,因为驱动程序的分辨率和WebDriver的实例化是同时进行的。其次,它允许通过选择特定的管理器(如chromedriver()firefoxdriver()等)简单地指定浏览器类型(即 Chrome、Firefox 等)。此外,我们可以轻松地参数化选择管理器以创建跨浏览器测试(详见第八章)。最后,WebDriverManager 允许您指定特定于浏览器的功能(详见第五章),并轻松地在 Docker 容器中使用浏览器(详见第六章)。

WebDriverManager 通过这种方法保留了创建的WebDriver对象的引用。此外,它启动了一个关闭挂钩以监视WebDriver实例的正确处置。如果在 JVM 关闭时仍然有活动的WebDriver会话,WebDriverManager 会退出这些浏览器。您可以通过在示例中删除teardown()方法来尝试此功能。

注意

虽然 WebDriverManager 会自动退出WebDriver对象,但我建议您在每个测试中显式执行此操作。否则,在执行测试套件的典型情况下,所有浏览器将保持打开状态,直到测试套件执行结束。

WebDriver 方法

WebDriver接口提供了一组方法,这些方法是 Selenium WebDriver API 的基础。表 3-2 总结了这些方法。示例 3-3 展示了使用其中多个方法进行基本测试的示例。

表 3-2. WebDriver 方法

方法 返回 描述

|

get(String url)

|

void
在当前浏览器中加载一个网页。

|

getCurrentUrl()

|

String
获取当前浏览器中加载的 URL。

|

getTitle()

|

String
获取当前网页的标题(<title> HTML 标签)。

|

findElement(By by)

|

WebElement
在当前网页中使用给定的定位器查找第一个WebElement。换句话说,如果有多个元素匹配定位器,则返回第一个元素(在文档对象模型[DOM]中)(详见“定位 WebElement”获取更多详细信息)。

|

findElements(By by)

|

List<WebElement>
在当前网页中使用给定的定位器查找每个WebElement(另请参见“定位 WebElement”)。

|

getPageSource()

|

String
获取当前网页的 HTML 源代码。

|

navigate()

|

Navigation
访问浏览器历史记录并导航至指定的网址(详见第四章)。

|

getWindowHandle()

|

String
获取当前浏览器中打开窗口的窗口句柄,即唯一标识符(另请参见第四章)。

|

getWindowHandles()

|

Set<String>
获取当前浏览器中当前打开的窗口句柄集合(另请参见第四章)。

|

switchTo()

|

TargetLocator
选择当前浏览器中的帧或窗口(另请参见第四章)。

|

manage()

|

Options
用于管理浏览器不同方面的通用实用程序(例如,浏览器大小和位置、Cookie、超时或日志)。

|

close()

|

void
关闭当前窗口,如果没有更多窗口打开,则退出浏览器。

|

quit()

|

void
关闭所有窗口并退出浏览器。
提示

从现在开始,我仅展示示例逻辑。这些测试使用在测试之前创建的WebDriver对象(在设置方法中),并在测试之后关闭(在拆卸方法中)。作为约定,本书中展示的是 JUnit 5 测试(尽管您也可以在示例存储库中找到 JUnit 4、Selenium-Jupiter 和 TestNG 的示例)。

示例 3-3. 测试使用 Selenium WebDriver API 的几种基本方法
@Test
void testBasicMethods() {
    String sutUrl = "https://bonigarcia.dev/selenium-webdriver-java/";
    driver.get(sutUrl); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    assertThat(driver.getTitle())
            .isEqualTo("Hands-On Selenium WebDriver with Java"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(driver.getCurrentUrl()).isEqualTo(sutUrl); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(driver.getPageSource()).containsIgnoringCase("</html>"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们打开实践网站。

2

我们验证页面标题是否符合预期。

3

我们确认当前的网址仍然相同。

4

我们检查页面的源 HTML 是否包含特定标记。

会话标识符

每次我们实例化一个WebDriver对象时,底层驱动程序(例如,chromedriver、geckodriver 等)会创建一个称为sessionId的唯一标识符来跟踪浏览器会话。我们可以在测试中使用这个值来唯一标识浏览器会话。为此,我们需要在驱动程序对象中调用getSessionId()方法。注意,这个方法在表格 3-2 中不可用,因为它属于RemoteWebDriver类。在实际应用中,我们用于控制浏览器的类型(例如ChromeDriverFirefoxDriver等)都继承自该类。因此,我们只需将WebDriver对象转换为RemoteWebDriver来调用getSessionId()方法。示例 3-4 展示了使用它的基本测试。

示例 3-4. 测试读取 sessionId
@Test
void testSessionId() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");

    SessionId sessionId = ((RemoteWebDriver) driver).getSessionId(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(sessionId).isNotNull(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    log.debug("The sessionId is {}", sessionId.toString()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们将驱动程序对象转换为RemoteWebDriver并读取其 sessionId。

2

我们验证 sessionId 具有某些值。

3

我们在标准输出上记录 sessionId。

WebDriver 释放

正如你在表格 3-2 中所看到的,有两种方法来处理WebDriver对象,分别是close()quit()。作为一般规则,我在示例中使用quit(),因为这个方法会关闭浏览器和所有相关的窗口。另一方面,close()方法仅终止当前窗口。因此,我仅在同一浏览器中处理不同窗口(或标签页)时,并且希望关闭一些窗口(或标签页)而仍然使用其他窗口(或标签页)时使用close()

定位 WebElements

Selenium WebDriver API 最重要的一个方面之一是能够与网页的不同元素进行交互。这些元素通过 Selenium WebDriver 使用 WebElement 接口进行处理,它是 HTML 元素的抽象表示。正如在 表格 3-2 中介绍的,有两种方法可以定位给定网页中的 WebElement。首先,findElement() 方法返回文档对象模型(DOM)中给定节点的第一个匹配项(如果有)。其次,findElements() 方法返回 DOM 节点列表。这两种方法都接受一个参数 By,指定定位策略。

文档对象模型(DOM)

DOM 是一个跨平台接口,允许以树结构表示 XML 类似文档(例如基于 HTML 的网页)。示例 3-5 展示了一个简单的网页;内存中对应的 DOM 树结构在 图 3-1 中表示。正如你所见,每个 HTML 标签(例如 <html><head><body><a> 等)在树中产生一个节点(或元素)。然后,每个标准 HTML 属性(例如 charsethref 等)在结果树中产生一个等效的 DOM 属性。此外,HTML 标签的文本内容在生成的树中也可用。像 JavaScript 这样的语言使用 DOM 方法访问和修改树结构。多亏了这一点,网页可以根据用户事件动态更改其布局和内容。

示例 3-5. 基本网页
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>DOM example</title>
</head>
<body>
  <h1>Heading text</h1>
  <a href="#">Link text</a>
</body>
</html>

hosw 0301

图 3-1. 从 示例 3-5 生成的 DOM 结构

WebElement 方法

表格 3-3 包含了 WebElement 类中可用方法的摘要。你将在本节的后续部分找到每个方法的示例。

表格 3-3. WebElement 方法

方法 返回 描述

|

click()

|

void
执行鼠标点击(即左键单击)当前元素。

|

submit()

|

void
发送网页表单(当前元素为表单时)。

|

sendKeys(CharSequence... keys)

|

void
模拟使用键盘输入(例如在输入文本元素中)。

|

clear()

|

void
重置输入文本元素的值。

|

getTagName()

|

String
获取元素的标签名称。

|

getDomProperty(String name)

|

String
获取 DOM 属性的值。

|

getDomAttribute(String name)

|

String
获取元素在其 HTML 标记中声明的属性值。

|

getAttribute(String name)

|

String
获取给定 HTML 属性(例如 class)的值作为 String。更准确地说,此方法尝试获取具有给定名称的 DOM 属性的有意义值(如果存在)。例如,对于布尔属性(例如 readonly),如果存在则返回 true,否则返回 null

|

getAriaRole()

|

String
获取元素在 W3C WAI-ARIA 规范中定义的角色。

|

getAccessibleName()

|

String
获取由 WAI-ARIA 定义的元素可访问名称。

|

isSelected()

|

boolean
判断复选框、选择框中的选项或单选按钮是否已选中。

|

isEnabled()

|

boolean
判断元素是否启用(例如表单字段)。

|

isDisplayed()

|

boolean
判断元素是否可见。

|

getText()

|

String
获取元素的可见文本,包括其子元素(如果有)。

|

getLocation()

|

Point
获取呈现元素左上角的位置(xy 坐标)。

|

getSize()

|

Dimension
获取呈现元素的宽度和高度。

|

getRect()

|

Rectangle
获取呈现元素的位置和大小。

|

getCssValue(String propName)

|

String
获取元素的 CSS 属性值。

|

getShadowRoot()

|

SearchContext
获取影子根以在影子树中进行搜索(参见“影子 DOM”)。

|

findElements(By by)

|

List<WebElement>
查找当前元素中匹配定位器的所有子元素。

|

findElement(By by)

|

WebElement
查找当前元素中匹配定位器的第一个子元素。

定位策略

Selenium WebDriver 提供了八种基本的定位策略,总结在表 3-4 中。 此外,如下一节所述,还有其他高级定位策略,即复合定位器和相对定位器。

我们使用 Selenium WebDriver API 中的类By指定基本定位器。 下面的子节展示了所有这些策略的示例。 我们使用实践网页表单来达到这个目的。 图 3-2 显示了此表单的截图。

表 3-4. Selenium WebDriver 中定位策略的摘要

定位器 根据定位器查找元素
标签名 HTML 标签的名称(例如 apdivimg 等)。
链接文本 链接显示的确切文本值(即 a HTML 标签)。
部分链接文本 链接中包含的文本(即 a HTML 标签)。
名称 属性name的值。
Id 属性id的值。
类名 属性class的值。
CSS 选择器 遵循W3C Selectors建议的模式。 CSS 模式的原始目的是选择网页中的元素以应用 CSS 样式。 Selenium WebDriver 允许重用这些 CSS 选择器来查找并与网页元素交互。
XPath 使用XPath(XML Path Language)语言进行查询。XPath 是 W3C 标准的查询语言,用于从类似 XML 的文档(如网页)中选择节点。

hosw 0302

Figure 3-2. 在定位器示例中使用的实践网页表单

通过 HTML 标签名定位

在查找网页元素的最基本策略之一是通过标签名。 示例 3-6 展示了使用此策略的测试。 此测试定位了实践网页表单中可用的文本区域,其 HTML 标记如下:

<textarea class="form-control" id="my-textarea" rows="3"></textarea>
示例 3-6. 使用标签名定位策略的测试
@Test
void testByTagName() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement textarea = driver.findElement(By.tagName("textarea")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(textarea.getDomAttribute("rows")).isEqualTo("3"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

1

我们使用定位器 By.tagName("textarea") 来找到此元素。 在这种情况下,由于这是网页上唯一声明的文本区域,我们可以确信 findElement() 方法将找到此元素。

2

我们确保属性rows的值与 HTML 标记中定义的相同。

通过 HTML 属性(名称、标识符、类名)定位

另一个直接的定位策略是通过 HTML 属性来找到 Web 元素,例如名称(name)、标识符(id)或类名(class)。考虑练习表单中提供的以下输入文本。请注意,它包括标准属性classnameid和非标准属性myprop(用于说明WebDriver方法之间的差异)。示例 3-7 展示了使用此策略的测试。

<input type="text" class="form-control" name="my-text" id="my-text-id"
    myprop="myvalue">
示例 3-7. 使用 HTML 属性(名称、标识符和类名)定位的测试
@Test
void testByHtmlAttributes() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    // By name
    WebElement textByName = driver.findElement(By.name("my-text")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(textByName.isEnabled()).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    // By id
    WebElement textById = driver.findElement(By.id("my-text-id")); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(textById.getAttribute("type")).isEqualTo("text"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(textById.getDomAttribute("type")).isEqualTo("text");
    assertThat(textById.getDomProperty("type")).isEqualTo("text");

    assertThat(textById.getAttribute("myprop")).isEqualTo("myvalue"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    assertThat(textById.getDomAttribute("myprop")).isEqualTo("myvalue");
    assertThat(textById.getDomProperty("myprop")).isNull();

    // By class name
    List<WebElement> byClassName = driver
            .findElements(By.className("form-control")); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    assertThat(byClassName.size()).isPositive(); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    assertThat(byClassName.get(0).getAttribute("name")).isEqualTo("my-text"); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

1

我们通过名称定位文本输入。

2

我们断言该元素已启用(即用户可以在其中输入)。

3

我们通过标识符找到相同的文本输入元素。

4

这个断言(以及接下来的两个)返回相同的值,因为属性type是标准的,并且如前所述,它在 DOM 中变为一个属性

5

这个断言(以及接下来的两个)返回不同的值,因为属性myprop不是标准的,因此在 DOM 中不可用。

6

我们通过类名定位一个元素列表。

7

我们验证列表有多于一个元素。

8

我们检查通过类名找到的第一个元素与之前定位的输入文本相同。

通过链接文本定位

最后一个基本的定位器是通过链接文本。这个策略有两个方面:精确定位和部分文本出现定位。我们使用练习表单中的一个链接来说明在以下 HTML 标记中使用此定位器。然后,示例 3-8 展示了使用这些定位器的测试。

<a href="./index.html">Return to index</a>
示例 3-8. 使用链接文本定位器的测试
@Test
void testByLinkText() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement linkByText = driver
            .findElement(By.linkText("Return to index")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(linkByText.getTagName()).isEqualTo("a"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(linkByText.getCssValue("cursor")).isEqualTo("pointer"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    WebElement linkByPartialText = driver
            .findElement(By.partialLinkText("index")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(linkByPartialText.getLocation())
            .isEqualTo(linkByText.getLocation()); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    assertThat(linkByPartialText.getRect()).isEqualTo(linkByText.getRect());
}

1

我们通过完整的链接文本来定位元素。

2

我们检查其标签名称为a

3

我们检查其 CSS 属性cursor是否为pointer(即通常用于可点击元素的样式)。

4

我们通过部分链接文本找到一个元素。这个链接与步骤 1 中的相同。

5

我们验证两个元素共享相同的位置和大小。

通过 CSS 选择器定位

到目前为止,我们看到的策略易于应用,但也有一些局限性。首先,通过标签名定位可能有些棘手,因为同一标签在网页上可能出现多次。接下来,通过 HTML 属性(如 name、id 或 class)查找元素是一种有限的方法,因为这些属性并非始终可用。此外,id 可能会在不同会话之间自动生成并且不稳定。最后,通过链接文本定位仅限于链接。为了克服这些限制,Selenium WebDriver 提供了两种强大的定位策略:CSS 选择器和 XPath。

创建 CSS 选择器有很多可能性。表 3-5 显示了基本 CSS 选择器的综合总结。

表 3-5. 基本 CSS 选择器

Category Syntax Description Example Example explanation
Universal * 选择所有元素 * 匹配所有元素
Type elementName 选择所有具有给定标签名的元素 input 匹配所有 <input> 元素
Class .classname 选择具有给定 class 属性的元素 .form-control 匹配所有类为 form-control 的元素
Id #id 选择具有给定id属性的元素 #my-text-id 匹配所有 id 为my-text-id的元素
Attribute [attr] 选择具有给定属性的元素 [target] 匹配所有具有 target 属性的元素
[attr=value] 选择具有给定属性和值的元素 [target=_blank] 匹配所有具有 target="_blank" 属性的元素
[attr~=value] 选择具有包含某个文本值的给定属性的元素 [title~=hands] 匹配所有title属性包含单词 hands 的元素
[attr&#124;=value] 选择具有等于或以某个值开始的给定属性的元素 [lang&#124;=en] 匹配所有等于或以 en 开头的元素
[attr^=value] 选择以某个值开头的给定属性的元素 a[href^="https"] 匹配所有href属性以 https 开头的链接
[attr$=value] | 选择以某个值结尾的给定属性的元素 | a[href$=".pdf"] 匹配所有href属性以 .pdf 结尾的链接
[attr*=value] 选择具有包含某些字符串的给定属性值的元素 a[href*="github"] 匹配所有href属性包含github的链接

下面的 HTML 摘录显示了实践中的隐藏输入文本,然后,示例 3-9 展示了使用 CSS 选择器定位此元素的可能方法。此定位器的优势在于即使在 HTML 标记中更改 name 属性,选择器仍然有效。

<input type="hidden" name="my-hidden">
示例 3-9. 使用基本的 CSS 选择器进行测试
    @Test
    void testByCssSelectorBasic() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

        WebElement hidden = driver
                .findElement(By.cssSelector("input[type=hidden]")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        assertThat(hidden.isDisplayed()).isFalse(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

1

我们使用 CSS 选择器来定位隐藏的输入。

2

我们检查隐藏字段是否可见。

有很多可能性来创建高级 CSS 选择器。表格 3-6 显示了一些摘要信息。CSS 选择器的完整参考资料可在官方W3C 推荐中找到。

表格 3-6. 高级 CSS 选择器

类别 语法 描述 示例 示例解释
分组 , 将两个(或多个)选择器分组 div, span 匹配 <span><div> 元素
组合器 (空格) 选择作为后代的元素 div span 匹配所有在 <div> 内的 <span> 元素
A > B 选择作为另一个元素的直接子元素 ul > li 匹配直接嵌套在 <ul> 内的所有 <li> 元素
A ~ B 选择共享同一父级的元素(即兄弟),并且第二个元素跟随第一个(不一定是立即的) p ~ span 匹配所有跟随 <p><span> 元素(无论是否立即)
A + B 兄弟元素,并且第二个元素紧跟在第一个后面 h2 + p 匹配紧跟在 <h2> 后面的所有 <p> 元素。
伪类 : 选择 CSS 的伪类(即所选元素的特殊状态) a:visited 匹配所有已访问链接
:nth-child(n) 根据在组中的位置选择元素(从开头开始) p:nth-child(2) 匹配每第二个 <p> 子元素
:not(selector) 选择不匹配给定选择器的元素 :not(p) 匹配除 <p> 外的所有元素
:nth-last-child(n) 根据在组中的位置选择元素(从结尾开始) p:nth-last-child(2) 匹配每第二个 <p> 子元素(从最后一个子元素开始计数)
:: 选择 CSS 的伪元素(即所选元素的特定部分) p::first-line 匹配所有 <p> 元素的第一行

考虑以下 HTML 片段(通常包含在实践网页表单中)。正如您所见,有几个复选框:其中一个被选中,另一个未选中。我们可以使用 Selenium WebDriver API 和 CSS 选择器确定哪个元素被选中。为此,示例 3-10 使用 CSS 伪类。

<input class="form-check-input" type="checkbox" name="my-check" id="my-check-1"
        checked>
<input class="form-check-input" type="checkbox" name="my-check" id="my-check-2">
示例 3-10. 使用 CSS 选择器进行高级定位测试
@Test
void testByCssSelectorAdvanced() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement checkbox1 = driver
            .findElement(By.cssSelector("[type=checkbox]:checked")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(checkbox1.getAttribute("id")).isEqualTo("my-checkbox-1"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(checkbox1.isSelected()).isTrue(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    WebElement checkbox2 = driver
            .findElement(By.cssSelector("[type=checkbox]:not(:checked)")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(checkbox2.getAttribute("id")).isEqualTo("my-checkbox-2"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    assertThat(checkbox2.isSelected()).isFalse(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们使用伪类 checked 来定位已点击的复选框。

2

我们检查元素 ID 是否符合预期。

3

我们确认所选项已被选中。

4

我们使用伪类 checked 和操作符 not 来定位默认复选框。

5

我们检查元素 ID 是否符合预期。

6

我们确认所选项未选中。

定位 XPath

XPath(XML 路径语言)是导航到类 XML 文档(如 HTML 页面)DOM 的强大方式。它包括两百多个内置函数,用于创建选择节点的高级查询。有两种类型的 XPath 查询。首先,绝对 查询使用斜杠符号(/)从根节点遍历 DOM。例如,考虑 示例 3-5 中的基本 HTML 页面,要使用此方法选择此页面中存在的链接元素,我们需要以下 XPath 查询:

/html/body/a

绝对 XPath 查询很容易创建,但它们有一个显著的不便之处:页面布局的任何最小更改都会导致使用此策略构建的定位器失败。因此,通常建议避免使用绝对 XPath。相反,相对 查询更加方便。

相对 XPath 查询的一般语法如下:

//tagname[@attribute='value']

示例 3-11 展示了使用 XPath 定位器选择实践网络中的隐藏字段的测试。

示例 3-11. 使用基本的 XPath 定位器进行测试
@Test
void testByXPathBasic() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement hidden = driver
            .findElement(By.xpath("//input[@type='hidden']")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(hidden.isDisplayed()).isFalse(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

1

我们在实践网络中定位隐藏字段。

2

我们确认此元素对用户不可见。

XPath 的真正威力来自其内置函数。 表 3-7 包含了一些最相关的 XPath 函数。您可以在 W3C XPath 推荐标准 中找到完整的 XPath 参考资料。

表 3-7. 相关 XPath 内置函数摘要

类别 语法 描述 示例 示例说明
属性 contains(@attr, 'string') 检查属性是否包含字符串 //a[contains(@href, 'github')] 匹配 href 包含 github 的链接
starts-with(@attr, 'string') 检查属性是否以字符串开头 //a[starts-with(@href, 'https')] 匹配所有使用 HTTPS 的链接
ends-with(@attr, 'string') 检查属性是否以字符串结尾 //a[ends-with(@href, *https*)] 匹配所有指向 PDF 文档的链接
文本 text()='string' 根据文本内容定位元素 //*[text()=*click*] 匹配所有文本为 click 的元素
子节点 [index] 定位子元素 //div/*[0] <div> 的第一个子元素
布尔 or 逻辑运算符 or //@type='submit' or @type='reset'] 匹配提交和清除表单的按钮
and 逻辑运算符 and //@type='submit' and @id ='my-button'] 匹配具有给定 id 的提交按钮
not() 逻辑运算符 not //@type='submit' and not(@id ='my-button')] 匹配与给定 id 不同的提交按钮
轴(用于定位相对节点) following::item 在当前节点之后的节点 //*[@type='text']//following::input 匹配第一个文本输入框后的所有输入框
descendant::item 选择当前节点的后代元素(子元素等) //*[@id='my-id']//descendant::a 匹配给定父节点下的所有后代链接
ancestor::item 选择当前节点的祖先元素(父元素等) //input[@id='my-id']//ancestor::label 匹配给定输入文本的所有前置标签
child::item 选择当前节点的子元素 //*[@id='my-id']//child::li 匹配给定节点下的所有列表元素
preceding::item 选择当前节点之前的所有节点 //*[@id='my-id']//preceding::input 匹配给定节点之前的所有input元素
following-sibling::item 选择当前节点之后的下一个节点 //*[@id='my-id']//following-sibling::input 匹配给定节点之后的下一个输入元素
parent::item 选择当前节点的父节点 //*[@id='my-id']//parent::div 匹配给定节点的父div元素

示例 3-12 展示了如何在练习网页表单中使用 XPath 定位器来操作单选按钮。这些单选按钮的 HTML 标记如下:

<input class="form-check-input" type="radio" name="my-radio" id="my-radio-1"
        checked>
<input class="form-check-input" type="radio" name="my-radio" id="my-radio-2">
示例 3-12. 使用高级 XPath 定位器进行测试
@Test
void testByXPathAdvanced() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement radio1 = driver
            .findElement(By.xpath("//*[@type='radio' and @checked]")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(radio1.getAttribute("id")).isEqualTo("my-radio-1"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(radio1.isSelected()).isTrue(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    WebElement radio2 = driver
            .findElement(By.xpath("//*[@type='radio' and not(@checked)]")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(radio2.getAttribute("id")).isEqualTo("my-radio-2"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    assertThat(radio2.isSelected()).isFalse(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们使用 XPath 来定位已选中的单选按钮。

2

我们检查元素 ID 是否符合预期。

3

我们确认所选项已被选中。

4

我们使用 XPath 来定位未选中的单选按钮。

5

我们检查元素 ID 是否符合预期。

6

我们确认所选项未被选中。

提示

“你应该使用什么策略?”对比了 CSS 选择器和 XPath,并提供了选择定位策略的一些提示。

在网页上查找定位器

如第 1-4 表所示,在第 1 章中,我们可以使用不同的工具来帮助生成我们 WebDriver 测试的定位器。本节展示了如何使用主流浏览器内置开发者工具的主要功能,例如基于 Chromium 的浏览器(如 Chrome 和 Edge)的Chrome DevTools,以及 Firefox 的Firefox Developer Tools

您可以通过右键单击要测试的网页界面部分,然后选择检查选项来打开这两个开发者工具。图 3-3 展示了 Chrome DevTools 的屏幕截图,通常位于浏览器底部(您可以根据需要移动它)。

开发者工具提供了在 Web 页面中定位元素的不同方式。首先,我们使用元素选择器,点击位于开发者工具面板左上角的图标(箭头覆盖一个方框)。然后,我们可以将鼠标移到页面上以突出显示每个 Web 元素,并在元素面板中检查它们的标记、属性等。

hosw 0303

图 3-3. 在导航练习网站时使用 Chrome DevTools

在同一视图中,我们可以通过右键单击元素并选择菜单选项“复制”,来使用工具复制其完整的 CSS 或 XPath 选择器。这种机制允许快速生成定位器的第一种方法,尽管我不建议直接使用这些定位器,因为它们往往比较脆弱(即与当前页面布局紧密相关)且难以阅读。

要创建稳健的 CSS 或 XPath 定位器,我们需要考虑我们正在处理的 Web 页面的特定特征,并根据这些知识创建自定义选择器。同样,开发者工具可以帮助我们完成这项任务。我们可以按下组合键 Ctrl + F 在 Chrome DevTools 中搜索字符串、CSS 选择器或 XPath。图 3-4 展示了此功能的实际示例。

请注意,我们正在使用练习的 Web 表单,并键入字符串 #my-text-id,它对应使用 CSS 选择器定位的元素。 DevTools 找到页面上的 Web 元素并将其高亮显示。

hosw 0304

图 3-4. 在 Chrome DevTools 中搜索 CSS 选择器

我们可以在 Firefox 中采用类似的方法。我们需要使用控制台面板并键入 $$("css-selector") 以搜索 CSS 选择器或 $x("xpath-query") 以进行 XPath 查询。图 3-5 展示了如何通过 id 使用 CSS 选择器和 XPath 查询来定位练习 Web 表单的第一个输入文本元素。

hosw 0305

图 3-5. 在 Firefox 开发者工具中搜索 CSS 选择器和 XPath

复合定位器

Selenium WebDriver API 拥有多个支持类,可以组合我们看到的不同定位器类型。这些类包括:

ByIdOrName(String idOrName)

它首先按 id 寻找,如果不可用,则按名称寻找。

ByChained(By... bys)

它按顺序寻找元素(即第二个应该出现在第一个内部,依此类推)。

ByAll(By... bys)

它按照一系列定位策略匹配元素(对这些定位器采用 逻辑条件)。

示例 3-13 展示了使用 ByIdOrName 的测试。该测试查找练习 Web 表单中可用的文件选择字段。请注意,该字段指定了 name 属性(但没有 id)。

<input class="form-control" type="file" name="my-file">
示例 3-13. 使用 id 或名称复合定位器的测试
@Test
void testByIdOrName() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement fileElement = driver.findElement(new ByIdOrName("my-file")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(fileElement.getAttribute("id")).isBlank(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(fileElement.getAttribute("name")).isNotBlank(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们使用 id 或名称来定位。

2

我们检查元素是否具有属性name

3

我们验证了同一元素中缺少属性name

示例 3-14 展示了两个测试案例,说明了ByChainedByAll复合定位器之间的差异。这两个定位器再次使用了实践网页表单。如果您检查其源代码,您将注意到在<form>内有三个单独的<div class="row">

示例 3-14. 使用链式定位器和全部复合定位器的测试
@Test
void testByChained() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    List<WebElement> rowsInForm = driver.findElements(
            new ByChained(By.tagName("form"), By.className("row"))); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assertThat(rowsInForm.size()).isEqualTo(1); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

@Test
void testByAll() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    List<WebElement> rowsInForm = driver.findElements(
            new ByAll(By.tagName("form"), By.className("row"))); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(rowsInForm.size()).isEqualTo(5); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们使用ByChained定位器。

2

我们找到一个元素,因为在表单内仅有一个row元素。

3

我们使用ByAll定位器。

4

我们找到了五个元素,因为定位器匹配了页面上一个<form>元素和四个<div class="row">元素。

相对定位器

Selenium WebDriver 版本 4 引入了一种新的在网页中查找元素的方法:相对定位器。这些新的定位器旨在找到相对于另一个已知元素的网页元素。这一功能基于 CSS 盒模型。该模型确定网页文档中的每个元素都使用矩形框进行呈现。图 3-6 展示了在实践表单中给定网页元素的盒模型示例。

hosw 0306

图 3-6. 展示网页元素盒模型的实践表单

利用这种盒模型,在 Selenium WebDriver API 中可用的相对定位器允许根据另一个网页元素的位置来查找元素。为此,首先我们需要使用标准的定位策略(例如按 id、名称、属性等)定位到该网页元素。然后,我们需要使用类RelativeLocator的静态方法with指定相对于原始网页元素的定位器类型。结果,我们得到一个RelativeBy对象,它扩展了标准定位策略中使用的抽象类ByRelativeBy对象提供以下方法来执行相对定位:

above()

找到位于原始元素顶部的元素。

below()

找到位于原始元素下方的元素。

near()

找到位于原始元素附近的元素。默认距离用于判断元素是否靠近另一个元素是一百像素。此定位器可以重载以指定另一个距离。

toLeftOf()

找到位于原始元素左侧的元素。

toRightOf()

找到位于原始元素右侧的元素。

示例 3-15 展示了使用相对定位器进行基本测试的案例。再次使用示例网页表单来说明这一特性。

示例 3-15. 使用相对定位器进行测试
@Test
void testRelativeLocators() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement link = driver.findElement(By.linkText("Return to index")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    RelativeBy relativeBy = RelativeLocator.with(By.tagName("input")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    WebElement readOnly = driver.findElement(relativeBy.above(link)); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(readOnly.getAttribute("name")).isEqualTo("my-readonly"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们定位文本为Return to index的链接。

2

我们指定相对定位器类型,将会是标签名为input的元素。

3

我们使用相对定位器来查找位于原始网页元素(即链接)上方的 Web 元素(应该是一个input字段)。

4

我们验证上面的参考链接是否是一个只读字段(参见图 3-2 以进行双重检查)。

警告

相对定位器可以帮助根据其他元素的相对位置找到元素。但另一方面,这种策略对页面布局非常敏感。例如,在响应式页面中使用相对定位器时需要特别小心,因为布局可能会根据视口的大小而变化。

一个具有挑战性的例子

到目前为止,我们看到的例子都相当简单。现在让我们来看一个更复杂的使用情况。练习网页中的一个非默认元素是日期选择器。顾名思义,该元素提供了一个方便的方法来使用 web GUI 选择日期。由于练习站点使用的 CSS 框架是Bootstrap,我使用bootstrap-datepicker实现了日期选择器。此日期选择器附加到一个输入字段上。当用户点击此字段时,一个日历会出现在网页上(参见图 3-7)。用户可以通过导航到不同的天、月和年来选择给定日期。

hosw 0307

图 3-7. 练习网页表单中的日期选择器

我们希望使用 Selenium WebDriver 实现一个自动化测试,通过与日期选择器 GUI 交互选择当前天和月,但选择前一年。示例 3-16 展示了实现结果。

提示

要跟随此示例,建议您在浏览器中打开练习网页表单(在代码示例中的 URL),并使用开发者工具检查日期选择器选择器的内部元素,注意使用的不同选择器策略。

示例 3-16. 与日期选择器交互的测试
@Test
void testDatePicker() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    // Get the current date from the system clock ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    LocalDate today = LocalDate.now();
    int currentYear = today.getYear();
    int currentDay = today.getDayOfMonth();

    // Click on the date picker to open the calendar ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    WebElement datePicker = driver.findElement(By.name("my-date"));
    datePicker.click();

    // Click on the current month by searching by text ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    WebElement monthElement = driver.findElement(By.xpath(
            String.format("//th[contains(text(),'%d')]", currentYear)));
    monthElement.click();

    // Click on the left arrow using relative locators ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    WebElement arrowLeft = driver.findElement(
            RelativeLocator.with(By.tagName("th")).toRightOf(monthElement));
    arrowLeft.click();

    // Click on the current month of that year ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    WebElement monthPastYear = driver.findElement(RelativeLocator
            .with(By.cssSelector("span[class$=focused]")).below(arrowLeft));
    monthPastYear.click();

    // Click on the present day in that month ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    WebElement dayElement = driver.findElement(By.xpath(String.format(
            "//td[@class='day' and contains(text(),'%d')]", currentDay)));
    dayElement.click();

    // Get the final date on the input text ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    String oneYearBack = datePicker.getAttribute("value");
    log.debug("Final date in date picker: {}", oneYearBack);

    // Assert that the expected date is equal to the one selected in the
    // date picker ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
    LocalDate previousYear = today.minusYears(1);
    DateTimeFormatter dateFormat = DateTimeFormatter
            .ofPattern("MM/dd/yyyy");
    String expectedDate = previousYear.format(dateFormat);
    log.debug("Expected date: {}", expectedDate);

    assertThat(oneYearBack).isEqualTo(expectedDate);
}

1

从系统时钟获取当前日期。我们使用标准的java.time API 来完成此操作。

2

点击日期选择器以打开日历。我们使用名称定位器(By.name("my-date"))。

3

通过搜索文本点击当前月份。我们使用 XPath 查询来定位此定位器。完成此步骤后,日期选择器 GUI 中会显示年份的其余月份。

4

使用相对定位器点击左箭头(即月份元素的右侧)。完成此步骤后,日历将移到前一年。

5

点击该年份中的当前月份。在这里,我们使用了 CSS 选择器。

6

点击该月份中的当前日期。在这一步中,我们使用 XPath 查询。点击后,日期被选中,并且其值出现在输入文本中。

7

获取输入文本中的最终日期。在这里,我们使用基本的属性定位器。

8

断言预期日期是否等于日期选择器中选择的日期。我们使用标准的 Java 计算预期日期,并像往常一样使用 AssertJ 进行断言。

您应该使用什么策略?

在本节中,我们回顾了 Selenium WebDriver API 允许在网页中定位元素的不同方法。这个主题是使用 Selenium WebDriver 进行浏览器自动化中最基本的例程之一。也许你正在问自己:我应该使用哪种最佳策略? 正如小说和电影《我,机器人》中的人物 Alfred Lanning 博士所说:“侦探,那才是正确的问题。” 在我看来,这是一个困难的问题,没有简单的答案。换句话说,对这个问题的答案可能是“取决于情况”。本节提供了几个提示,以便为常见用例识别合适的定位器策略。首先,表 3-8 比较了不同的定位策略。

表 3-8. 不同定位策略的优缺点和典型用例

定位器 优点 缺点 典型用例
通过属性(id、name、class) 使用简单 这些属性并不总是可用 定义这些属性的元素是不可变的(即,不会动态变化)
通过链接文本(完全或部分) 使用简单 仅适用于链接 适用于文本链接
通过标签名 使用简单 当页面上标签重复出现时,难以选择特定的一个元素 当标签是唯一的,或者结果 DOM 节点有固定的位置时
通过 CSS 选择器或 XPath 功能强大 编写健壮的选择器不容易 用于复杂的定位器
复合定位器 轻松组合现有的定位器 限于特定情况 当查找 id 或 name(ByIdOrName)时,查找嵌套元素(ByChained)时,以及同时使用多种策略(ByAll)时
相对定位器 人类语言方法 需要与其他定位器结合使用 基于已知元素的相对位置(上方、下方、附近等)查找元素

如您在此表中所见,CSS 选择器和 XPath 共享相同的优点、缺点和用例。这是否意味着这些策略相同?答案是否定的。两者都非常强大,并允许创建复杂的定位器。然而,它们之间存在着明显的区别。表 3-9 总结了这些差异。

表 3-9. XPath 和 CSS 选择器之间的一些差异

XPath CSS 选择器
XPath 允许双向定位,即遍历可以从父级到子级,反之亦然 CSS 允许单向定位,即遍历只能从父级到子级
XPath 在性能上较慢 CSS 比 XPath 更快
XPath 允许使用 text() 函数识别屏幕上的可见文本 CSS 不允许按其文本内容定位元素

为了更好地说明 XPath 和 CSS 选择器之间的区别,表 3-10 比较了使用这两种策略的特定定位器。

表 3-10. 比较 XPath 和 CSS 选择器的示例

定位器 XPath CSS 选择器
所有元素 //* *
所有 <div> 元素 //div div
通过 id 定位元素 //*[@id='my-id'] #my-id
类名定位 //*[contains(@class='my-class')] .my-class
带有属性的元素 //*[@attr] *[attr]
<div> 中查找文本 //div[text()='search-string'] 不可行
<div> 的第一个子元素 //div/*[1] div>*:first-child
所有带有链接子元素的 <div> //div[a] 不可行
<div> 下一个元素 //div/following-sibling::*[1] div + *
<div> 的前一个元素 //div/preceding-sibling::*[1] 不可行

总之,我们可以看到 XPath 提供了最通用的策略。然而,在某些情况下,CSS 选择器提供了更友好的语法(例如,通过 id 或类定位)和更好的通用性能。

键盘操作

如表 3-3 所介绍的,WebDriver 对象中的两个主要方法允许模拟键盘用户操作:sendKeys()clear()。示例 3-17 展示了使用这些方法的测试。

示例 3-17. 模拟键盘事件测试
@Test
void testSendKeys() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement inputText = driver.findElement(By.name("my-text")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String textValue = "Hello World!";
    inputText.sendKeys(textValue); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(inputText.getAttribute("value")).isEqualTo(textValue); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    inputText.clear(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(inputText.getAttribute("value")).isEmpty(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们使用实践网页表单来定位名为 my-text 的输入文本。

2

我们使用 sendKeys() 方法模拟键盘输入。

3

我们评估输入值是否符合预期。

4

我们使用 clear() 来重置它的内容。

5

我们评估输入值是否为空。

文件上传

在通过 Selenium WebDriver 与网页交互时,有几种用例需要模拟键盘动作。第一个是文件上传。用于 Web 应用程序上传文件的标准机制是使用带有type="file"<input>元素。例如,实践网页表单包含其中一个这样的元素:

<input class="form-control" type="file" name="my-file">

Selenium WebDriver API 不提供处理文件输入的机制。相反,我们应将用于上传文件的输入元素视为常规文本输入,因此需要模拟用户键入。特别是,我们需要输入要上传的绝对文件路径。示例 3-18 说明了如何操作。

示例 3-18. 测试上传文件
@Test
void testUploadFile() throws IOException {
    String initUrl = "https://bonigarcia.dev/selenium-webdriver-java/web-form.html";
    driver.get(initUrl);

    WebElement inputFile = driver.findElement(By.name("my-file")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    Path tempFile = Files.createTempFile("tempfiles", ".tmp"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    String filename = tempFile.toAbsolutePath().toString();
    log.debug("Using temporal file {} in file uploading", filename);
    inputFile.sendKeys(filename); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    driver.findElement(By.tagName("form")).submit(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(driver.getCurrentUrl()).isNotEqualTo(initUrl); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们使用按名称策略定位输入字段。

2

我们使用标准 Java 创建临时文件。

3

我们向输入字段键入其绝对路径。

4

我们提交表单。

5

我们验证结果页面(在action表单属性中定义)与初始网页不同。

提示

发送到输入文件的文件路径应对应于运行测试的机器上的现有存档。否则,测试将因InvalidArgumentException异常而失败。有关异常的更多详细信息,请参阅“WebDriver 异常”中的第五章。

当向远程浏览器上传文件(如第六章中所述)时,我们需要明确从本地文件系统加载文件。以下一行展示了如何指定本地文件检测器。

((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector());

范围滑块

类似的情况发生在<input type="range">表单字段上。这些元素允许用户使用图形滑块选择一个范围内的数字。你可以在实践的网页表单中找到一个例子:

<input type="range" class="form-range" name="my-range" min="0" max="10" step="1"
        value="5">

再次说明,Selenium WebDriver API 不提供处理这些字段的特定实用程序。我们可以通过模拟键盘动作与 Selenium WebDriver 互动来操作它们。示例 3-19 展示了与这些字段的测试交互。

示例 3-19. 使用表单滑块选择数字
@Test
void testSlider() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement slider = driver.findElement(By.name("my-range"));
    String initValue = slider.getAttribute("value");
    log.debug("The initial value of the slider is {}", initValue);

    for (int i = 0; i < 5; i++) {
        slider.sendKeys(Keys.ARROW_RIGHT); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    String endValue = slider.getAttribute("value");
    log.debug("The final value of the slider is {}", endValue);
    assertThat(initValue).isNotEqualTo(endValue); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

1

我们向实践网页表单中的范围字段发送键盘按键。我们使用 Selenium WebDriver API 中可用的Keys类来处理特殊键盘字符。特别是,我们向滑块发送右箭头键,结果它向右移动(即增加了范围内选择的数字)。

2

我们断言结果选择的值与原始位置的值不同。

鼠标操作

除了键盘外,与 Web 应用程序交互的另一个主要输入设备是计算机鼠标。首先,单击(也称为左键单击或简称为单击)通过 Selenium WebDriver API 使用click()方法来模拟,这是 Selenium WebDriver 中每个WebElement可用的方法之一。本节展示了使用此功能的两种典型用例:网页导航和与 Web 表单中的复选框和单选按钮的交互。

Selenium WebDriver 还允许使用称为Actions的辅助类来模拟其他常见的鼠标操作,如右键单击(也称为上下文单击)、双击、光标移动、拖放或悬停。最后,通过执行 JavaScript,可以在 WebDriver 中实现滚动。我将在“执行 JavaScript”中详细解释这一功能。

网页导航

示例 3-20 展示了使用 Selenium WebDriver 实现自动化网页导航的测试。该测试使用 XPath 定位链接并点击它们,调用click()方法。最后,它读取 Web 页面body的文本内容,并验证其包含预期字符串。

示例 3-20. 通过点击链接进行导航测试
@Test
void testNavigation() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");

    driver.findElement(By.xpath("//a[text()='Navigation']")).click();
    driver.findElement(By.xpath("//a[text()='Next']")).click();
    driver.findElement(By.xpath("//a[text()='3']")).click();
    driver.findElement(By.xpath("//a[text()='2']")).click();
    driver.findElement(By.xpath("//a[text()='Previous']")).click();

    String bodyText = driver.findElement(By.tagName("body")).getText();
    assertThat(bodyText).contains("Lorem ipsum");
}

复选框和单选按钮

示例 3-21 展示了使用click()方法操作复选框和单选按钮的另一种基本用法。为了验证点击操作后这些元素的预期状态,我们使用基于isSelected()方法的断言。

示例 3-21. 测试与复选框和单选按钮的交互
@Test
void testNavigation() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement checkbox2 = driver.findElement(By.id("my-checkbox-2"));
    checkbox2.click();
    assertThat(checkbox2.isSelected()).isTrue();

    WebElement radio2 = driver.findElement(By.id("my-radio-2"));
    radio2.click();
    assertThat(radio2.isSelected()).isTrue();
}

用户手势

Selenium WebDriver 提供了Actions类,这是一个强大的资产,用于自动化不同的用户操作,包括键盘和鼠标。该类遵循构建者模式。通过这种方式,您可以链式调用多个方法(即不同的操作),并在最后调用build()来执行所有操作。表 3-11 总结了该类中可用的公共方法。我们将通过下面的子节示例来回顾这些方法。

表 3-11. Actions 方法

方法 描述

|

keyDown(CharSequence key)
keyDown(WebElement target,
    CharSequence key)
在当前位置(或给定元素)发送单个按键(可以使用Keys类来输入特殊字符)。按键保持按下状态,直到调用keyUp()为止。

|

keyUp(CharSequence key)
keyUp(WebElement target,
    CharSequence key)
释放之前按下的按键keyDown()

|

sendKeys(CharSequence... keys)
sendKeys(WebElement target,
    CharSequence... keys)
在当前位置(或给定元素)发送按键序列。该方法与WebElement#sendKeys(CharSequence...)不同之处在于:1)修饰键(例如Keys.CONTROLKeys.SHIFT)不会被显式释放。2)没有重新聚焦到元素,因此Keys.TAB应该能够正常工作。

|

clickAndHold()
clickAndHold(WebElement target)
在不释放当前位置(或给定元素的中心)的情况下点击。

|

release()
release(WebElement target)
释放之前按下的左键鼠标按钮clickAndHold()

|

click()
click(WebElement target)
点击当前位置(或给定元素)。

|

doubleClick()
doubleClick(WebElement target)
双击当前位置(或元素)。

|

contextClick()
contextClick(WebElement target)
右键单击当前位置(或元素)。

|

moveToElement(WebElement target)
moveToElement(WebElement target,
    int xOffset, int yOffset)
将鼠标光标移动到中间(或移动到给定偏移量)的元素上。

|

moveByOffset(int xOffset,
    int yOffset)
将鼠标从当前位置(默认为0,0)按给定偏移量移动。

|

dragAndDrop(WebElement source,
    WebElement target)
dragAndDropBy(WebElement source,
    int xOffset, int yOffset)
dragAndDropBy(WebElement source,
    int xOffset, int yOffset)
此操作包括三个步骤:1)在源元素位置的中间(或按给定偏移量移动)点击并保持。2)将鼠标移动到目标元素位置。3)释放鼠标点击。

|

pause(long pause)
pause(Duration duration)
在操作链中执行暂停(以毫秒或使用 Java Duration)。

|

build()
生成包含所有先前操作的组合动作。

|

perform()
执行组合动作。

右键单击和双击

您可以在练习网站上找到一个使用三个下拉菜单的演示页面(参见 Figure 3-8)。在此页面上,单击其按钮时会出现第一个下拉菜单,第二个使用右键单击,第三个需要双击。Example 3-22 展示了使用此页面模拟用户手势的测试,通过 WebDriver 类的Actions

hosw 0308

图 3-8. 带有下拉菜单的实践网页
示例 3-22. 使用上下文和双击进行测试
@Test
void testContextAndDoubleClick() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/dropdown-menu.html");
    Actions actions = new Actions(driver);

    WebElement dropdown2 = driver.findElement(By.id("my-dropdown-2"));
    actions.contextClick(dropdown2).build().perform(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebElement contextMenu2 = driver.findElement(By.id("context-menu-2"));
    assertThat(contextMenu2.isDisplayed()).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebElement dropdown3 = driver.findElement(By.id("my-dropdown-3"));
    actions.doubleClick(dropdown3).build().perform(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    WebElement contextMenu3 = driver.findElement(By.id("context-menu-3"));
    assertThat(contextMenu3.isDisplayed()).isTrue(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们在中间下拉菜单中使用contextClick()

2

确认中间菜单显示正确。

3

在右侧下拉菜单中使用doubleClick()

4

确认右侧菜单显示正确。

鼠标悬停

第二个使用Actions处理的示例展示了一个实现了鼠标悬停的示例网页。该页面显示四个图像,当鼠标指针悬停在图像上时,每个图像下方显示一个文本标签。Example 3-23 包含一个使用此页面的测试。当鼠标悬停在第一张图片上时,Figure 3-9 展示了此页面。

示例 3-23. 使用鼠标悬停进行测试
@Test
void testMouseOver() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/mouse-over.html");
    Actions actions = new Actions(driver);

    List<String> imageList = Arrays.asList("compass", "calendar", "award",
            "landscape");
    for (String imageName : imageList) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        String xpath = String.format("//img[@src='img/%s.png']", imageName);
        WebElement image = driver.findElement(By.xpath(xpath)); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        actions.moveToElement(image).build().perform(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        WebElement caption = driver.findElement(
                RelativeLocator.with(By.tagName("div")).near(image)); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

        assertThat(caption.getText()).containsIgnoringCase(imageName); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    }
}

1

我们遍历一个字符串列表,定位页面上的四个图像。

2

我们使用 XPath 来查找每个<img>网页元素。

3

我们使用moveToElement()将鼠标指针移动到每个图像的中间。

4

我们使用相对定位器来查找显示的标签。

5

使用断言来验证文本是否符合预期。

hosw 0309

图 3-9. 带有鼠标悬停图像的实践网页

拖放

Example 3-24 演示了拖放的使用。此测试使用图示网页中的实践,显示在 Figure 3-10 中。

示例 3-24. 使用拖放进行测试
@Test
void testDragAndDrop() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/drag-and-drop.html");
    Actions actions = new Actions(driver);

    WebElement draggable = driver.findElement(By.id("draggable")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    int offset = 100;
    Point initLocation = draggable.getLocation();
    actions.dragAndDropBy(draggable, offset, 0)
            .dragAndDropBy(draggable, 0, offset)
            .dragAndDropBy(draggable, -offset, 0)
            .dragAndDropBy(draggable, 0, -offset).build().perform(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(initLocation).isEqualTo(draggable.getLocation()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    WebElement target = driver.findElement(By.id("target")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    actions.dragAndDrop(draggable, target).build().perform(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    assertThat(target.getLocation()).isEqualTo(draggable.getLocation()); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们定位可拖动元素。

2

我们使用 dragAndDropBy() 将该元素向右、向下、向左和向上各移动固定数量的像素(100)四次。

3

我们断言元素位置与开始时相同。

4

我们找到第二个元素(这次不可拖动)。

5

我们使用 dragAndDrop() 将可拖动的元素移动到第二个元素。

6

我们断言两个元素的位置相同。

hosw 0310

图 3-10. 带有可拖动元素的练习网页

点击并保持

下面的示例展示了复杂的用户手势,包括点击并保持。为此,我们练习使用 图 3-11 中的网页。

hosw 0311

图 3-11. 带有可绘制画布的练习网页

该页面使用名为 Signature Pad 的开源 JavaScript 库,使用鼠标在 HTML 画布上绘制签名。示例 3-25 展示了使用它的测试。

示例 3-25. 测试在画布上画一个圆
@Test
void testClickAndHold() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/draw-in-canvas.html");
    Actions actions = new Actions(driver);

    WebElement canvas = driver.findElement(By.tagName("canvas")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    actions.moveToElement(canvas).clickAndHold(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    int numPoints = 10;
    int radius = 30;
    for (int i = 0; i <= numPoints; i++) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        double angle = Math.toRadians(360 * i / numPoints);
        double x = Math.sin(angle) * radius;
        double y = Math.cos(angle) * radius;
        actions.moveByOffset((int) x, (int) y); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    actions.release(canvas).build().perform(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们通过标签名称定位画布。

2

我们将鼠标移动到此元素上,然后将动作 clickAndHold()(用于在画布上绘制)添加到动作流程中。

3

我们使用固定数量的点进行迭代,使用等式来找到圆周上的点。

4

我们使用周长点(xy)通过偏移量来移动鼠标(moveByOffset())。由于点击是从前一步持续的,所以结果的复合动作将在按住点击按钮的同时移动鼠标。

5

我们释放点击,构建动作并执行整个链。结果,画布上应该出现一个圆。

复制和粘贴

这个最后一个用户手势的示例自动化了一个广泛存在的用户动作:使用键盘进行复制和粘贴。在这里,我们使用可用于练习网站上的网络表单。示例 3-26 展示了一个模拟复制和粘贴的测试。

示例 3-26. 测试模仿复制和粘贴
@Test
void testCopyAndPaste() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
    Actions actions = new Actions(driver);

    WebElement inputText = driver.findElement(By.name("my-text")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebElement textarea = driver.findElement(By.name("my-textarea"));

    Keys modifier = SystemUtils.IS_OS_MAC ? Keys.COMMAND : Keys.CONTROL; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    actions.sendKeys(inputText, "hello world").keyDown(modifier)
            .sendKeys(inputText, "a").sendKeys(inputText, "c")
            .sendKeys(textarea, "v").build().perform(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    assertThat(inputText.getAttribute("value"))
            .isEqualTo(textarea.getAttribute("value")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们定位两个网页元素:一个输入文本和一个文本区域。

2

为了复制(在 Windows 和 Linux 中)或复制(在 macOS 中)使用组合 Ctrl + C,我们使用了在 Maven/Gradle 项目中以传递方式使用的开源库Apache Commons IO中可用的SystemUtils类。

3

我们实现了包括以下步骤的操作链:

  1. 将字符序列hello world发送到输入文本。

  2. 按下修饰键(Ctrl 或 Cmd,取决于操作系统)。请记住,此键将保持按下状态,直到我们明确释放它。

  3. 我们发送键a到输入文本。由于修饰符处于活动状态,因此产生的组合是 Ctrl + A(或 Cmd + A),结果是选择输入文本中的所有文本。

  4. 我们发送键c到输入文本。同样,由于修饰符处于活动状态,组合是 Ctrl + C(或 Cmd + C),并且输入文本被复制到剪贴板中。

  5. 我们向文本区域发送键v。这意味着发送 Ctrl + V(或 Cmd + V),并将剪贴板内容粘贴到文本区域中。

4

我们断言两个元素的内容(输入文本和文本区域)在文本结束时是相同的。

等待策略

Web 应用程序是客户端-服务器分布式服务,其中客户端是 Web 浏览器,而 Web 服务器通常是远程主机。中间网络延迟可能会影响 WebDriver 测试的可靠性。例如,在高延迟网络或服务器过载的情况下,缓慢的响应可能会对 WebDriver 测试的预期条件产生负面影响。此外,现代 Web 应用程序倾向于是动态和异步的。如今,JavaScript 允许使用回调、Promise 或 async/await 等不阻塞(即异步)操作。此外,我们还可以通过异步方式从其他服务器检索数据,例如使用 AJAX(异步 JavaScript 和 XML)或 REST(表述性状态转移)服务。

总之,在我们的 WebDriver 测试中,有机制暂停并等待特定条件是非常重要的。因此,Selenium WebDriver API 提供了不同的等待机制。三种主要的等待策略是隐式显式流畅 等待。以下子节将解释并展示示例。

警告

在 Java 中等待时,您可能考虑在代码中包含Thread.sleep()命令。一方面,这是一个简单的解决方案,但另一方面,它被认为是一种坏味道(即一种弱信号),可能会导致测试不可靠(因为延迟条件可能会改变)。总的来说,我强烈建议您不要使用它。而是考虑使用前述的等待策略。

隐式等待

Selenium WebDriver 提供的第一个等待策略称为隐式。此机制允许在查找元素时指定等待时间。默认情况下,此等待时间为零秒(即根本不等待)。但是当我们定义了隐式等待值时,Selenium WebDriver 会在尝试查找元素时轮询 DOM,等待指定的时间。轮询时间特定于驱动程序的实现,并且通常少于 500 毫秒。如果元素在经过的时间内出现,则脚本继续执行。否则,它会抛出异常。

示例 3-27 展示了这种策略。此测试使用一个练习页面(参见图 3-12),动态加载几张图片到 DOM 中。由于这些图片在页面加载之前不可用,我们需要等待这些图片可用。

hosw 0312

图 3-12. 练习网页加载图片
示例 3-27. 在“加载图片”页面使用隐式等待的测试
@Test
void testImplicitWait() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/loading-images.html");
    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebElement landscape = driver.findElement(By.id("landscape")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(landscape.getAttribute("src"))
            .containsIgnoringCase("landscape");
}

1

在与元素交互之前,我们指定了一个隐式等待策略。在这种情况下,我们设置了一个 10 秒的超时时间。

2

在接下来的调用中,我们像往常一样使用 Selenium WebDriver API。

提示

您可以尝试将测试中的隐式等待移除(步骤 1)。如果这样做,您会注意到测试由于NoSuchElementException在步骤 2 中失败。

虽然被 Selenium WebDriver API 支持,但隐式等待有一些不便之处需要知道。首先,隐式等待仅在查找元素时有效。其次,我们无法定制其行为,因为其实现是特定于驱动程序的。最后,由于隐式等待是全局应用的,通常检查网页元素的缺失会增加整个脚本的执行时间。因此,在大多数情况下,隐式等待通常被认为是不良实践,推荐使用显式和流畅等待代替。

显式等待

第二种等待策略称为显式,允许在特定条件发生之前最多暂停测试执行的一定时间。为了使用这种策略,我们需要创建一个WebDriverWait的实例,使用WebDriver对象作为第一个构造器参数,并使用Duration的实例作为第二个参数(用于指定超时时间)。

Selenium WebDriver 提供了一个全面的预期条件集合,使用ExpectedConditions类。这些条件非常易读,无需进一步解释即可理解其目的。我建议您在喜爱的 IDE 中使用自动完成功能来发现所有可能性。例如,图 3-13 展示了 Eclipse 中这个列表。

hosw 0313

图 3-13. Eclipse 中的 ExpectedConditions 类的自动完成

示例 3-28 展示了使用显式等待的测试。在这个示例中,我们使用presenceofElementLocated条件来等待直到练习网页上的一个图像可用。

示例 3-28. 在“加载图像”页面使用显式等待进行测试
@Test
void testExplicitWait() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/loading-images.html");
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebElement landscape = wait.until(ExpectedConditions
            .presenceOfElementLocated(By.id("landscape"))); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(landscape.getAttribute("src"))
            .containsIgnoringCase("landscape");
}

1

我们创建wait实例。在本例中,选择的超时时间为 10 秒。

2

我们通过在WebDriverWait对象中调用until()方法显式等待给定条件的出现(在本例中是特定元素的存在)。为了使语句更易读,您还可以静态导入这个预期条件(presenceOfElementLocated)。在本书中,我决定保留这些条件中的类名(ExpectedConditions),以便在 IDE 中使用自动完成功能时更容易理解,如前面所述。

示例 3-29 展示了另一个使用显式等待的测试。这个测试使用了另一个名为“慢速计算器”的练习网页,其中包含一个基本计算器的 GUI,调整为等待可配置时间以获取基本算术运算的结果(默认情况下为五秒)。图 3-14 展示了此页面的屏幕截图。

示例 3-29. 在“慢速计算器”页面使用显式等待进行测试
@Test
void testSlowCalculator() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/slow-calculator.html");

    // 1 + 3
    driver.findElement(By.xpath("//span[text()='1']")).click(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    driver.findElement(By.xpath("//span[text()='+']")).click();
    driver.findElement(By.xpath("//span[text()='3']")).click();
    driver.findElement(By.xpath("//span[text()='=']")).click();

    // ... should be 4, wait for it
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions.textToBe(By.className("screen"), "4")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

1

我们使用 XPath 定位器来点击对应于操作 1 + 3 的按钮。

2

由于测试应该等到结果准备就绪,我们显式等待这一点。在这种情况下,条件是具有类名screen的元素的文本等于 4。

hosw 0314

图 3-14. “慢速计算器”演示的实践网页

流畅等待

最后一种策略是流畅等待。这种机制是显式等待的一种泛化。换句话说,我们使用流畅等待来暂停测试,直到满足某些条件,而且流畅等待还提供了精细的配置能力。表 3-12 总结了FluentWait中可用的方法。顾名思义,这个类提供了流畅的 API,因此我们可以在同一行中链式调用多个调用。示例 3-30 展示了使用流畅等待的测试。

表 3-12. 流畅等待方法

方法 描述

|

withTimeout(Duration timeout)
使用 Java Duration 设置超时

|

pollingEvery(Duration interval)
条件评估频率(默认为五百毫秒)

|

withMessage(String message)
withMessage(Supplier<String> messageSupplier)
自定义错误消息

|

ignoring(Class<? extends Throwable> exceptionType)
ignoring(Class<? extends Throwable> firstType,
    Class<? extends Throwable> secondType)
ignoreAll(Collection<Class<? extends Throwable>>
    types)
在等待条件时忽略特定异常

|

until(Function<? super T, V> isTrue)
预期条件
示例 3-30. 使用流畅等待进行测试
@Test
void testFluentWait() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/loading-images.html");
    Wait<WebDriver> wait = new FluentWait<>(driver)
            .withTimeout(Duration.ofSeconds(10))
            .pollingEvery(Duration.ofSeconds(1))
            .ignoring(NoSuchElementException.class); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebElement landscape = wait.until(ExpectedConditions
            .presenceOfElementLocated(By.id("landscape")));
    assertThat(landscape.getAttribute("src"))
            .containsIgnoringCase("landscape");
}

1

如您所见,这个测试与示例 3-28 非常相似,但使用FluentWait实例,我们可以指定额外的特性。在这种情况下,我们将轮询时间更改为一秒。

小贴士

WebDriverWait类(在上一小节中介绍)扩展了通用类FluentWait。因此,你可以在显式等待中使用 Table 3-12 中展示的所有方法。

概述与展望

本章介绍了 Selenium WebDriver API 的基础知识。首先,你学习了如何创建和关闭WebDriver实例。这些对象代表了通过 Selenium WebDriver 控制的浏览器。因此,我们使用ChromeDriver实例来操作 Chrome 浏览器,FirefoxDriver实例来操作 Firefox 浏览器,依此类推。其次,你学习了WebElement,这是一个代表不同网页元素(如链接、图像、表单字段等)的类。Selenium WebDriver 提供了多种定位网页元素的策略:通过 HTML 属性(id、name 或 class)、标签名、链接文本(完整或部分)、CSS 选择器和 XPath。我们还探讨了 Selenium WebDriver 4 的全新定位策略,称为相对定位器。然后,我们涵盖了模拟用户操作,包括键盘和鼠标的使用。你可以使用这些操作进行简单的动作(如点击链接、填写文本输入等)或复杂的用户手势(如拖放、点击悬停等)。最后,我们研究了在 Selenium WebDriver 测试中等待的能力。由于当前网络应用的分布式、动态和异步特性,这一功能至关重要。在 Selenium WebDriver 中有三种主要的等待策略:隐式等待(指定等待元素的一般超时时间)、显式等待(暂停测试执行直到满足给定条件)、以及流畅等待(显式等待的扩展,具有更精细的设置)。

下一章将继续深入探讨 Selenium WebDriver API。特别是,第四章 将回顾在不同浏览器(Chrome、Edge、Firefox 等)中的互操作特性。在这些特性中,你将了解如何执行 JavaScript、指定事件监听器、配置页面和脚本加载的超时时间、管理浏览器历史、生成屏幕截图、操作 cookies、操作下拉列表(即 selects 和 data lists)、处理窗口目标(即标签、框架和 iframe)和对话框(即警报、提示、确认和模态弹出)、使用 Web 存储,以及理解 WebDriver 异常。

第四章:浏览器无关特性

本章回顾了 Selenium WebDriver 中在不同 Web 浏览器中可互操作的特性。在此组中,一个相关的多功能特性是执行 JavaScript。此外,Selenium WebDriver API 允许配置页面和脚本加载的超时时间。另一个方便的功能是对浏览器屏幕进行截图,或仅截取给定元素对应的部分。然后,我们可以使用 WebDriver 管理受控浏览器的不同方面,如浏览器大小和位置、历史记录或 cookies。然后,WebDriver 提供了各种资产来控制特定的 Web 元素,如下拉列表(即 HTML 选择字段和数据列表)、导航目标(即窗口、选项卡、框架和 iframe)或对话框(即警报、提示、确认和模态对话框)。最后,我们学习如何使用 Web 存储处理本地和会话数据,实现事件侦听器,并使用 Selenium WebDriver API 提供的异常。

执行 JavaScript

JavaScript 是一种高级编程语言,被所有主要浏览器支持。我们可以在 Web 应用程序的客户端使用 JavaScript 进行各种操作,如 DOM 操作、用户交互、处理来自远程服务器的请求-响应或与正则表达式工作等。对于测试自动化而言,幸运的是 Selenium WebDriver 允许注入和执行任意 JavaScript 片段。为此,Selenium WebDriver API 提供了 JavascriptExecutor 接口。表格 4-1 介绍了此接口中可用的公共方法,分为同步、固定和异步脚本三类。随后的小节提供了更多详细信息,并通过不同示例展示了它们的使用。

表格 4-1. JavascriptExecutor 方法

分类 方法 返回 描述
同步脚本
executeScript(
    String script,
    Object... args)

|

Object
在当前页面上执行 JavaScript 代码。
固定脚本
pin(String
    script)

|

ScriptKey
将 JavaScript 片段附加到 WebDriver 会话中。固定 脚本可在 WebDriver 会话处于活动状态时多次使用。

|

unpin(ScriptKey
    key)

|

void
分离先前固定的脚本与 WebDriver 会话。

|

getPinnedScripts()

|

Set<ScriptKey>
收集所有固定脚本(每个脚本由唯一的 ScriptKey 标识)。

|

executeScript(
    ScriptKey key,
    Object... args)

|

Object
调用先前固定的脚本(使用其 ScriptKey 标识)。
异步脚本
executeAsyncScript(
    String script,
    Object... args)

|

Object
在当前页面上执行 JavaScript 代码(通常是异步操作)。与 executeScript() 的区别在于,使用 executeAsyncScript() 执行的脚本必须显式地通过调用回调函数来标识其终止。按照惯例,此回调函数作为其最后一个参数注入到脚本中。

任何从RemoteWebDriver类继承的驱动对象也实现了JavascriptExecutor接口。因此,当使用以WebDriver接口声明的主要浏览器(例如ChromeDriverFirefoxDriver等)时,可以像以下片段中显示的方式将其转换为JavascriptExecutor。然后,我们可以使用执行器(在示例中使用变量js)来调用 Table 4-1 中介绍的方法。

WebDriver driver = new ChromeDriver();
JavascriptExecutor js = (JavascriptExecutor) driver;

同步脚本

JavascriptExecutor对象的executeScript()方法允许在 WebDriver 会话的当前网页上下文中执行一段 JavaScript 代码。该方法的调用(在 Java 中)会阻塞控制流,直到脚本终止。因此,我们通常用这个方法来执行在测试网页中执行同步脚本。executeScript()方法允许两个参数:

String script

必需的要执行的 JavaScript 片段。此代码作为匿名函数(即没有名称的 JavaScript 函数)在当前页面的主体中执行。

Object... args

可选参数脚本。这些参数必须是以下类型之一:数字,布尔值,字符串,WebElement,或者这些类型的List(否则,WebDriver 会抛出异常)。这些参数在注入的脚本中可以使用内置的 JavaScript 变量arguments

当脚本返回某个值(即代码包含return语句)时,Selenium WebDriver 的executeScript()方法也会在 Java 中返回一个值(否则,executeScript()返回null)。可能的返回类型包括:

WebElement

当返回 HTML 元素时

Double

对于小数

Long

对于非十进制数

Boolean

对于布尔值

List<Object>

对于数组

Map<String, Object>

对于键-值集合

String

对于所有其他情况

需要使用 Selenium WebDriver 执行 JavaScript 的情况非常多样化。以下各小节将回顾两种情况,其中 Selenium WebDriver 没有提供内置功能,而需要使用 JavaScript 来自动化:滚动网页和处理 Web 表单中的颜色选择器。

滚动

正如在 Chapter 3 中解释的那样,Selenium WebDriver 允许模拟不同的鼠标操作,包括单击、右键单击或双击等。然而,使用 Selenium WebDriver API 无法向上或向下滚动网页。相反,我们可以通过执行简单的 JavaScript 代码轻松实现此自动化。Example 4-1 展示了在练习网页中使用实际网页的基本示例(请查看测试方法的第一行中此页面的 URL)。

示例 4-1. 测试执行 JavaScript 来向下滚动像素量
@Test
void testScrollBy() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/long-page.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    JavascriptExecutor js = (JavascriptExecutor) driver; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    String script = "window.scrollBy(0, 1000);";
    js.executeScript(script); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

打开一个包含非常长文本的练习网页(请参见 Figure 4-1)。

2

driver对象转换为JavascriptExecutor。我们将使用变量js在浏览器中执行 JavaScript。

3

执行一段 JavaScript 代码。在这种情况下,我们调用 JavaScript 函数scrollBy()来按给定的数量(在本例中为 1,000 像素)向下滚动文档。请注意,此片段不使用return,因此在 Java 逻辑中我们不会接收任何返回的对象。此外,我们也没有向脚本传递任何参数。

hosw 0401

图 4-1. 具有长内容的实践网页

Example 4-2 展示了另一个使用滚动和同样的网页示例的测试。这次,我们不是移动固定数量的像素,而是将文档滚动到网页中的最后一个段落。

Example 4-2. 测试执行 JavaScript 以滚动到给定元素
@Test
void testScrollIntoView() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/long-page.html");
    JavascriptExecutor js = (JavascriptExecutor) driver;
    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebElement lastElememt = driver
            .findElement(By.cssSelector("p:last-child")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    String script = "arguments[0].scrollIntoView();"; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    js.executeScript(script, lastElememt); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

为了使此测试更加健壮,我们指定了一个隐式超时时间。否则,在执行后续命令时,如果页面没有完全加载,测试可能会失败。

2

我们使用 CSS 选择器找到网页中的最后一个段落。

3

我们定义要注入页面的脚本。请注意,此脚本不返回任何值,但作为新颖之处,它使用第一个函数参数调用 JavaScript 函数scrollIntoView()

4

我们执行先前的脚本,将定位的WebElement作为参数传递。此元素将是脚本的第一个参数(即arguments[0])。

最后一个滚动示例是无限滚动。当用户滚动到网页底部时,这种技术可以动态加载更多内容。自动化这种网页是一个有教育意义的用例,因为它涉及到 Selenium WebDriver API 的不同方面。例如,您可以使用类似的方法使用 Selenium WebDriver 爬取网页。Example 4-3 展示了使用无限滚动页面的测试。

Example 4-3. 在无限滚动页面中执行 JavaScript 的测试
@Test
void testInfiniteScroll() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/infinite-scroll.html");
    JavascriptExecutor js = (JavascriptExecutor) driver;
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    By pLocator = By.tagName("p");
    List<WebElement> paragraphs = wait.until(
            ExpectedConditions.numberOfElementsToBeMoreThan(pLocator, 0));
    int initParagraphsNumber = paragraphs.size(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebElement lastParagraph = driver.findElement(
            By.xpath(String.format("//p[%d]", initParagraphsNumber))); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String script = "arguments[0].scrollIntoView();";
    js.executeScript(script, lastParagraph); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(pLocator,
            initParagraphsNumber)); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们定义了一个显式等待,因为我们需要暂停测试直到新内容加载完成。

2

我们找到页面上段落的初始数量。

3

我们定位页面的最后一个段落。

4

我们滚动到这个元素中。

5

我们等待直到页面上有更多的段落可用。

颜色选择器

HTML 中的颜色选择器是一种输入类型,允许用户通过点击和拖动光标来选择颜色。实践中的 Web 表单包含了这种元素(参见 图 4-2)。

hosw 0402

图 4-2. 实践中的颜色选择器

以下代码显示了颜色选择器的 HTML 标记。注意它设置了一个初始颜色值(否则,默认颜色是黑色)。

<input type="color" class="form-control form-control-color" name="my-colors"
        value="#563d7c">

示例 4-4 展示了如何与这个颜色选择器交互。因为 Selenium WebDriver API 不提供控制颜色选择器的任何方法,所以我们使用 JavaScript。此外,这个测试还展示了在 Selenium WebDriver API 中可用的支持类 Color 来处理颜色的使用。

示例 4-4. 执行 JavaScript 与颜色选择器交互的测试
@Test
void testColorPicker() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
    JavascriptExecutor js = (JavascriptExecutor) driver;

    WebElement colorPicker = driver.findElement(By.name("my-colors")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String initColor = colorPicker.getAttribute("value"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    log.debug("The initial color is {}", initColor);

    Color red = new Color(255, 0, 0, 1); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String script = String.format(
            "arguments[0].setAttribute('value', '%s');", red.asHex());
    js.executeScript(script, colorPicker); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    String finalColor = colorPicker.getAttribute("value"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    log.debug("The final color is {}", finalColor);
    assertThat(finalColor).isNotEqualTo(initColor); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    assertThat(Color.fromString(finalColor)).isEqualTo(red);
}

1

我们通过名称定位颜色选择器。

2

我们读取颜色选择器的初始值(应该是 #563d7c)。

3

我们定义了一个颜色,使用以下 RGBA 组件:红色=255(最大值),绿色=0(最小值),蓝色=0(最小值),alpha=1(最大值,即完全不透明)。

4

我们使用 JavaScript 来改变颜色选择器中选择的值。或者,我们可以调用语句 colorPicker.sendKeys(red.asHex()); 来改变选择的颜色。

5

我们读取颜色选择器的结果值(应该是 #ff0000)。

6

我们断言颜色与初始值不同,但是符合预期。

固定脚本

Selenium WebDriver API 允许您在 Selenium WebDriver 4 中固定脚本。这个功能允许将 JavaScript 片段附加到 WebDriver 会话,并为每个片段分配一个唯一的键,并在需要时(甚至在不同的网页上)执行这些片段。示例 4-5 展示了使用固定脚本的测试。

示例 4-5. 作为固定脚本执行 JavaScript 的测试
@Test
void testPinnedScripts() {
    String initPage = "https://bonigarcia.dev/selenium-webdriver-java/";
    driver.get(initPage);
    JavascriptExecutor js = (JavascriptExecutor) driver;

    ScriptKey linkKey = js
            .pin("return document.getElementsByTagName('a')[2];"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    ScriptKey firstArgKey = js.pin("return arguments[0];"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    Set<ScriptKey> pinnedScripts = js.getPinnedScripts(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(pinnedScripts).hasSize(2); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    WebElement formLink = (WebElement) js.executeScript(linkKey); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    formLink.click(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    assertThat(driver.getCurrentUrl()).isNotEqualTo(initPage); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)

    String message = "Hello world!";
    String executeScript = (String) js.executeScript(firstArgKey, message); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
    assertThat(executeScript).isEqualTo(message); ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png)

    js.unpin(linkKey); ![10](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/10.png)
    assertThat(js.getPinnedScripts()).hasSize(1); ![11](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/11.png)
}

1

我们附加了一个 JavaScript 片段来定位网页中的一个元素。请注意,我们可以使用标准的 WebDriver API 做同样的事情。然而,出于演示目的,我们使用了这种方法。

2

我们附加了另一段 JavaScript,它返回我们传递给它的第一个参数。

3

我们读取了一组固定脚本。

4

我们断言固定脚本的数量与预期相同(即2)。

5

我们执行第一个固定脚本。结果,我们在 Java 中作为 WebElement 得到网页中的第三个链接。

6

我们点击此链接,应对应实践网页链接。结果,浏览器应该导航到该页面。

7

我们断言当前 URL 与初始 URL 不同。

8

我们执行第二个固定脚本。注意,尽管页面在浏览器中已更改(因为脚本附加到会话而不是单个页面),但仍可以运行固定脚本。

9

我们断言返回的消息符合预期。

10

我们取消固定其中一个脚本。

11

我们验证固定脚本的数量符合预期(即此时为 1)。

异步脚本

JavascriptExecutor 接口的 executeAsyncScript() 方法允许使用 Selenium WebDriver 在网页上下文中执行 JavaScript 脚本。与之前解释的 executeScript() 类似,executeAsyncScript() 执行提供的 JavaScript 代码在当前页面的匿名函数中。该函数的执行会阻塞 Selenium WebDriver 的控制流。不同之处在于,在 executeAsyncScript() 中,我们必须通过调用 done 回调显式地标记脚本终止。该回调作为相应匿名函数的最后一个参数注入到执行的脚本中(即 arguments[arguments.length - 1])。示例 4-6 展示了使用这种机制的测试。

示例 4-6. 测试执行异步 JavaScript
@Test
void testAsyncScript() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    JavascriptExecutor js = (JavascriptExecutor) driver;

    Duration pause = Duration.ofSeconds(2); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String script = "const callback = arguments[arguments.length - 1];"
            + "window.setTimeout(callback, " + pause.toMillis() + ");"; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    long initMillis = System.currentTimeMillis(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    js.executeAsyncScript(script); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    Duration elapsed = Duration
            .ofMillis(System.currentTimeMillis() - initMillis); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    log.debug("The script took {} ms to be executed", elapsed.toMillis());
    assertThat(elapsed).isGreaterThanOrEqualTo(pause); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们定义一个暂停时间为 2 秒。

2

我们定义要执行的脚本。在第一行中,我们为回调定义一个常量(即最后一个脚本参数)。然后,我们使用 JavaScript 函数 window.setTimeout() 来暂停给定时间的脚本执行。

3

我们获取当前系统时间(以毫秒为单位)。

4

我们执行脚本。如果一切正常,测试执行会在这一行中阻塞 second 秒(如步骤 1 中定义)。

5

我们计算执行前一行所需的时间。

6

我们断言经过的时间符合预期(通常高于定义的暂停时间几毫秒)。

提示

您可以找到一个额外的示例,执行 “Notifications” 上的异步脚本。

超时

Selenium WebDriver 允许指定三种类型的超时。我们可以通过在 Selenium WebDriver API 中调用 manage().timeouts() 方法来使用它们。第一个超时是隐式等待,已在“隐式等待”(作为等待策略的一部分)中解释过。其他选项是页面加载和脚本加载超时,稍后进行解释。

页面加载超时

页面加载超时 提供了中断导航尝试的时间限制。换句话说,此超时限制了加载网页的时间。当超过此超时(默认值为 30 秒)时,将抛出异常。示例 4-7 展示了此超时的示例。正如您所见,此代码片段是 SUT 中的一个虚拟实现的负面测试。换句话说,它检查 SUT 中的意外条件。

示例 4-7. 使用页面加载超时的测试
@Test
void testPageLoadTimeout() {
    driver.manage().timeouts().pageLoadTimeout(Duration.ofMillis(1)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    assertThatThrownBy(() -> driver
            .get("https://bonigarcia.dev/selenium-webdriver-java/"))
                    .isInstanceOf(TimeoutException.class); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

1

我们指定了最小可能的页面加载超时,即一毫秒。

2

我们加载一个网页。这个调用(实现为 Java lambda)将失败,因为不可能在少于一毫秒的时间内加载该网页。因此,预期在 lambda 中抛出 TimeoutException 异常,使用 AssertJ 方法 assertThatThrownBy

注意

您可以通过删除超时声明(即第一步)来测试它。如果这样做,测试将失败,因为预期会抛出异常,但未抛出。

脚本加载超时

脚本加载超时 提供了中断正在评估的脚本的时间限制。此超时的默认值为三百秒。示例 4-8 展示了使用脚本加载超时的测试。

示例 4-8. 使用脚本加载超时的测试
@Test
void testScriptTimeout() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    JavascriptExecutor js = (JavascriptExecutor) driver;
    driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(3)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    assertThatThrownBy(() -> {
        long waitMillis = Duration.ofSeconds(5).toMillis();
        String script = "const callback = arguments[arguments.length - 1];"
                + "window.setTimeout(callback, " + waitMillis + ");"; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        js.executeAsyncScript(script);
    }).isInstanceOf(ScriptTimeoutException.class); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们定义了三秒的脚本超时。这意味着超过此时间的脚本将抛出异常。

2

我们执行一个异步脚本,暂停执行五秒钟。

3

脚本执行时间大于配置的脚本超时,导致 ScriptTimeoutException。同样,此示例是一个负面测试,即预期此异常。

截图

Selenium WebDriver 主要用于执行 Web 应用程序的端到端功能测试。换句话说,我们使用它与用户界面交互(即使用 Web 浏览器)来验证 Web 应用程序的行为是否符合预期。这种方法非常方便地自动化高级用户场景,但也存在不同的困难。端到端测试的主要挑战之一是诊断测试失败的根本原因。假设失败是合理的(即不是由于实现不良的测试引起的),根本原因可能多种多样:客户端(例如,不正确的 JavaScript 逻辑)、服务器端(例如,内部异常)或与其他组件的集成(例如,对数据库的访问不足),等等。在 Selenium WebDriver 中用于失败分析的最普遍机制之一是进行浏览器截图。本节介绍了 Selenium WebDriver API 提供的机制。

提示

“失败分析”回顾了确定测试何时失败以及执行不同失败分析技术(如截图、录制和日志收集)的框架特定技术。

Selenium WebDriver 提供了接口TakesScreenshot来进行浏览器截图。从RemoteWebDriver继承的任何驱动程序对象(参见图 2-2)也实现了此接口。因此,我们可以将一个实例化了其中一个主要浏览器(例如ChromeDriverFirefoxDriver等)的WebDriver对象进行强制类型转换,如下所示:

WebDriver driver = new ChromeDriver();
TakesScreenshot ts = (TakesScreenshot) driver;

接口TakesScreenshot仅提供一个名为getScreenshotAs(OutputType<X> target)的方法来进行截图。参数OutputType<X> target确定截图类型和返回值。表 4-2 展示了此参数的可用替代方案。

表 4-2. OutputType 参数

参数 描述 返回 示例

|

OutputType.FILE
将截图保存为 PNG 文件(位于临时系统目录中)
File

|

File screenshot =
    ts.getScreenshotAs(
    OutputType.FILE);

|

|

OutputType.BASE64
将截图保存为 Base64 格式(即编码为 ASCII 字符串)
String

|

String screenshot =
    ts.getScreenshotAs(
    OutputType.BASE64);

|

|

OutputType.BYTES
将截图保存为原始字节数组
byte[]

|

byte[] screenshot =
    ts.getScreenshotAs(
    OutputType.BYTES);

|

提示

方法getScreenshotAs()允许对浏览器视窗进行截图。此外,Selenium WebDriver 4 还允许使用不同的机制创建全页截图(参见“全页截图”)。

示例 4-9 展示了一个以 PNG 格式进行浏览器截图的测试。示例 4-10 展示了另一个以 Base64 字符串创建截图的测试。生成的截图显示在图 4-3 中。

示例 4-9. 将浏览器截图保存为 PNG 文件
@Test
void testScreenshotPng() throws IOException {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    TakesScreenshot ts = (TakesScreenshot) driver;

    File screenshot = ts.getScreenshotAs(OutputType.FILE); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    log.debug("Screenshot created on {}", screenshot);

    Path destination = Paths.get("screenshot.png"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    Files.move(screenshot.toPath(), destination, REPLACE_EXISTING); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    log.debug("Screenshot moved to {}", destination);

    assertThat(destination).exists(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们将浏览器屏幕保存为 PNG 文件。

2

默认情况下,此文件位于临时文件夹中,因此我们将其移动到名为screenshot.png的新文件(位于根项目文件夹中)。

3

我们使用标准的 Java 将截图文件移动到新位置。

4

我们使用断言来验证目标文件是否存在。

hosw 0403

Figure 4-3. 练习网站首页的浏览器截图
Example 4-10. 测试制作 Base64 格式的截图
@Test
void testScreenshotBase64() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    TakesScreenshot ts = (TakesScreenshot) driver;

    String screenshot = ts.getScreenshotAs(OutputType.BASE64); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    log.debug("Screenshot in base64 "
          + "(you can copy and paste it into a browser navigation bar to watch it)\n"
          + "data:image/png;base64,{}", screenshot); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(screenshot).isNotEmpty(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们将浏览器屏幕制作成 Base64 格式。

2

我们在 Base64 字符串前追加前缀data:image/png;base64,并将其记录在标准输出中。您可以将生成的字符串复制粘贴到浏览器导航栏中以显示图片。

3

我们断言截图字符串具有内容。

注意

将截图记录为 Base64 格式,正如前面示例中所述,在没有文件系统访问权限的 CI 服务器(例如 GitHub Actions)中运行测试时,这非常有用。

WebElement 截图

WebElement接口扩展了TakesScreenshot接口。这样,可以对给定的网页元素的可见内容进行部分截图(参见 Example 4-11)。请注意,此测试与使用 PNG 文件的上一个测试非常相似,但在本例中,我们直接使用网页元素调用getScreenshotAs()方法。Figure 4-4 展示了生成的截图。

Example 4-11. 测试制作部分 PNG 文件截图
@Test
void testWebElementScreenshot() throws IOException {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement form = driver.findElement(By.tagName("form"));
    File screenshot = form.getScreenshotAs(OutputType.FILE);
    Path destination = Paths.get("webelement-screenshot.png");
    Files.move(screenshot.toPath(), destination, REPLACE_EXISTING);

    assertThat(destination).exists();
}

hosw 0404

Figure 4-4. 练习网页表单的部分截图

窗口大小和位置

Selenium WebDriver API 允许轻松地使用Window接口来操作浏览器的大小和位置。可以通过驱动器对象访问这种类型,使用以下语句。Table 4-3 显示了此接口中可用的方法。接着,Example 4-12 展示了关于此功能的基本测试。

Window window = driver.manage().window();

表 4-3. 窗口方法

方法 返回 描述

|

getSize()

|

Dimension
获取当前窗口大小。返回的是外部窗口尺寸,而不仅仅是视口(即网页的对终端用户可见的可见区域)。

|

setSize(Dimension
    targetSize)

|

void
更改当前窗口大小(再次指的是其外部尺寸,而不是视口)。

|

getPosition()

|

Point
获取当前窗口位置(相对于屏幕左上角)。

|

setPosition(Point
    targetPosition)

|

void
更改当前窗口位置(再次相对于屏幕左上角)。

|

maximize()

|

void
最大化当前窗口。

|

minimize()

|

void
最小化当前窗口。

|

fullscreen()

|

void
全屏当前窗口。
Example 4-12. 测试读取和更改浏览器大小和位置
@Test
void testWindow() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    Window window = driver.manage().window();

    Point initialPosition = window.getPosition(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    Dimension initialSize = window.getSize(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    log.debug("Initial window: position {} -- size {}", initialPosition,
            initialSize);

    window.maximize(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    Point maximizedPosition = window.getPosition();
    Dimension maximizedSize = window.getSize();
    log.debug("Maximized window: position {} -- size {}", maximizedPosition,
            maximizedSize);

    assertThat(initialPosition).isNotEqualTo(maximizedPosition); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(initialSize).isNotEqualTo(maximizedSize);
}

1

我们读取窗口位置。

2

我们读取窗口大小。

3

我们将浏览器窗口最大化。

4

我们验证最大化的位置(以及下一行中的大小)与原始窗口不同。

浏览器历史

Selenium WebDriver 允许通过 Navigation 接口操纵浏览器历史。以下语句说明了如何从 WebDriver 对象中访问此接口。使用此接口非常简单。表 4-4 显示了其公共方法,示例 4-13 展示了一个基本示例。请注意,此测试使用这些方法导航到不同的网页,并在测试结束时验证网页 URL 是否符合预期。

Navigation navigation = driver.navigate();

表 4-4. 导航方法

方法 返回 描述

|

back()

|

void
在浏览器历史中后退

|

forward()

|

void
在浏览器历史中前进

|

to(String url)
to(URL url)

|

void
在当前窗口中加载新的网页

|

refresh()

|

void
刷新当前页面
示例 4-13. 使用导航方法进行测试
@Test
void testHistory() {
    String baseUrl = "https://bonigarcia.dev/selenium-webdriver-java/";
    String firstPage = baseUrl + "navigation1.html";
    String secondPage = baseUrl + "navigation2.html";
    String thirdPage = baseUrl + "navigation3.html";

    driver.get(firstPage);

    driver.navigate().to(secondPage);
    driver.navigate().to(thirdPage);
    driver.navigate().back();
    driver.navigate().forward();
    driver.navigate().refresh();

    assertThat(driver.getCurrentUrl()).isEqualTo(thirdPage);
}

阴影 DOM

正如“文档对象模型(DOM)”中介绍的那样,DOM 是一个编程接口,允许我们使用树结构来表示和操作网页。阴影 DOM 是这个编程接口的一个特性,它允许在常规 DOM 树内创建作用域子树。阴影 DOM 允许封装 DOM 子树的一个组(称为阴影树,如图 4-5 中所示),可以指定与原始 DOM 不同的 CSS 样式。阴影树附加到的常规 DOM 中的节点称为阴影宿主。阴影树的根节点称为阴影根。如图 4-5 所示,阴影树被展平到原始 DOM 中,形成一个单一的组合树,以在浏览器中呈现。

hosw 0405

图 4-5. 阴影 DOM 的示意图
注意

阴影 DOM 是标准套件的一部分(与 HTML 模板或自定义元素一起),它允许实现web 组件(即用于 Web 应用程序的可重用自定义元素)。

阴影 DOM 允许创建自包含组件。换句话说,阴影树与原始 DOM 隔离开来。这个特性对于网页设计和组合非常有用,但对于使用 Selenium WebDriver 进行自动化测试可能具有挑战性(因为常规的定位策略无法在阴影树内找到网页元素)。幸运的是,Selenium WebDriver 4 提供了一个 WebElement 方法,允许访问阴影 DOM。示例 4-14 演示了这个用法。

示例 4-14. 测试读取阴影 DOM
@Test
void testShadowDom() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/shadow-dom.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebElement content = driver.findElement(By.id("content")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    SearchContext shadowRoot = content.getShadowRoot(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    WebElement textElement = shadowRoot.findElement(By.cssSelector("p")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(textElement.getText()).contains("Hello Shadow DOM"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们打开包含影子树的实践网页。 您可以检查此页面的源代码,以检查用于创建影子树的 JavaScript 方法。

2

我们定位影子主机元素。

3

我们从主机元素获取影子根。 结果,我们得到一个 SearchContext 的实例,它是由 WebDriverWebElement 实现的接口,允许我们使用 findElement()find​Ele⁠ments() 方法查找元素。

4

我们在影子树中找到第一个段落元素。

5

我们验证影子元素的文本内容是否符合预期。

警告

此 W3C WebDriver 规范的功能在撰写本文时较新,因此可能尚未在所有驱动程序中实现(例如,chromedriver、geckodriver)。 例如,它从 Chrome 和 Edge 的 96 版本开始可用。

Cookies

HTTP 1.x 是一种无状态协议,这意味着服务器不跟踪用户状态。 换句话说,Web 服务器不会跨不同请求记住用户。 Cookie 机制是 HTTP 的一个扩展,它允许通过从服务器发送小段文本(称为cookie)来跟踪用户。 这些 Cookie 必须由客户端发送回来,这样,服务器就记住了它们的客户端。 Cookie 允许您维护 Web 会话或在网站上个性化用户体验,等等。

Web 浏览器允许手动管理浏览器 Cookie。 Selenium WebDriver 可以通过编程方式实现等效操作。 Selenium WebDriver API 提供了 表 4-5 中显示的方法来完成此操作。 它们可通过 WebDriver 对象的 manage() 函数访问。

表 4-5. Cookie 管理方法

方法 返回 描述

|

addCookie(Cookie cookie)

|

void
添加新 Cookie

|

deleteCookieNamed(String name)

|

void
通过名称删除现有的 Cookie

|

deleteCookie(Cookie cookie)

|

void
通过实例删除现有的 Cookie

|

deleteAllCookies()

|

void
删除所有 Cookie

|

getCookies()

|

Set<Cookie>
获取所有 Cookie

|

getCookieNamed(String name)

|

Cookie
通过名称获取 Cookie

正如此表所示,Cookie 类在 Java 中提供了对单个 Cookie 的抽象。 表 4-6 总结了此类中可用的方法。 此外,此类具有几个构造函数,它们按位置接受以下参数:

String name

Cookie 名称(必需)

String value

Cookie 值(必需)

String domain

Cookie 可见的域(可选)

String path

Cookie 可见的路径(可选)

Date expiry

Cookie 过期日期(可选)

boolean isSecure

Cookie 是否需要安全连接(可选)

boolean isHttpOnly

此 Cookie 是否为 HTTP-only Cookie,即 Cookie 不可通过客户端脚本访问(可选)

String sameSite

该 cookie 是否为同站点 cookie,即限定为第一方或同站点上下文(可选)

表 4-6. Cookie 方法

方法 返回 描述

|

getName()

|

String
读取 cookie 名称

|

getValue()

|

String
读取 cookie 值

|

getDomain()

|

String
读取 cookie 域名

|

getPath()

|

String
读取 cookie 路径

|

isSecure()

|

boolean
读取 cookie 是否需要安全连接

|

isHttpOnly()

|

boolean
读取 cookie 是否为 HTTP-only

|

getExpiry()

|

Date
读取 cookie 过期日期

|

getSameSite()

|

String
读取 cookie 同站点上下文

|

validate()

|

void
检查 cookie 的不同字段,并在遇到任何问题时抛出 IllegalArgumentException

|

toJson()

|

Map<String, Object>
将 cookie 值映射为键值对

下面的示例展示了使用 Selenium WebDriver API 管理网页 cookie 的不同测试。这些示例使用一个练习网页,在 GUI 上显示网站的 cookie(见 图 4-6):

  • 示例 4-15 展示了如何读取网站现有的 cookie。

  • 示例 4-16 展示了如何添加新的 cookie。

  • 示例 4-17 解释了如何编辑现有 cookie。

  • 示例 4-18 演示了如何删除 cookie。

hosw 0406

@Test
void testReadCookies() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/cookies.html");

    Options options = driver.manage(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    Set<Cookie> cookies = options.getCookies(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(cookies).hasSize(2);

    Cookie username = options.getCookieNamed("username"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(username.getValue()).isEqualTo("John Doe"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(username.getPath()).isEqualTo("/");

    driver.findElement(By.id("refresh-cookies")).click(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们获取用于管理 cookie 的 Options 对象。

2

我们读取此页面上所有可用的 cookie。应该包含两个 cookie。

3

我们读取名称为 username 的 cookie。

4

之前的 cookie 值应为 John Doe

5

最后一条语句不影响测试。我们调用此命令来检查浏览器 GUI 中的 cookie。

@Test
void testAddCookies() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/cookies.html");

    Options options = driver.manage();
    Cookie newCookie = new Cookie("new-cookie-key", "new-cookie-value"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.addCookie(newCookie); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    String readValue = options.getCookieNamed(newCookie.getName())
            .getValue(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(newCookie.getValue()).isEqualTo(readValue); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    driver.findElement(By.id("refresh-cookies")).click();
}

1

我们创建了一个新的 cookie。

2

我们将 cookie 添加到当前页面。

3

我们读取刚刚添加的 cookie 的值。

4

我们验证该值是否符合预期。

@Test
void testEditCookie() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/cookies.html");

    Options options = driver.manage();
    Cookie username = options.getCookieNamed("username"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    Cookie editedCookie = new Cookie(username.getName(), "new-value"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.addCookie(editedCookie); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    Cookie readCookie = options.getCookieNamed(username.getName()); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(editedCookie).isEqualTo(readCookie); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    driver.findElement(By.id("refresh-cookies")).click();
}

1

我们读取一个已存在的 cookie。

2

我们创建一个新的 cookie,并重用之前的 cookie 名称。

3

我们向网页添加新的 cookie。

4

我们读取刚刚添加的 cookie。

5

我们验证 cookie 已正确编辑。

@Test
void testDeleteCookies() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/cookies.html");

    Options options = driver.manage();
    Set<Cookie> cookies = options.getCookies(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    Cookie username = options.getCookieNamed("username"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.deleteCookie(username); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    assertThat(options.getCookies()).hasSize(cookies.size() - 1); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    driver.findElement(By.id("refresh-cookies")).click();
}

1

我们读取所有的 cookie。

2

读取名为username的 cookie。

3

删除先前的 cookie。

4

我们验证 cookie 的大小是否符合预期。

下拉列表

网页表单中典型的元素是下拉列表。这些字段允许用户在选项列表中选择一个或多个元素。用于呈现这些字段的经典 HTML 标签是<select><option>。通常,实践中的网页表单包含其中一个元素(见图 4-7),其 HTML 定义如下:

<select class="form-select" name="my-select">
  <option selected>Open this select menu</option>
  <option value="1">One</option>
  <option value="2">Two</option>
  <option value="3">Three</option>
</select>

hosw 0407

图 4-7. 实践网页表单中的选择字段

这些元素在网页表单中分布广泛。因此,Selenium WebDriver 提供了一个名为Select的辅助类,用于简化它们的操作。这个类封装了一个选择WebElement,并提供了各种功能。表 4-7 总结了Select类中的公共方法。之后,示例 4-19 展示了使用这个类的基本测试。

表 4-7. 选择方法

方法 返回 描述

|

Select(WebElement element)

|

Select
使用WebElement作为参数的构造函数(必须是<select>元素),否则会抛出UnexpectedTagNameException

|

getWrappedElement()

|

WebElement
获取包装的WebElement(即构造函数中使用的那个)

|

isMultiple()

|

boolean
选择元素是否支持选择多个选项

|

getOptions()

|

List<WebElement>
读取属于选择元素的所有选项

|

getAllSelectedOptions()

|

List<WebElement>
读取所有选定的选项

|

getFirstSelectedOption()

|

WebElement
读取第一个选定选项

|

selectByVisibleText(String text)

|

void
取消选择所有与给定显示文本匹配的选项

|

selectByIndex(int index)

|

void
根据索引号选择一个选项

|

selectByValue(String value)

|

void
根据值属性选择选项

|

deselectAll()

|

void
取消选择所有选项

|

deselectByValue(String value)

|

void
根据值属性取消选择选项

|

deselectByIndex(int index)

|

void
根据索引号取消选择

|

deselectByVisibleText(String text)

|

void
取消选择与给定显示文本匹配的选项
Example 4-19. 测试与选择字段交互
@Test
void test() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    Select select = new Select(driver.findElement(By.name("my-select"))); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String optionLabel = "Three";
    select.selectByVisibleText(optionLabel); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    assertThat(select.getFirstSelectedOption().getText())
            .isEqualTo(optionLabel); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们通过名称找到选择元素,并使用生成的WebElement实例化一个Select对象。

2

我们使用按文本策略选择其中一个可用的选项。

3

我们验证选定的选项文本是否符合预期。

数据列表元素

在 HTML 中实现下拉列表的另一种方法是使用数据列表。虽然数据列表在图形上与选择元素非常相似,但它们之间有明显区别。一方面,选择字段显示选项列表,用户选择其中一个(或多个)可用选项。另一方面,数据列表显示与输入表单(文本)字段关联的建议选项列表,用户可以自由选择其中一个建议值或输入自定义值。实践中的网页表单包含了其中一个数据列表。您可以在以下代码片段中找到其标记,以及 图 4-8 中的截图。

<input class="form-control" list="my-options" name="my-datalist"
        placeholder="Type to search...">
<datalist id="my-options">
  <option value="San Francisco">
  <option value="New York">
  <option value="Seattle">
  <option value="Los Angeles">
  <option value="Chicago">
</datalist>

hosw 0408

图 4-8. 实践中的数据列表字段

Selenium WebDriver 不提供自定义辅助类来操作数据列表。相反,我们需要像操作标准输入文本一样与它们交互,唯一的区别是点击输入字段时会显示它们的选项。示例 4-20 展示了说明此功能的测试。

示例 4-20. 与数据列表字段交互的测试
@Test
void testDatalist() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");

    WebElement datalist = driver.findElement(By.name("my-datalist")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    datalist.click(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebElement option = driver
            .findElement(By.xpath("//datalist/option[2]")); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String optionValue = option.getAttribute("value"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    datalist.sendKeys(optionValue); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    assertThat(optionValue).isEqualTo("New York"); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们定位用于数据列表的输入字段。

2

我们点击它以显示其选项。

3

我们找到第二个选项。

4

我们读取定位选项的值。

5

我们在输入字段中键入该值。

6

我们断言选项的值符合预期。

导航目标

当使用浏览器导航网页时,默认情况下,我们使用导航栏中的 URL 对应的单个页面。然后,我们可以在新的浏览器标签中打开另一个页面。当链接定义属性target时,可以显式打开这第二个标签,或者用户可以通过按住修改键 Ctrl(或 macOS 中的 Cmd)并与鼠标点击网页链接一起强制导航到新标签。另一种可能性是在新窗口中打开网页。为此,网页通常使用 JavaScript 命令window.open(url)。同时,显示不同页面的另一种方式是使用framesiframes。框架是定义特定区域(在frameset集合中)的 HTML 元素类型,用于显示网页的区域。iframe 是另一个 HTML 元素,允许将 HTML 页面嵌入当前页面。

警告

不建议使用框架,因为这些元素有许多缺点,例如性能和可访问性问题。出于兼容性考虑,我解释了如何在 Selenium WebDriver 中使用它们。尽管如此,我强烈建议在全新的网页应用程序中避免使用框架。

Selenium WebDriver API 提供 TargetLocator 接口,用于处理先前提到的目标(例如标签页、窗口、框架和 iframes)。这个接口允许改变 WebDriver 对象未来命令的焦点(例如切换到新标签页、窗口等)。可以通过在 WebDriver 对象上调用 switchTo() 方法访问这个接口。Table 4-8 描述了它的公共方法。

Table 4-8. TargetLocator 方法

方法 返回值 描述

|

frame(int index)

|

WebDriver
通过索引号切换焦点到一个框架(或 iframe)。

|

frame(String
    nameOrId)

|

WebDriver
通过名称或 id 切换焦点到一个框架(或 iframe)。

|

frame(WebElement
    frameElement)

|

WebDriver
通过先前作为 WebElement 定位的方式切换焦点到一个框架(或 iframe)。

|

parentFrame()

|

WebDriver
切换焦点到父上下文。

|

window(String
    nameOrHandle)

|

WebDriver
切换焦点到另一个窗口,通过名称或 句柄。窗口句柄是一个十六进制字符串,唯一标识窗口或标签页。

|

newWindow(WindowType
    typeHint)

|

WebDriver
创建一个新的浏览器窗口(使用 WindowType.WINDOW)或标签页(使用 WindowType.TAB),并将焦点切换到它。

|

defaultContent()

|

WebDriver
选择主文档(在使用 iframes 时)或页面上的第一个框架(在使用 frameset 时)。

|

activeElement()

|

WebElement
获取当前选中的元素。

|

alert()

|

Alert
切换焦点到窗口警告框(详见 “Dialog Boxes”)。

标签页和窗口

Example 4-21 展示了一个测试,我们在其中打开一个新标签页以导航到第二个网页。Example 4-22 展示了一个类似的情况,但是打开了一个新窗口以访问第二个网页。请注意,这些例子之间的区别仅在于参数 WindowType.TABWindowType.WINDOW

Example 4-21. 测试打开一个新标签页
@Test
void testNewTab() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String initHandle = driver.getWindowHandle(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.switchTo().newWindow(WindowType.TAB); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(driver.getWindowHandles().size()).isEqualTo(2); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    driver.switchTo().window(initHandle); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    driver.close(); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    assertThat(driver.getWindowHandles().size()).isEqualTo(1); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

1

我们导航到一个网页。

2

我们获取当前窗口句柄。

3

我们打开一个新标签页并将焦点切换到它。

4

我们打开另一个网页(因为焦点在第二个标签页上,所以页面在第二个标签页中打开)。

5

我们验证此时窗口句柄的数量为 2。

6

我们将焦点切回初始窗口(使用它的句柄)。

7

我们仅关闭当前窗口,第二个标签页保持打开状态。

8

我们验证此时窗口句柄的数量现在是 1。

Example 4-22. 测试打开一个新窗口
@Test
void testNewWindow() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    String initHandle = driver.getWindowHandle();

    driver.switchTo().newWindow(WindowType.WINDOW); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
    assertThat(driver.getWindowHandles().size()).isEqualTo(2);

    driver.switchTo().window(initHandle);
    driver.close();
    assertThat(driver.getWindowHandles().size()).isEqualTo(1);
}

1

在这个例子中,这一行不同。在这种情况下,我们打开一个新窗口(而不是标签页),并将焦点放在它上面。

框架和 iframes

示例 4-23 显示了一个测试,其中被测试的网页包含一个 iframe。示例 4-24 显示了相同的情况,但使用了框架集。

示例 4-23. 处理 iframes 的测试
@Test
void testIFrames() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/iframes.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions
            .frameToBeAvailableAndSwitchToIt("my-iframe")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    By pName = By.tagName("p");
    wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(pName, 0)); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    List<WebElement> paragraphs = driver.findElements(pName);
    assertThat(paragraphs).hasSize(20); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们打开一个包含 iframe 的网页(见 图 4-9)。

2

我们使用显式等待来等待框架并切换到它。

3

我们使用另一个显式等待来暂停,直到 iframe 中包含的段落可用。

4

我们断言段落的数量与预期相同。

hosw 0409

图 4-9. 使用 iframe 的练习网页
示例 4-24. 框架处理测试
@Test
void testFrames() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/frames.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    String frameName = "frame-body";
    wait.until(ExpectedConditions
            .presenceOfElementLocated(By.name(frameName))); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    driver.switchTo().frame(frameName); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    By pName = By.tagName("p");
    wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(pName, 0));
    List<WebElement> paragraphs = driver.findElements(pName);
    assertThat(paragraphs).hasSize(20);
}

1

我们打开一个包含框架集的网页(见 图 4-10)。

2

我们等待框架可用。注意,示例 4-23 中的步骤 2 和 3 等同于此步骤。

3

我们将焦点切换到这个框架。

hosw 0410

图 4-10. 使用框架的练习网页

对话框

JavaScript 提供了不同的对话框(有时称为 弹出窗口)与用户交互,包括:

警告

要显示一个消息并等待用户按下 OK 按钮(对话框中的唯一选择)。例如,以下代码将打开一个对话框,显示“你好,世界!”并等待用户按下 OK 按钮。

alert("Hello world!");

确认

要显示一个带有问题和两个按钮(确定和取消)的对话框。例如,以下代码将打开一个对话框,显示消息“这正确吗?”并提示用户单击确定或取消。

let correct = confirm("Is this correct?");

提示

要显示一个带有文本消息、输入文本字段和 OK 和 Cancel 按钮的对话框。例如,以下代码显示了一个弹出窗口,显示“请输入您的姓名”,用户可以在其中输入,以及两个按钮(确定和取消)。

let username = prompt("Please enter your name");

此外,CSS 允许实现另一种称为 模态窗口 的对话框。此对话框会禁用主窗口(但保持可见性),同时覆盖一个子弹出窗口,通常显示消息和一些按钮。你可以在包含所有这些对话框(警告、确认、提示和模态)的练习网页上找到一个示例页面。图 4-11 显示了此页面的截图,模态对话框处于活动状态。

hosw 0411

图 4-11. 包含对话框(警告、确认、提示和模态)的练习网页

警告、确认和提示

Selenium WebDriver API 提供了Alert接口来操作 JavaScript 对话框(如警告、确认和提示框)。表 4-9 描述了此接口提供的方法。接下来,示例 4-25 展示了与警告交互的基本测试。

表 4-9. 警告方法

方法 返回 描述

|

accept()

|

void
单击确定

|

getText()

|

String
读取对话框消息

|

dismiss()

|

void
单击取消(在警告中不可用)

|

sendKeys(String text)

|

void
在输入文本框中输入一些字符串(仅适用于提示)
示例 4-25. 测试处理警告对话框
@Test
void testAlert() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/dialog-boxes.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));

    driver.findElement(By.id("my-alert")).click(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    wait.until(ExpectedConditions.alertIsPresent()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    Alert alert = driver.switchTo().alert(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(alert.getText()).isEqualTo("Hello world!"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    alert.accept(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们打开启动对话框的练习网页。

2

我们单击左侧按钮启动 JavaScript 警告。

3

我们等待警告对话框显示在屏幕上。

4

我们切换焦点到警告弹出窗口。

5

我们验证警告文本是否符合预期。

6

我们点击警告对话框的确定按钮。

我们可以用一个显式等待语句来替换步骤 3 和 4,如下所示(您可以在示例库中同一类中的第二个测试中找到它):

Alert alert = wait.until(ExpectedConditions.alertIsPresent());

下一个测试(示例 4-26)说明了如何处理确认对话框。请注意,这个示例与前一个示例非常相似,但在这种情况下,我们可以调用dismiss()方法单击确认对话框上的取消按钮。最后,示例 4-27 展示了如何处理提示对话框。在这种情况下,我们可以在输入文本框中输入字符串。

示例 4-26. 测试处理确认对话框
@Test
void testConfirm() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/dialog-boxes.html");
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));

    driver.findElement(By.id("my-confirm")).click();
    wait.until(ExpectedConditions.alertIsPresent());
    Alert confirm = driver.switchTo().alert();
    assertThat(confirm.getText()).isEqualTo("Is this correct?");
    confirm.dismiss();
}
示例 4-27. 测试处理提示对话框
@Test
void testPrompt() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/dialog-boxes.html");
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));

    driver.findElement(By.id("my-prompt")).click();
    wait.until(ExpectedConditions.alertIsPresent());
    Alert prompt = driver.switchTo().alert();
    prompt.sendKeys("John Doe");
    assertThat(prompt.getText()).isEqualTo("Please enter your name");
    prompt.accept();
}

模态窗口

模态窗口是由基本的 CSS 和 HTML 构建的对话框。因此,Selenium WebDriver 不提供任何特定于操作它们的工具。相反,我们使用标准的 WebDriver API(定位器、等待等)与模态窗口进行交互。示例 4-28 展示了使用包含对话框的练习网页的基本测试。

示例 4-28. 测试处理模态对话框
@Test
void testModal() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/dialog-boxes.html");
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));

    driver.findElement(By.id("my-modal")).click();
    WebElement close = driver
            .findElement(By.xpath("//button[text() = 'Close']"));
    assertThat(close.getTagName()).isEqualTo("button");
    wait.until(ExpectedConditions.elementToBeClickable(close));
    close.click();
}

Web 存储

Web Storage API 允许 Web 应用在客户端文件系统中本地存储数据。此 API 提供两个 JavaScript 对象:

window.localStorage

永久存储数据

window.sessionStorage

在会话期间存储数据(当浏览器选项卡关闭时,数据将被删除)

Selenium WebDriver 为操作 Web Storage API 提供了 WebStorage 接口。由 Selenium WebDriver 支持的大多数 WebDriver 类型都继承了此接口:ChromeDriverEdgeDriverFirefoxDriverOperaDriverSafariDriver。这样,我们可以在这些浏览器中使用此功能。Example 4-29 在 Chrome 中展示了这一用法。此测试使用了本地存储和会话存储两种类型的 Web Storage。

Example 4-29. 使用 Web Storage 的测试
@Test
void testWebStorage() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/web-storage.html");
    WebStorage webStorage = (WebStorage) driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    LocalStorage localStorage = webStorage.getLocalStorage();
    log.debug("Local storage elements: {}", localStorage.size()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    SessionStorage sessionStorage = webStorage.getSessionStorage();
    sessionStorage.keySet()
            .forEach(key -> log.debug("Session storage: {}={}", key,
                    sessionStorage.getItem(key))); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(sessionStorage.size()).isEqualTo(2);

    sessionStorage.setItem("new element", "new value");
    assertThat(sessionStorage.size()).isEqualTo(3); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    driver.findElement(By.id("display-session")).click();
}

1

我们将驱动对象转换为 WebStorage

2

我们记录本地存储元素的数量。

3

我们记录会话存储(它应该包含两个元素)。

4

添加新元素后,会话存储中应该有三个元素。

事件监听器

Selenium WebDriver API 允许创建监听器,通知发生在 WebDriver 及其派生对象中的事件。在之前的 Selenium WebDriver 版本中,可以通过 EventFiringWebDriver 类访问此功能。随着 Selenium WebDriver 4 的推出,该类已被弃用,现在应改用以下内容:

EventFiringDecorator

用于 WebDriver 及其派生对象(例如 WebElementTargetLocator 等)的包装类。它允许注册一个或多个监听器(即 WebDriverListener 实例)。

WebDriverListener

应实现注册在装饰器中的监听器的接口。它支持三种类型的事件:

Before 事件

在某个事件开始前插入的逻辑

After 事件

在某个事件终止后插入的逻辑

Error 事件

在抛出异常之前插入的逻辑

要实现事件监听器,首先我们应创建一个监听器类。换句话说,我们需要创建一个实现 WebDriverListener 的类。此接口使用 default 关键字定义了所有方法,因此可选择性地重写这些方法。得益于这个特性(从 Java 8 开始提供),我们的类只需实现我们需要的方法即可。有许多监听器方法可用,例如 afterGet()(在调用 get() 方法后执行)、beforeQuit()(在调用 quit() 方法前执行),等等。建议使用您喜欢的 IDE 查看可以重写/实现的可能方法。Figure 4-12 在 Eclipse 中展示了执行此操作的向导。

hosw 0412

Figure 4-12. Eclipse 中的 WebDriverListener 方法

一旦我们实现了我们的监听器,我们需要创建装饰类。有两种方法可以做到这一点。如果我们想要装饰一个 WebDriver 对象,我们可以创建一个 EventFiringDecorator 实例(将监听器作为参数传递给构造函数),然后调用 decorate() 方法来传递 WebDriver 对象。例如:

WebDriver decoratedDriver = new EventFiringDecorator(myListener)
        .decorate(originalDriver);

第二种方法是装饰 Selenium WebDriver API 的其他对象,即 WebElementTargetLocatorNavigationOptionsTimeoutsWindowAlertVirtualAuthenticator。在这种情况下,我们需要在 EventFiringDecorator 对象中调用 createDecorated() 方法以获得一个 Decorated<T> 泛型类。以下片段展示了使用 WebElement 作为参数的示例:

Decorated<WebElement> decoratedWebElement = new EventFiringDecorator(
        listener).createDecorated(myWebElement);

让我们看一个完成的示例。首先,示例 4-30 展示了实现 WebDriverListener 接口的类。请注意,此类实现了两个方法:afterGet()beforeQuit()。这两个方法都调用 takeScreenshot() 来获取浏览器截图。总之,我们在加载网页后(通常在测试开始时)和退出之前(通常在测试结束时)收集浏览器截图。接下来,示例 4-31 展示了使用此监听器的测试。

示例 4-30. 实现 afterGet()beforeQuit() 方法的事件监听器
public class MyEventListener implements WebDriverListener {

    static final Logger log = getLogger(lookup().lookupClass());

    @Override
    public void afterGet(WebDriver driver, String url) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        WebDriverListener.super.afterGet(driver, url);
        takeScreenshot(driver);
    }

    @Override
    public void beforeQuit(WebDriver driver) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        takeScreenshot(driver);
    }

    private void takeScreenshot(WebDriver driver) {
        TakesScreenshot ts = (TakesScreenshot) driver;
        File screenshot = ts.getScreenshotAs(OutputType.FILE);
        SessionId sessionId = ((RemoteWebDriver) driver).getSessionId();
        Date today = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                "yyyy.MM.dd_HH.mm.ss.SSS");
        String screenshotFileName = String.format("%s-%s.png",
                dateFormat.format(today), sessionId.toString());
        Path destination = Paths.get(screenshotFileName); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        try {
            Files.move(screenshot.toPath(), destination);
        } catch (IOException e) {
            log.error("Exception moving screenshot from {} to {}", screenshot,
                    destination, e);
        }
    }

}

1

我们重写此方法以在使用 WebDriver 对象加载网页后执行自定义逻辑

2

我们重写此方法以在退出 WebDriver 对象之前执行自定义逻辑 之前

3

我们为 PNG 截图使用唯一的名称。为此,我们获取系统日期(日期和时间)加上会话标识符。

示例 4-31. 使用 EventFiringDecorator 和前一个监听器进行测试
class EventListenerJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        MyEventListener listener = new MyEventListener();
        WebDriver originalDriver = WebDriverManager.chromedriver().create();
        driver = new EventFiringDecorator(listener).decorate(originalDriver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testEventListener() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle())
                .isEqualTo("Hands-On Selenium WebDriver with Java");
        driver.findElement(By.linkText("Web form")).click(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

}

1

我们使用 MyEventListener 实例创建装饰后的 WebDriver 对象。我们使用生成的 driver@Test 逻辑中控制浏览器。

2

我们单击网页链接以更改页面。监听器中获取的两个结果截图应不同。

WebDriver 异常

WebDriver API 提供的所有异常都继承自 WebDriverException 类,且为 unchecked 异常(如果您对此术语不熟悉,请参阅以下边栏)。图 4-13 展示了 Selenium WebDriver 4 中的这些异常。正如此图所示,有许多不同的异常类型。表 4-10 总结了一些最常见的原因。

hosw 0413

图 4-13. Selenium WebDriver 异常

表 4-10. 常见 WebDriver 异常及其常见原因

异常 描述 常见原因

|

NoSuchElementException
Web 元素不可用
  • 定位策略无效

  • 元素尚未渲染(可能需要等待它)

|

|

NoAlertPresentException
对话框(警报、提示或确认框)不可用 尝试执行无法使用的对话框操作(例如 accept()dismiss()

|

NoSuchWindowException
窗口或标签不可用 尝试切换到不可用的窗口或标签

|

NoSuchFrameException
框架或 iframe 不可用 尝试切换到不可用的框架或 iframe

|

InvalidArgumentException
调用 Selenium WebDriver API 某些方法时参数不正确
  • 导航方法中的错误 URL

  • 上传文件时路径不存在

  • JavaScript 脚本中的错误参数类型

|

|

StaleElementReferenceException
元素已经 过时,即不再出现在页面上 当尝试与先前定位的元素交互时,DOM 被更新

|

UnreachableBrowserException
与浏览器通信问题
  • 无法建立与远程浏览器的连接

  • WebDriver 会话中浏览器在中途崩溃

|

|

TimeoutException
页面加载超时 某些网页加载时间超过预期

|

ScriptTimeoutException
脚本加载超时 某些脚本执行时间超过预期

|

ElementNotVisibleException
ElementNotSelectableException
ElementClickInterceptedException
元素在 DOM 中但不可见/可选择/可点击
  • 等待直到元素显示/可选择/可点击不足(或不存在)

  • 页面布局(可能由视口变化引起)导致该元素覆盖我们尝试与之交互的元素

|

概述与展望

本章全面回顾了在不同 Web 浏览器中可互操作的 WebDriver API 特性。您在其中了解了如何使用 Selenium WebDriver 执行 JavaScript,包括同步、固定(即附加到 WebDriver 会话)和异步脚本。然后,您学习了超时的使用,用于指定页面加载和脚本执行的时间限制间隔。此外,您还看到了如何管理多个浏览器方面,如大小和位置、导航历史、阴影 DOM 和 cookies。接下来,您了解了如何与特定的 Web 元素交互,如下拉列表(选择框和数据列表)、导航目标(窗口、标签、框架和 iframe)和对话框(警报、提示、确认框和模态框)。最后,我们回顾了在 Selenium WebDriver 4 中实现 Web 存储和事件监听器的机制以及最相关的 WebDriver 异常(及其常见原因)。

下一章继续揭示 Selenium WebDriver API 的特性。该章节详细解释了针对特定浏览器(例如 Chrome、Firefox 等)的特定方面,包括浏览器功能(例如 ChromeOptionsFirefoxOptions 等)、Chrome DevTools Protocol(CDP)、网络拦截、地理位置模拟坐标、WebDriver 双向协议(BiDi)、认证机制或打印网页为 PDF 等功能。

第五章:浏览器特定操作

正如你所见,Selenium WebDriver API 的许多特性在各种浏览器中是兼容的,即我们可以使用 Selenium WebDriver 以编程方式控制不同类型的浏览器。Selenium WebDriver API 的其他部分在浏览器之间是不可互操作的。换句话说,一些 WebDriver 特性对于某些浏览器(如 Chrome 或 Edge)是可用的,而对于其他浏览器(如 Firefox)则不可用(或不同)。本章回顾了这些特定于浏览器的特性。

浏览器 Capabilities

Selenium WebDriver 允许通过capabilities来指定特定于浏览器的方面。例如,无头模式、页面加载策略、使用网页扩展或推送通知管理等功能。正如图 5-1 所示,Selenium WebDriver API 提供了一组 Java 类来定义这些 capabilities。Capabilities接口位于这个层次结构的顶部。在内部,capabilities 接口使用键-值对处理数据,封装浏览器的特定方面。然后,不同的 Java 类实现此接口,为 Web 浏览器(如 Chrome、Edge、Firefox 等)指定 capabilities。表 5-1 总结了Capabilities层次结构的主要类及其对应的目标浏览器。

hosw 0501

图 5-1. Capabilities 层次结构

表 5-1. Capabilities 层次结构描述

浏览器

|

org.openqa.selenium

|

MutableCapabilities
通用(跨浏览器)

|

org.openqa.selenium.chrome

|

ChromeOptions
Chrome

|

org.openqa.selenium.edge

|

EdgeOptions
Edge

|

org.openqa.selenium.firefox

|

FirefoxOptions
Firefox

|

org.openqa.selenium.safari

|

SafariOptions
Safari

|

org.openqa.selenium.opera

|

OperaOptions
Opera

|

org.openqa.selenium.ie

|

InternetExplorerOptions
Internet Explorer

|

org.openqa.selenium.remote

|

DesiredCapabilities
远程浏览器(参见第六章)

以下小节将回顾本书讨论的主要 Web 浏览器(如 Chrome、Edge 和 Firefox)的最重要 capabilities。由于 Chrome 和 Edge 都是基于 Chromium 的浏览器,因此两者可用的 capabilities 是等效的。这一事实反映在图 5-1 中,显示了ChromeOptionsEdgeOptions这两个 capability 类都继承自同一个父类(称为ChromiumOptions)。

无头浏览器

不需要图形界面与 Web 应用程序进行交互的浏览器称为 无头 浏览器。这些浏览器的主要用途之一是端到端测试,即自动化与 Web 应用程序的交互。目前的 Web 浏览器如 Chrome、Edge 或 Firefox 都可以作为无头浏览器运行。Selenium WebDriver API 允许使用能力(capabilities)在无头模式下启动这些浏览器。为此,首先需要创建一个浏览器能力(capabilities)的实例。在主要浏览器中,这些对象分别是 ChromeOptionsEdgeOptionsFirefoxOptions 的实例。然后,通过调用浏览器能力对象中的 setHeadless(true) 方法来启用无头模式。最后,在创建 WebDriver 对象时设置这些能力。

正如 “WebDriver 创建” 所解释的,我们有不同的方法来创建 WebDriver 对象。首先,我们可以使用 WebDriver 构造函数(例如 new ChromeDriver())。此外,我们可以使用 Selenium WebDriver API 提供的构建器(即 RemoteWebDriver.builder())。最后,我们可以使用 WebDriverManager 构建器解析驱动程序,并在一行代码中创建 WebDriver 实例。以下示例展示了这些替代方法,与浏览器能力结合使用,以启用无头浏览器模式,即:

  • 示例 5-1 使用 Chrome 无头模式。此示例使用所需的构造函数(在本例中为 ChromeDriver)创建一个 WebDriver 实例。

  • 示例 5-2 使用 Edge 无头模式。此示例使用 Selenium WebDriver API 中提供的构建器创建一个 WebDriver 实例。

  • 示例 5-3 使用 Firefox 无头模式。此示例使用 WebDriverManager 创建一个 WebDriver 实例。请注意,在这种情况下,不需要设置方法,因为 WebDriverManager 在 WebDriver 实例化的同一行解析了驱动程序。

  • 示例 5-4 通过 Selenium-Jupiter 使用 Chrome 无头模式。此示例使用 Selenium-Jupiter 提供的参数解析机制,因此我们只需在测试方法中声明一个 ChromeDriver 参数。然后,我们使用注解 @Arguments 对这个参数进行修饰,以指定该浏览器的无头模式。

示例 5-1. 使用 Chrome 无头模式进行测试
class HeadlessChromeJupiterTest {

    WebDriver driver;

    @BeforeAll
    static void setupClass() {
        WebDriverManager.chromedriver().setup(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @BeforeEach
    void setup() {
        ChromeOptions options = new ChromeOptions(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        options.setHeadless(true); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        driver = new ChromeDriver(options); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testHeadless() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们解析所需的驱动程序(在本例中为 chromedriver)。

2

我们使用 ChromeOptions 构造函数创建浏览器能力。

3

我们启用无头模式。这一行相当于 options.add​Arguments("--headless");

4

我们通过在ChromeDriver构造函数中将选项作为参数来设置浏览器能力。

示例 5-2. 使用 Edge 浏览器的无头模式测试
class HeadlessEdgeJupiterTest {

    WebDriver driver;

    @BeforeAll
    static void setupClass() {
        WebDriverManager.edgedriver().setup(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @BeforeEach
    void setup() {
        EdgeOptions options = new EdgeOptions(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        options.setHeadless(true); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        driver = RemoteWebDriver.builder().oneOf(options).build(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testHeadless() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

与往常一样,我们需要解析所需的驱动程序(本例中是 msedgedriver)。

2

由于我们打算使用 Edge,因此我们需要创建一个EdgeOptions实例来指定能力。

3

我们启用无头模式。同样,这行等同于options.addArguments("--headless");

4

我们使用 WebDriver 构建器来创建WebDriver对象,并将选项作为参数传递。

示例 5-3. 使用 Firefox 无头模式测试
class HeadlessFirefoxJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        FirefoxOptions options = new FirefoxOptions(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        options.setHeadless(true); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

        driver = WebDriverManager.firefoxdriver().capabilities(options)
                .create(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testHeadless() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

在这个测试中,我们使用 Firefox,因此我们创建一个FirefoxOptions对象来指定能力。

2

与前面的示例相同,我们启用了无头模式。

3

在此示例中,我们使用 WebDriverManager 来解析所需的驱动程序,并创建WebDriver对象,同时指定先前创建的浏览器能力。

注意

在这些示例中,创建WebDriver对象的策略是可以互换的。换句话说,例如,我们也可以为每个浏览器的无头模式使用 WebDriverManager 构建器。

示例 5-4. 使用 Selenium-Jupiter 中的 Chrome 无头模式测试
@ExtendWith(SeleniumJupiter.class)
class HeadlessChromeSelJupTest {

    @Test
    void testHeadless(@Arguments("--headless") ChromeDriver driver) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们使用注解@Arguments来指定浏览器(本例中为 Chrome)中的无头模式。

页面加载策略

Selenium WebDriver 允许配置不同的页面加载方式。为此,Selenium WebDriver API 提供了PageLoadStrategy枚举。表 5-2 描述了该枚举的可能值及其目的。Selenium WebDriver 内部使用 DOM API 的document.readyState属性来检查网页加载状态。

表 5-2. PageLoadStrategy 值

加载策略 描述 准备状态

|

PageLoadStrategy.NORMAL
默认模式。Selenium WebDriver 等待整个页面加载完成(即 HTML 内容和子资源,如样式表、图像、JavaScript 文件等)。
"complete"

|

|

PageLoadStrategy.EAGER
Selenium WebDriver 等待 HTML 文档完成加载和解析,但子资源(脚本、图像、样式表等)仍在加载中。
"interactive"

|

|

PageLoadStrategy.NONE
Selenium WebDriver 仅等待 HTML 文档下载完成。
"loading"

|

我们需要调用浏览器能力(例如ChromeOptionsFirefoxOptions等)的setPageLoadStrategy()方法来设置这些策略(NORMALEAGERNONE)。示例 5-5 展示了使用 Chrome 和NORMAL策略的测试。在示例库中,您可以找到使用其他策略(EAGERNONE)的 Edge 和 Firefox 的等效示例。在这些示例中,除了在测试设置中指定加载策略外,测试逻辑还计算加载页面所需的时间,并在标准输出中显示此值。

示例 5-5. 在 Chrome 中使用正常页面加载策略的测试
class PageLoadChromeJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;

    PageLoadStrategy pageLoadStrategy;

    @BeforeEach
    void setup() {
        ChromeOptions options = new ChromeOptions(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        pageLoadStrategy = PageLoadStrategy.NORMAL;
        options.setPageLoadStrategy(pageLoadStrategy); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

        driver = WebDriverManager.chromedriver().capabilities(options).create(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testPageLoad() {
        long initMillis = System.currentTimeMillis(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        Duration elapsed = Duration
                .ofMillis(System.currentTimeMillis() - initMillis); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

        Capabilities capabilities = ((RemoteWebDriver) driver)
                .getCapabilities(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
        Object pageLoad = capabilities
                .getCapability(CapabilityType.PAGE_LOAD_STRATEGY); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
        String browserName = capabilities.getBrowserName();
        log.debug(
                "The page took {} ms to be loaded using a '{}' strategy in {}",
                elapsed.toMillis(), pageLoad, browserName); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)

        assertThat(pageLoad).isEqualTo(pageLoadStrategy.toString()); ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png)
    }

}

1

由于我们在此测试中使用 Chrome,我们实例化ChromeOptions以指定能力。

2

我们设置页面加载策略为NORMAL

3

我们使用 WebDriverManager 解析驱动程序,创建WebDriver实例并指定能力。

4

在加载页面之前获取系统时间戳。

5

在加载页面后获取系统时间戳。

6

我们读取WebDriver对象的能力。

7

我们读取了所用的页面加载策略。

8

我们追踪加载网页所需的时间。

9

我们验证加载策略是否如最初配置的那样。

设备仿真

主要的网页浏览器使用开发工具(即 Chromium 基浏览器中的 DevTools 和 Firefox 中的 Developer Tools)以以下方式模拟移动设备:

模拟移动视窗

使用给定移动设备的宽度和高度减少网页的用户可见区域

限制网络带宽

为了模拟移动网络(例如 3G),需要降低连接速度。

限制 CPU 速度

减慢处理性能

模拟地理位置

设置自定义全球定位系统(GPS)坐标

设置方向

旋转屏幕

图 5-2 展示了通过 DevTools 在 Chrome 中使用移动仿真的屏幕截图。

hosw 0502

图 5-2. 使用 Chrome 开发工具进行移动仿真

在撰写本文时,这种移动设备仿真可以通过 Selenium WebDriver API 在基于 Chromium 的浏览器(Chrome 和 Edge)中自动化,但在 Firefox 中不能(因为 geckodriver 中未实现)。为此,我们需要在ChromeOptionsEdgeOptions中设置实验性选项mobileEmulation

然后,有两种方法来指定要模拟的移动设备。首先,我们可以指定特定的移动设备(例如,Pixel 2、iPad Pro 或 Galaxy Fold 等)。由于此列表在每个 Chromium 发布版中更新,检查可能性的最佳方法是检查 DevTools 中可用设备(例如,图 5-2 中选择了 iPhone X)。示例 5-6 展示了一个测试设置,其中我们使用标签 iPhone 6/7/8 指定了特定的移动设备。

示例 5-6. 通过指定设备进行移动模拟的测试设置
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    Map<String, Object> mobileEmulation = new HashMap<>(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    mobileEmulation.put("deviceName", "iPhone 6/7/8"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.setExperimentalOption("mobileEmulation", mobileEmulation); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们需要创建一个 HashMap 对象来指定移动模拟选项。

2

然后,我们只需要选择设备名称(在本例中为 iPhone 6/7/8)。

3

我们使用实验选项设置设备模拟。

4

通常情况下,我们创建一个指定这些选项的 WebDriver 对象。

设置移动模拟的第二种选择是指定模拟设备的各个属性。这些属性包括:

width

设备屏幕宽度(以像素为单位)

height

设备屏幕高度(以像素为单位)

pixelRatio

物理像素与逻辑像素之间的比率

touch

是否模拟触摸事件;默认值为 true

除了这些属性之外,我们还可以指定模拟设备的 用户代理。在 HTTP 中,用户代理是指定在请求标头中的字符串,唯一地标识 Web 浏览器的类型。它包含开发代号、版本、平台和其他信息。示例 5-7 展示了一个测试设置,说明了此功能的使用。

示例 5-7. 通过指定各个属性进行设备模拟的测试设置
@BeforeEach
void setup() {
    EdgeOptions options = new EdgeOptions();
    Map<String, Object> mobileEmulation = new HashMap<>();
    Map<String, Object> deviceMetrics = new HashMap<>(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    deviceMetrics.put("width", 360);
    deviceMetrics.put("height", 640);
    deviceMetrics.put("pixelRatio", 3.0);
    deviceMetrics.put("touch", true);
    mobileEmulation.put("deviceMetrics", deviceMetrics);  ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    mobileEmulation.put("userAgent",
            "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) "
                    + "AppleWebKit/535.19 (KHTML, like Gecko) "
                    + "Chrome/18.0.1025.166 Mobile Safari/535.19");  ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    options.setExperimentalOption("mobileEmulation", mobileEmulation);

    driver = WebDriverManager.edgedriver().capabilities(options).create();
}

1

我们创建了一个哈希映射来存储模拟移动设备的各个属性,即,widthheightpixelRatiotouch

2

我们通过在移动模拟映射中设置标签 deviceMetrics 来设置这些属性。

3

我们为 Nexus 5 设备中的 Chrome Mobile 18 设置了自定义用户代理。

网页扩展

网页扩展(也称为 插件附加组件)是可以修改或增强 Web 浏览器默认操作的程序。用户通常使用 Web 商店安装网页扩展。这些商店是由浏览器维护者支持的 Web 应用程序,用于托管公共 Web 扩展。表 5-3 总结了 Chrome、Edge 和 Firefox 的 Web 商店。

表 5-3. 主要浏览器的网络商店

网络商店 浏览器 网址
Chrome 网上应用店 Chrome https://chrome.google.com/webstore/category/extensions
Edge 插件 Edge https://microsoftedge.microsoft.com/addons/Microsoft-Edge-Extensions-Home
Firefox 浏览器插件 Firefox https://addons.mozilla.org/zh-CN/firefox

我们可以在 WebDriver 会话中使用功能来安装网页扩展。在基于 Chromium 的浏览器中,如 Chrome 和 Edge,我们使用 ChromeOptionsEdgeOptions 对象的 addExtensions() 方法。示例 5-8 展示了在 Chrome 中安装本地扩展的测试设置。

示例 5-8. 在 Chrome 中安装网页扩展的测试设置
@BeforeEach
void setup() throws URISyntaxException {
    Path extension = Paths
            .get(ClassLoader.getSystemResource("dark-bg.crx").toURI()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    ChromeOptions options = new ChromeOptions();
    options.addExtensions(extension.toFile()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

1

我们安装一个打包为 Chrome 扩展(CRX)文件的网页扩展。这个文件是测试资源(位于 Java 项目的 src\test\resources 文件夹中)。该扩展会改变网站的外观和感觉,以在黑色背景上使用浅色文本。图 5-3 展示了使用 WebDriver 测试加载此扩展时的实践网站的截图。

2

我们在 Chrome 选项中添加扩展,将扩展作为 Java File 传递。

hosw 0503

图 5-3. 使用 dark-bg.crx 扩展加载时的实践站点

当 Firefox 由 WebDriver 控制时,也允许加载网页扩展。然而,语法有所不同。示例 5-9 说明了这一点。

示例 5-9. 在 Firefox 中安装网页扩展的测试设置
@BeforeEach
void setup() throws URISyntaxException {
    Path extension = Paths
            .get(ClassLoader.getSystemResource("dark-bg.xpi").toURI()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    FirefoxOptions options = new FirefoxOptions();
    FirefoxProfile profile = new FirefoxProfile(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    profile.addExtension(extension.toFile()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    options.setProfile(profile); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

1

我们在 Firefox 中使用与 Chrome/Edge 相同的扩展,但在这种情况下,打包是专门针对 Firefox 的。请注意文件是不同的。这次,它被打包为 XPInstall 文件,即包含网页扩展源代码、资源(例如图像)和元数据的压缩归档。

2

我们需要创建一个自定义的 Firefox 配置文件(即存储自定义设置的地方)。

3

我们将扩展作为 Java File 添加到 Firefox 配置文件中。

4

我们在 Firefox 选项中设置配置文件。

基于 Chromium 的浏览器(例如 Chrome、Edge)还允许从源代码加载扩展(即未打包为 CRX 文件)。这个功能在开发期间自动测试网页扩展非常方便。示例 5-10 展示了说明此功能的测试设置。

示例 5-10. 从源代码中安装 Edge 中的网页扩展的测试设置
@BeforeEach
void setup() throws URISyntaxException {
    Path extension = Paths
            .get(ClassLoader.getSystemResource("web-extension").toURI()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    EdgeOptions options = new EdgeOptions();
    options.addArguments(
            "--load-extension=" + extension.toAbsolutePath().toString()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.edgedriver().capabilities(options).create();
}

1

此示例中使用的扩展位于web-extension文件夹中;这是一个测试资源文件夹(存储在 Java 项目的src\test\resources中)。此扩展遵循浏览器扩展 API。它使用 JavaScript 来更改一级标题(h1 标签)的内容为自定义消息。图 5-4 显示了使用此扩展时练习网站的屏幕截图。

2

我们使用 --load-extension 参数指定扩展路径。

注意

Selenium WebDriver 每次执行时都会创建一个新的浏览器配置文件。因此,通过 Selenium WebDriver 安装的 Web 扩展在目标浏览器中不是永久性的。

hosw 0504

图 5-4. 使用本地扩展加载练习网站时的情况

自 Selenium 4.1 起,Firefox 也允许从其源代码安装 Web 扩展。为此,FirefoxDriver 扩展了接口 HasExtensions,提供了方法 installExtension。示例 5-11 展示了使用此功能的测试设置。

示例 5-11. 在 Firefox 中安装 Web 扩展的测试设置
@BeforeEach
void setup() throws URISyntaxException {
    Path extensionFolder = Paths
            .get(ClassLoader.getSystemResource("web-extension").toURI()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    zippedExtension = zipFolder(extensionFolder); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.firefoxdriver().create();
    ((FirefoxDriver) driver).installExtension(zippedExtension, true); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们使用位于项目类路径中的 Web 扩展源代码。

2

方法 installExtension 要求从其源代码安装的扩展是压缩的。WebDriverManager 提供了名为 zipFolder(Path) 的静态辅助方法来简化此过程。

3

我们将压缩的扩展作为临时附加组件安装到 Firefox 中。

地理位置

地理位置 API 是一个 W3C 规范,允许访问与 Web 浏览器的主机设备(如笔记本电脑或移动设备)关联的地理位置信息。通常的地理位置数据源包括 GPS 数据和从网络推断的位置,例如 IP 地址。地理位置 API 可通过调用 JavaScript 对象 navigator.geolocation 在 Web 浏览器中使用。出于隐私原因,使用此语句时,会弹出提示用户允许报告位置数据。

实践站点包含一个使用地理位置的网页。图 5-5 展示了此页面的屏幕截图。该图显示了用户点击“获取坐标”按钮时显示给用户的权限弹出窗口。为了使用 Selenium WebDriver API 处理此对话框,我们使用功能。与其他情况一样,Chrome/Edge 中授予访问地理位置数据所需的功能不同于 Firefox。以下代码片段展示了这种差异。首先,示例 5-12 展示了在 Chrome 中授予地理位置访问权限的测试设置。Edge 中将使用相同的实验性偏好(profile.default_content_setting_values.geolocation),你可以在示例存储库中找到完整的测试。接下来,示例 5-13 展示了等效的 Firefox 测试设置。

hosw 0505

图 5-5. 显示地理位置权限弹出窗口的实践站点
示例 5-12. 在 Chrome 中允许地理位置的测试设置
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    Map<String, Object> prefs = new HashMap<>(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    prefs.put("profile.default_content_setting_values.geolocation", 1); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.setExperimentalOption("prefs", prefs); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

1

我们为实验选项创建一个哈希映射。

2

我们将实验选项 profile.default_content_setting_​val⁠ues.geolocation 设置为 1,以允许访问地理位置。其他可能的值包括:默认行为的 0 和阻止访问地理位置数据的 2

3

我们使用 Chrome 选项中的 prefs 标签设置实验选项。

注意

假设你需要在 macOS 设备的 Chrome 或 Edge 中访问地理位置坐标。那么,你还需要在 macOS 的偏好设置(系统偏好设置 → 安全性与隐私 → 位置服务)中启用这些浏览器的位置服务。图 5-6 展示了这个配置。

hosw 0506

图 5-6. 在 macOS 中为 Chrome 和 Edge 启用位置服务
示例 5-13. 在 Firefox 中允许地理位置的测试设置
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    options.addPreference("geo.enabled", true); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.addPreference("geo.prompt.testing", true); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.addPreference("geo.provider.use_corelocation", true); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

1

要启用地理位置 API

2

要允许访问地理位置数据(即点击访问弹出窗口中的 allow

3

要使用设备中所有可用组件(如 GPS、WiFi 或蓝牙)收集数据

通知

通知 API是一个标准的 Web API,允许网站发送显示在操作系统桌面上的通知。该 API 通过 JavaScript 对象Notification提供。在网站能够发送通知之前,用户必须授予权限。此授权类似于地理位置数据,在对话框弹出中提示用户同意。练习站点包含一个使用通知 API 的网页。图 5-7 显示了此页面的通知权限弹出窗口的屏幕截图。图 5-8 显示了此网页在 Linux 主机上发送的消息。

hosw 0507

图 5-7. 练习站点显示通知权限弹出窗口

hosw 0508

图 5-8. 练习站点在 Linux 桌面上显示通知

Selenium WebDriver API 允许通过使用能力来授予通知。与其他功能一样,这些能力的语法在 Chrome/Edge 和 Firefox 中有所不同。示例 5-14 展示了启用 Chrome 选项中通知的测试设置。我们在 Edge 中使用相同的偏好设置(profile.default_content_setting_values.notifications)来允许通知。示例 5-15 展示了 Firefox 的等效测试设置。在这种情况下,偏好标签(permissions.default.desktop-notification)不同,尽管其值(1)用于允许通知。另一个可能的值是2,用于阻止通知(在 Chrome/Edge 和 Firefox 中均适用)。

示例 5-14. 在 Chrome 中允许通知的测试设置
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    Map<String, Object> prefs = new HashMap<>();
    prefs.put("profile.default_content_setting_values.notifications", 1);
    options.setExperimentalOption("prefs", prefs);

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}
示例 5-15. 在 Firefox 中允许通知的测试设置
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    options.addPreference("permissions.default.desktop-notification", 1);

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

示例 5-16 展示了与之前设置一起使用的测试逻辑。通常情况下,您可以在示例存储库中找到完整的测试用例。这个测试是异步脚本执行的一个例子。此脚本覆盖了原始的Notification JavaScript 对象。此对象的新实现获取通知消息的标题,该标题在脚本回调中返回给 WebDriver 测试。

示例 5-16. 测试处理通知
@Test
void testNotifications() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/notifications.html");
    JavascriptExecutor js = (JavascriptExecutor) driver;

    String script = String.join("\n",
            "const callback = arguments[arguments.length - 1];", ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
            "const OldNotify = window.Notification;", ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
            "function newNotification(title, options) {", ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
            "    callback(title);", ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
            "    return new OldNotify(title, options);", ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
            "}",
            "newNotification.requestPermission = " +
                    "OldNotify.requestPermission.bind(OldNotify);",
            "Object.defineProperty(newNotification, 'permission', {",
            "    get: function() {",
            "        return OldNotify.permission;",
            "    }",
            "});",
            "window.Notification = newNotification;",
            "document.getElementById('notify-me').click();"); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    log.debug("Executing the following script asynchronously:\n{}", script);

    Object notificationTitle = js.executeAsyncScript(script); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    assertThat(notificationTitle).isEqualTo("This is a notification"); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

1

如在异步脚本执行中通常的那样,最后一个参数是用于信号化脚本终止的回调函数。

2

我们存储原始Notification构造函数的副本。

3

我们为通知创建一个新的构造函数。

4

我们将消息标题作为回调的一个参数传递。因此,标题将返回给 WebDriver 调用(在本例中为 Java)。

5

我们使用旧构造函数创建一个原始的Notification对象。

6

我们点击触发网页上通知的按钮。

7

我们获取脚本执行后返回的对象。

8

我们验证通知标题是否符合预期。

浏览器二进制

Selenium WebDriver 可以自动检测控制的网络浏览器(例如 Chrome、Firefox 等)的路径。尽管如此,我们可以使用 capabilities 指定浏览器可执行文件的自定义路径。当浏览器安装路径不是标准路径时(例如 beta/development/canary 浏览器的情况),这一功能非常有用。

我们使用相同的 capabilities 语法来指定 Chrome、Edge 和 Firefox 的二进制路径。示例 5-17 展示了使用 Chrome beta 的测试设置。

示例 5-17. 设置 Chrome 自定义二进制路径的测试设置
@BeforeEach
void setup() {
    Path browserBinary = Paths.get("/usr/bin/google-chrome-beta"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assumeThat(browserBinary).exists(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    ChromeOptions options = new ChromeOptions();
    options.setBinary(browserBinary.toFile()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

1

我们使用 Java 的 Path 来获取浏览器二进制路径(例如 Linux 下的 Chrome beta)。

2

我们使用假设条件,在前述路径不存在时条件性地跳过这个测试(例如在 CI 服务器上)。

3

我们使用 Chrome options 的 setBinary 方法来设置二进制路径(作为 Java File)。

网页代理

在计算机网络中,代理 是充当客户端和服务器之间中介的服务器。网页代理是浏览器和 Web 服务器之间的代理,可以用于多种目的,例如:

访问特定区域的信息

代理通常位于不同于客户端的区域,因此服务器相应地响应该区域。

避免限制

代理可以帮助访问被中间防火墙阻断的网站。

捕获网络流量

代理可以收集 HTTP 请求和响应。

缓存

代理可以加速网站检索速度。

图 5-9 展示了 Selenium WebDriver 架构中网页代理的位置,与不使用网页代理的典型场景进行了比较。可以看到,网页代理位于浏览器和测试中的网页应用程序之间,并且在 HTTP 层面上起作用。这样,网页代理允许在 Selenium WebDriver 测试中实现前面提到的目的(例如捕获 HTTP 网络流量)。

hosw 0509

图 5-9. Selenium WebDriver 架构,包括和不包括网页代理

Selenium WebDriver API 提供了一个 Proxy 类来配置网页代理。这个类通过 capabilities 配置到 WebDriver 对象中。示例 5-18 展示了具体用法。

示例 5-18. 配置网页代理的测试设置
@BeforeEach
void setup() {
    Proxy proxy = new Proxy(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String proxyStr = "proxy:port"; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    proxy.setHttpProxy(proxyStr); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    proxy.setSslProxy(proxyStr); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    ChromeOptions options = new ChromeOptions();
    options.setAcceptInsecureCerts(true); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    options.setProxy(proxy); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

1

我们创建了一个 Proxy 类的实例。

2

指定代理的语法是host:port

3

我们指定代理用于 HTTP 连接。

4

我们还指定代理用于 HTTPS 连接。

5

虽然不是强制要求,但通常需要接受不安全的证书。

6

我们将代理设置为一个能力。这一行等同于options.setCapability(CapabilityType.PROXY, proxy);

提示

“捕获网络流量” 展示了如何使用第三方库在 Selenium WebDriver 测试中通过使用 Web 代理来捕获网络流量。

日志收集

Selenium WebDriver API 允许收集不同的日志来源。这一功能通过能力来启用,尽管目前仅在基于 Chromium 的浏览器中支持。示例 5-19 展示了一个测试设置,用于收集浏览器日志(即控制台消息)。这段代码还包含了测试逻辑,在其中我们需要调用driver.manage().logs()来收集日志列表。

示例 5-19. 使用 Chrome 测试收集浏览器日志
@BeforeEach
void setup() {
    LoggingPreferences logs = new LoggingPreferences();
    logs.enable(LogType.BROWSER, Level.ALL); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    ChromeOptions options = new ChromeOptions();
    options.setCapability(CapabilityType.LOGGING_PREFS, logs); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

@Test
void testBrowserLogs() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/console-logs.html"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    LogEntries browserLogs = driver.manage().logs().get(LogType.BROWSER); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    Assertions.assertThat(browserLogs.getAll()).isNotEmpty(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    browserLogs.forEach(l -> log.debug("{}", l)); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们启用收集所有级别的浏览器日志。

2

我们设置loggingPrefs能力。

3

我们打开一个练习页面,在浏览器控制台中记录几条迹象。

4

我们收集所有日志,并按浏览器(控制台追踪)进行过滤。

5

我们验证追踪数量不为零。

6

我们将每个日志显示在标准输出中。

警告

在写作时,W3C WebDriver 规范中不支持日志收集。尽管如此,某些驱动程序已经实现了这一功能,如 chromedriver 或 msedgedriver(即 Chrome 和 Edge),但在其他驱动程序如 geckodriver(即 Firefox)中不可用。

获取用户媒体

WebRTC 是一组标准技术,允许使用 Web 浏览器交换实时媒体。这项技术允许使用客户端 JavaScript API 创建音频和视频会议的 Web 应用程序。实践站点包含一个网页,使用getUserMedia JavaScript API 获取用户媒体(麦克风和摄像头)。与其他 API 类似,出于安全和隐私考虑,浏览器弹出窗口在访问用户媒体前请求权限。图 5-10 展示了提示此对话框时的示例网页。

hosw 0510

图 5-10. 弹出用户媒体权限的实践网站

我们使用能力来在 Selenium WebDriver API 中授予用户媒体访问权限。这些能力在 Chrome 和 Edge 中的语法相同(参见示例 5-20),但在 Firefox 中不同(参见示例 5-21)。

示例 5-20. 在 Chrome 中设置合成用户媒体的测试
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--use-fake-ui-for-media-stream"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.addArguments("--use-fake-device-for-media-stream"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

1

允许访问用户媒体(音频和视频)的参数。

2

通过合成视频(绿色旋转器)和音频(每秒钟一声蜂鸣)来伪造用户媒体的参数。您可以在图 5-11 中看到这个视频。

hosw 0511

图 5-11. 在 Chrome 中使用合成用户媒体的实践网站
示例 5-21. 在 Firefox 中设置合成用户媒体的测试
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    options.addPreference("media.navigator.permission.disabled", true); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.addPreference("media.navigator.streams.fake", true); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

1

优先访问用户媒体。

2

偏好于使用合成视频(背景颜色变化)和音频(恒定蜂鸣声)来伪造用户媒体。您可以在图 5-12 中看到这个视频。

hosw 0512

图 5-12. 在 Firefox 中使用合成用户媒体的实践网站

加载不安全页面

当 Web 浏览器尝试加载使用 HTTPS(安全超文本传输协议)的网页,但服务器端的证书无效时,浏览器会向用户发出警告。无效证书的示例包括自签名、吊销或密码学上不安全的证书。图 5-13 展示了 Chrome 中此警告的截图。

hosw 0513

图 5-13. 使用不安全证书的网页

这个问题并不一定意味着安全问题。例如,在开发网站时使用自签名证书可能会发生这种情况。因此,Selenium WebDriver API 允许使用acceptInsecureCerts能力禁用证书检查。该能力在 Chrome、Edge 和 Firefox 中都相同。示例 5-22 展示了在 Chrome 中使用此能力的测试设置。此代码片段还包含一个打开不安全网站的测试。

示例 5-22. 测试使用不安全证书的 Web 应用程序
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    options.setAcceptInsecureCerts(true); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

@Test
void testInsecure() {
    driver.get("https://self-signed.badssl.com/"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    String bgColor = driver.findElement(By.tagName("body"))
            .getCssValue("background-color");
    Color red = new Color(255, 0, 0, 1);
    assertThat(Color.fromString(bgColor)).isEqualTo(red); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们启用了允许不安全证书的能力。

2

我们打开了一个使用不安全证书(本例中为自签名证书)的网站。

3

如果网站已加载,则页面背景应为红色。

本地化

在软件工程中,本地化 指的是将应用程序调整为满足其最终用户文化和语言(称为语言环境)的过程。本地化有时被写作 l10n(10 是英语单词本地化中 ln 之间的字母数)。最常见的本地化活动是将应用程序 UI 中显示的文本翻译成不同的语言。此外,根据语言环境,还可以调整其他 UI 方面,如货币(欧元、美元等)、度量系统(例如,公制或英制)、或数字和日期格式。

本地化是 国际化(i18n)的一部分,国际化是设计和开发支持异构目标受众轻松进行本地化的应用程序的过程。启用 i18n 的常见实践包括使用 Unicode 进行文本编码或为垂直文本或非拉丁文字体添加 CSS 支持。

本地化测试 是一种非功能性测试形式,用于验证特定语言环境设置下的 SUT。Selenium WebDriver API 允许我们根据浏览器语言进行本地化测试,通过设置 intl.accept_languages 能力。此能力允许您指定语言环境标识符,如 en_US 表示美式英语或 es_ES 表示西班牙语(欧洲),等等。示例 5-23 展示了在 Chrome 中配置此能力的测试设置。在 Edge 中可以使用相同的语法,尽管我们在 Firefox 中将此能力作为首选项进行配置(参见 示例 5-24)。

示例 5-23. 使用 Chrome 首选语言的测试
String lang;

@BeforeEach
void setup() {
    lang = "es-ES";
    ChromeOptions options = new ChromeOptions();
    Map<String, Object> prefs = new HashMap<>();
    prefs.put("intl.accept_languages", lang); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.setExperimentalOption("prefs", prefs);

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

@Test
void testAcceptLang() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/multilanguage.html"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    ResourceBundle strings = ResourceBundle.getBundle("strings",
            Locale.forLanguageTag(lang)); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String home = strings.getString("home");
    String content = strings.getString("content");
    String about = strings.getString("about");
    String contact = strings.getString("contact");

    String bodyText = driver.findElement(By.tagName("body")).getText();
    assertThat(bodyText).contains(home).contains(content).contains(about)
            .contains(contact); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们在 Chrome 中指定西班牙语(欧洲)作为首选语言。

2

我们打开一个支持多语言(英语和西班牙语)的练习页面。

3

我们使用资源包读取文本翻译。您可以在项目文件夹 src/test/resources 中的 strings_es.properties(以及 strings_en.properties)文件中找到这些字符串。

4

我们断言文档正文包含所有预期的字符串。

示例 5-24. 指定 Firefox 的首选语言环境测试设置
@BeforeEach
void setup() {
    lang = "es-ES";
    FirefoxOptions options = new FirefoxOptions();
    options.addPreference("intl.accept_languages", lang);

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

还有另一种使用 Selenium WebDriver 进行本地化测试的选择。我们可以更改浏览器的默认语言,而不是更改首选语言(确定 HTTP 头accept-language)。如果该 HTTP 头不存在,多语言应用程序将交替使用浏览器语言。Selenium WebDriver API 允许使用一个简单的参数--lang来更改浏览器语言,该参数被指定为浏览器能力。这个参数在 Chrome、Edge 和 Firefox 中是可互操作的。示例 5-25 展示了如何使用 WebDriver 能力将浏览器语言设置为美式英语。

示例 5-25. 在 Chrome 中更改浏览器语言的测试设置
@BeforeEach
void setup() {
    lang = "en-US";
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--lang=" + lang);

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}

隐身

隐身模式确保浏览器以干净的状态运行。这种模式允许私密浏览,即与主会话和用户数据隔离运行。Selenium WebDriver API 通过能力启用了在隐身模式下执行浏览器的功能。对于 Chrome 和 Edge,这种模式是通过使用--incognito参数激活的(参见示例 5-26),而对于 Firefox,则使用-private偏好设置(参见示例 5-27)。

示例 5-26. 使用 Chrome 在隐身模式下的测试设置
@BeforeEach
void setup() {
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--incognito");

    driver = WebDriverManager.chromedriver().capabilities(options).create();
}
示例 5-27. 使用 Firefox 在隐身模式下的测试设置
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    options.addArguments("-private");

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

IE 模式中的 Edge

Edge 为微软传统浏览器即 Internet Explorer(IE)提供了内置支持。因此,要创建一个使用 IE 模式中的 Edge 的 Selenium WebDriver 测试,我们需要先在 Edge 中启用 IE 模式。如图 5-14 所示,这个选项是在 Edge 设置→默认浏览器→允许在 Internet Explorer 模式下重新加载网站中启用的。然后,我们可以使用 Selenium WebDriver API,如示例 5-28 所示。

hosw 0514

图 5-14. 启用 IE 模式中 Edge 的浏览器设置
示例 5-28. 使用 IE 模式中的 Edge 进行测试设置
@BeforeAll
static void setupClass() {
    assumeThat(IS_OS_WINDOWS).isTrue(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebDriverManager.iedriver().setup(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

@BeforeEach
void setup() {
    Optional<Path> browserPath = WebDriverManager.edgedriver()
            .getBrowserPath(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assumeThat(browserPath).isPresent();

    InternetExplorerOptions options = new InternetExplorerOptions();
    options.attachToEdgeChrome(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    options.withEdgeExecutablePath(browserPath.get().toString()); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    driver = new InternetExplorerDriver(options); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们假设测试在 Windows 中执行,因为 IE 模式不支持其他操作系统。

2

我们使用 WebDriverManager 来管理 IEDriver(Internet Explorer 所需的驱动程序)。

3

我们使用 WebDriverManager 来发现 Edge 的路径。

4

我们使用 IE 选项来指定我们使用 IE 模式中的 Edge。

5

我们在 IE 选项中设置了先前发现的 Edge 路径。

6

我们创建驱动程序实例来使用 Internet Explorer(实际上将是 IE 模式中的 Edge)。

Chrome 开发者工具协议

Chrome DevTools 是一组针对基于 Chromium 的浏览器(如 Chrome 和 Edge)的 web 开发工具。这些工具允许检查、调试或分析这些浏览器,以及其他功能。Chrome DevTools Protocol (CDP) 是一种通信协议,允许外部客户端操作 Chrome DevTools。Firefox 实现了 CDP 的子集,以支持像 Selenium WebDriver 这样的自动化工具。

在 Selenium WebDriver 中使用 CDP 有两种方式。从版本 4 开始,Selenium WebDriver 提供了 HasDevTools 接口,用于向浏览器发送 CDP 命令。这个接口由 ChromiumDriver(用于 Chrome 和 Edge)和 FirefoxDriver(用于 Firefox)实现。这种机制非常强大,因为它直接提供了与 Selenium WebDriver 结合使用 CDP 的访问权限。然而,它也有一个重要的限制,即与浏览器类型和版本紧密耦合。

因此,Selenium WebDriver API 提供了第二种使用 Chrome DevTools 协议(CDP)的方式,基于一组在浏览器上构建的包装类,用于高级浏览器操作。这些包装类允许执行不同的操作,如网络流量拦截或基本和摘要认证。下面的子节解释了这些包装类。之后,我会展示几个直接使用 CDP 命令的例子。

CDP Selenium 包装类

Selenium WebDriver API 包含一组辅助类,包装了部分 CDP 命令。这些类旨在为 Selenium WebDriver 测试提供友好的 API,支持高级功能。

网络拦截器

第一个构建在 CDP 之上的包装类称为 NetworkInterceptor。这个类允许桩后端请求、拦截网络流量并返回预先准备好的响应。这个特性可能通过使用快速、直接的响应来模拟外部调用,简化复杂的端到端测试。要实例化 NetworkInterceptor,需要在其构造函数中指定参数(参见 示例 5-29):

  • 实现了 CDP 的 WebDriver 对象(例如 ChromeDriverEdgeDriver

  • Route 对象用于将网络请求映射到响应

示例 5-29. 使用 NetworkInterceptor 测试拦截网络流量
@Test
void testNetworkInterceptor() throws Exception {
    Path img = Paths
            .get(ClassLoader.getSystemResource("tools.png").toURI()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    byte[] bytes = Files.readAllBytes(img);

    try (NetworkInterceptor interceptor = new NetworkInterceptor(driver,
            Route.matching(req -> req.getUri().endsWith(".png"))
                    .to(() -> req -> new HttpResponse()
                            .setContent(Contents.bytes(bytes))))) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        int width = Integer.parseInt(driver.findElement(By.tagName("img"))
                .getAttribute("width"));
        assertThat(width).isGreaterThan(80); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }
}

1

我们加载一个存储在 Java 项目中作为测试资源的本地图像。

2

我们创建一个网络拦截器实例,为所有以 .png 结尾的请求创建一个路由,并通过新的响应打桩这个请求,在这种情况下发送前一张图片的内容。

3

我们打开实践站点。

4

如果拦截正常工作,页面上的图片应该比原始的徽标宽。

注意

如果使用与前述代码不同的浏览器(如 Firefox),将抛出DevToolsException异常。

基本认证和摘要认证

HTTP 提供了两种内置机制来识别用户身份,称为基本摘要认证。这两种方法允许使用一对值(用户名和密码)来指定用户的凭据。它们之间的区别在于它们如何传递凭据。一方面,摘要认证方法通过将用户名和密码应用哈希函数发送加密的凭据。另一方面,基本认证使用 Base64 编码(而非加密)凭据。

Selenium WebDriver 提供了HasAuthentication接口以无缝实现基本和摘要认证。示例 5-30 展示了使用 Chrome 和基本认证的测试。您可以在 Edge 和摘要认证中使用相同的机制(详见示例库的完整测试)。

示例 5-30. 使用 Chrome 进行基本认证测试
@Test
void testBasicAuth() {
    ((HasAuthentication) driver)
            .register(() -> new UsernameAndPassword("guest", "guest")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    driver.get("https://jigsaw.w3.org/HTTP/Basic/"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebElement body = driver.findElement(By.tagName("body"));
    assertThat(body.getText()).contains("Your browser made it!"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们将驱动对象转换为HasAuthentication并注册凭据(用户名和密码)。

2

我们打开了一个使用基本认证保护的网站。

3

我们验证页面内容是否可用。

当使用其他浏览器(如 Firefox)时,我们无法将驱动对象转换为HasAuthentication。尽管如此,可以使用 URL 中的语法protocol://username:password@domain来发送凭据。示例 5-31 展示了此用法。

示例 5-31. 使用基本认证和 Firefox 进行测试
@Test
void testGenericAuth() {
    driver.get("https://guest:guest@jigsaw.w3.org/HTTP/Basic/");

    WebElement body = driver.findElement(By.tagName("body"));
    assertThat(body.getText()).contains("Your browser made it!");
}

CDP 原始命令

自版本 4 起,Selenium WebDriver 提供了HasDevTools接口来直接使用 CDP。该接口由ChromiumDriver(用于 Chrome 和 Edge)和FirefoxDriver(用于 Firefox)实现。要使用此功能,我们首先需要使用DevTools实例的createSession()方法打开 CDP 会话(即客户端与浏览器之间的 WebSocket 连接)。示例 5-32 展示了在 Selenium WebDriver 测试中使用 CDP 的推荐结构。正如您所见,CDP 会话在测试设置中创建,并在拆卸时关闭。每个测试将使用类属性devTools与 Chrome DevTools 交互。

示例 5-32. 使用 Chrome DevTools 进行测试结构
WebDriver driver;

DevTools devTools; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

@BeforeEach
void setup() {
    driver = WebDriverManager.chromedriver().create();
    devTools = ((ChromeDriver) driver).getDevTools(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    devTools.createSession(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

@AfterEach
void teardown() {
    devTools.close(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    driver.quit();
}

1

我们声明了一个DevTools类属性。

2

我们从驱动对象中获取DevTools实例。在本示例(以及其他示例中),我使用ChromeDriver(尽管EdgeDriver实例也是有效的)。

3

我们创建一个 CDP 会话以在测试逻辑中与 Chrome DevTools 交互。

4

每次测试结束并退出 WebDriver 会话之前,我们终止 CDP 会话。

下面的小节展示了几个示例,说明了 WebDriver 测试中 DevTools 的潜力。在这些示例中,我们使用 DevTools 实例通过 send() 方法发送 CDP 命令。Selenium WebDriver API 提供了各种命令,允许进行不同的操作,如模拟网络条件、处理 HTTP 标头、阻止 URL 等。

警告

使用原始 CDP 命令的 Selenium WebDriver 测试(如下一节中所述)与特定的浏览器版本相关联。您可以通过检查导入子句(例如 import org.openqa.selenium.devtools.v96.*;)来查看此版本,在示例存储库中提供了完整的测试。

模拟网络条件。

CDP 允许模拟不同网络(如移动 2G/3G/4G、WiFi 或蓝牙等)和条件(例如延迟或吞吐量)。这个功能对于测试特定连接参数下的 Web 应用行为非常有用。示例 5-33 展示了使用此功能的测试。如您所见,此测试发送了两个 CDP 命令:

Network.enable()

要激活网络跟踪。此命令有三个可选参数:

Optional<Integer> maxTotalBufferSize

网络有效载荷的最大缓冲区大小(以字节为单位)。

Optional<Integer> maxResourceBufferSize

单个资源的最大缓冲区大小(以字节为单位)。

Optional<Integer> maxPostDataSize

最长的请求正文大小(以字节为单位)。

Network.emulateNetworkConditions()

要激活网络仿真。使用以下参数指定仿真条件:

Boolean offline

要模拟无互联网连接。 Number latency:请求到响应的最小延迟(以毫秒为单位)。

Number downloadThroughput

最大下载吞吐量(以字节/秒为单位)。-1 禁用下载限制。

Number uploadThroughput

最大上传吞吐量(以字节/秒为单位)。-1 禁用上传限制。

Optional<ConnectionType> connectionType

模拟连接技术。枚举 ConnectionType 接受以下选项:NONECELLULAR2GCELLULAR3GCELLULAR4GBLUETOOTHETHERNETWIFIWIMAXOTHER

示例 5-33. 测试模拟网络条件
@Test
void testEmulateNetworkConditions() {
    devTools.send(Network.enable(Optional.empty(), Optional.empty(),
            Optional.empty())); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    devTools.send(Network.emulateNetworkConditions(false, 100, 50 * 1024,
            50 * 1024, Optional.of(ConnectionType.CELLULAR3G))); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    long initMillis = System.currentTimeMillis(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    Duration elapsed = Duration
            .ofMillis(System.currentTimeMillis() - initMillis); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    log.debug("The page took {} ms to be loaded", elapsed.toMillis());

    assertThat(driver.getTitle()).contains("Selenium WebDriver");
}

1

我们激活网络跟踪(无需调整任何网络参数)。

2

我们使用 50 KBps 的移动 3G 网络模拟下载和上传带宽。

3

我们在加载网页之前获取系统时间戳。

4

我们加载实践站点的索引页面。

5

我们计算加载此页面所需的时间。

网络监控

我们还可以使用 CDP 在与网页交互时监控网络流量。示例 5-34 展示了使用此功能的测试。此测试使用 DevTools 对象的 add​Lis⁠tener() 方法来跟踪 HTTP 请求和响应。

示例 5-34. 监控 HTTP 请求和响应的测试
@Test
void testNetworkMonitoring() {
    devTools.send(Network.enable(Optional.empty(), Optional.empty(),
            Optional.empty()));

    devTools.addListener(Network.requestWillBeSent(), request -> {
        log.debug("Request {}", request.getRequestId());
        log.debug("\t Method: {}", request.getRequest().getMethod());
        log.debug("\t URL: {}", request.getRequest().getUrl());
        logHeaders(request.getRequest().getHeaders());
    }); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    devTools.addListener(Network.responseReceived(), response -> {
        log.debug("Response {}", response.getRequestId());
        log.debug("\t URL: {}", response.getResponse().getUrl());
        log.debug("\t Status: {}", response.getResponse().getStatus());
        logHeaders(response.getResponse().getHeaders());
    }); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    assertThat(driver.getTitle()).contains("Selenium WebDriver");
}

void logHeaders(Headers headers) {
    log.debug("\t Headers:");
    headers.toJson().forEach((k, v) -> log.debug("\t\t{}:{}", k, v));
}

1

我们创建一个 HTTP 请求监听器,并在控制台中记录捕获的数据。

2

我们创建一个 HTTP 响应监听器,并在控制台中记录捕获的数据。

整页截图

CDP 的另一个可能用途是制作整页截图(即捕获超出视口的内容页面)。示例 5-35 在 Chrome 中展示了此功能。

示例 5-35. 在 Chrome 中使用 CDP 制作整页截图的测试
@Test
void testFullPageScreenshotChrome() throws IOException {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/long-page.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions.presenceOfNestedElementsLocatedBy(
            By.className("container"), By.tagName("p"))); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    GetLayoutMetricsResponse metrics = devTools
            .send(Page.getLayoutMetrics());
    Rect contentSize = metrics.getContentSize(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String screenshotBase64 = devTools
            .send(Page.captureScreenshot(Optional.empty(), Optional.empty(),
                    Optional.of(new Viewport(0, 0, contentSize.getWidth(),
                            contentSize.getHeight(), 1)),
                    Optional.empty(), Optional.of(true))); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    Path destination = Paths.get("fullpage-screenshot-chrome.png");
    Files.write(destination, Base64.getDecoder().decode(screenshotBase64)); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    assertThat(destination).exists(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们加载包含长文本的练习页面(因此其内容超出标准视口)。

2

我们等待段落加载完成。

3

我们获取页面布局指标(以计算页面尺寸)。

4

我们发送 CDP 命令以截取超出页面视口的屏幕截图。结果,我们将截图以 Base64 字符串形式获取。

5

我们将 Base64 内容解码为 PNG 文件。

6

我们断言测试结束时 PNG 文件存在。

此功能在其他完全实现了 CDP 的浏览器(如 Chrome 或 Edge)中可用。然而,在 Firefox 等其他浏览器中可能不可用。幸运的是,Firefox 通过 FirefoxDriver 对象中可用的 getFullPageScreenshotAs() 方法支持相同的特性。示例 5-36 展示了使用此方法和 Firefox 进行的测试。

示例 5-36. 在 Firefox 中制作整页截图的测试
@Test
void testFullPageScreenshotFirefox() throws IOException {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/long-page.html");
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions.presenceOfNestedElementsLocatedBy(
            By.className("container"), By.tagName("p")));

    byte[] imageBytes = ((FirefoxDriver) driver)
            .getFullPageScreenshotAs(OutputType.BYTES); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    Path destination = Paths.get("fullpage-screenshot-firefox.png");
    Files.write(destination, imageBytes);

    assertThat(destination).exists();
}

1

我们制作整个页面的截图。与常规截图一样(见第四章中的表 4-2),输出类型可以是 FILEBASE64BYTES。我们使用后者将截图获取为字节数组。

性能指标

CDP 允许收集运行时性能指标,例如加载的文档数、DOM 节点数、加载 DOM 的时间以及脚本持续时间等,示例 5-37 展示了一个收集这些指标并在标准输出中显示的测试。

例子 5-37. 测试收集性能指标
@Test
void testPerformanceMetrics() {
    devTools.send(Performance.enable(Optional.empty())); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");

    List<Metric> metrics = devTools.send(Performance.getMetrics()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assertThat(metrics).isNotEmpty();
    metrics.forEach(metric -> log.debug("{}: {}", metric.getName(),
            metric.getValue()));
}

1

我们启用收集指标。

2

我们收集所有指标。

额外的头部

CDP 允许在 HTTP 层面添加额外的头部。为此,我们需要在 CDP 会话中发送命令 Network.setExtraHTTPHeaders()。例子 5-38 展示了一个使用该命令来添加 HTTP 头部 Authorization 的测试,用于在需要基本认证登录的网页中发送凭据(用户名和密码)。

例子 5-38. 测试添加额外的 HTTP 头部
@Test
void testExtraHeaders() {
    devTools.send(Network.enable(Optional.empty(), Optional.empty(),
            Optional.empty()));

    String userName = "guest";
    String password = "guest";
    Map<String, Object> headers = new HashMap<>();
    String basicAuth = "Basic " + new String(Base64.getEncoder()
            .encode(String.format("%s:%s", userName, password).getBytes()));
    headers.put("Authorization", basicAuth); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    devTools.send(Network.setExtraHTTPHeaders(new Headers(headers))); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get("https://jigsaw.w3.org/HTTP/Basic/"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    String bodyText = driver.findElement(By.tagName("body")).getText();
    assertThat(bodyText).contains("Your browser made it!"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们将用户名和密码进行 Base64 编码。

2

我们创建授权头部。

3

我们打开一个使用基本认证保护的网页。

4

我们验证页面是否正确显示。

阻止 URL

CDP 提供了在会话中阻止给定 URL 的能力。例子 5-39 提供了一个测试,阻止练习网页的 logo URL。如果你运行这个测试并在执行期间检查浏览器,你会发现该 logo 不会显示在页面上。

例子 5-39. 测试阻止 URL
@Test
void testBlockUrl() {
    devTools.send(Network.enable(Optional.empty(), Optional.empty(),
            Optional.empty()));

    String urlToBlock =
            "https://bonigarcia.dev/selenium-webdriver-java/img/hands-on-icon.png";
    devTools.send(Network.setBlockedURLs(ImmutableList.of(urlToBlock))); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    devTools.addListener(Network.loadingFailed(), loadingFailed -> {
        BlockedReason reason = loadingFailed.getBlockedReason().get();
        log.debug("Blocking reason: {}", reason);
        assertThat(reason).isEqualTo(BlockedReason.INSPECTOR);
    }); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    assertThat(driver.getTitle()).contains("Selenium WebDriver");
}

1

我们阻止给定的 URL。

2

我们创建一个监听器以追踪失败的事件。

设备模拟

CDP 提供的另一个功能是模拟移动设备(例如智能手机、平板电脑)的能力。例子 5-40 说明了这个用法。该测试首先通过发送命令 Network.setUserAgentOverride() 来覆盖用户代理,然后通过发送命令 Emulation.setDeviceMetricsOverride 来模拟设备指标。

例子 5-40. 测试模拟移动设备
@Test
void testDeviceEmulation() {
    // 1\. Override user agent (Apple iPhone 6)
    String userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X)"
            + "AppleWebKit/600.1.3 (KHTML, like Gecko)"
            + "Version/8.0 Mobile/12A4345d Safari/600.1.4";
    devTools.send(Network.setUserAgentOverride(userAgent, Optional.empty(),
            Optional.empty(), Optional.empty())); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    // 2\. Emulate device dimension
    Map<String, Object> deviceMetrics = new HashMap<>();
    deviceMetrics.put("width", 375);
    deviceMetrics.put("height", 667);
    deviceMetrics.put("mobile", true);
    deviceMetrics.put("deviceScaleFactor", 2);
    ((ChromeDriver) driver).executeCdpCommand(
            "Emulation.setDeviceMetricsOverride", deviceMetrics); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    assertThat(driver.getTitle()).contains("Selenium WebDriver");
}

1

我们覆盖用户代理以模拟苹果 iPhone 6。

2

我们覆盖设备屏幕参数。

控制台监听器

CDP 允许您实现监听器以监视控制台事件,即网页 JavaScript 的日志和错误追踪。例子 5-41 展示了这个测试。该测试使用了一个在练习站点中意图追踪多个 JavaScript 消息(使用命令 console.log()console.error() 等)并抛出 JavaScript 异常的网页。

例子 5-41. 测试监听控制台事件
@Test
void testConsoleListener() throws Exception {
    CompletableFuture<ConsoleEvent> futureEvents = new CompletableFuture<>();
    devTools.getDomains().events()
            .addConsoleListener(futureEvents::complete); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    CompletableFuture<JavascriptException> futureJsExc = new CompletableFuture<>();
    devTools.getDomains().events()
            .addJavascriptExceptionListener(futureJsExc::complete); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/console-logs.html"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    ConsoleEvent consoleEvent = futureEvents.get(5, TimeUnit.SECONDS); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    log.debug("ConsoleEvent: {} {} {}", consoleEvent.getTimestamp(),
            consoleEvent.getType(), consoleEvent.getMessages()); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    JavascriptException jsException = futureJsExc.get(5,
            TimeUnit.SECONDS); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    log.debug("JavascriptException: {} {}", jsException.getMessage(),
            jsException.getSystemInformation());
}

1

我们创建一个控制台事件监听器。

2

我们为 JavaScript 错误创建另一个监听器。

3

我们打开一个在浏览器控制台中写入消息的练习页面。

4

我们等待最多五秒,直到收到一个控制台事件。

5

我们在标准输出中写入接收到的控制台事件的信息。

6

我们为 JavaScript 异常重复相同的过程。

地理位置覆盖

CDP 提供的另一个功能是能够覆盖主机设备处理的地理位置坐标。示例 5-42 展示了如何做到这一点。此测试发送命令 Emulation.setGeolocationOverride(),接受三个可选参数:纬度、经度和准确度。

示例 5-42。覆盖位置坐标的测试
@Test
void testGeolocationOverride() {
    devTools.send(Emulation.setGeolocationOverride(Optional.of(48.8584),
            Optional.of(2.2945), Optional.of(100))); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/geolocation.html"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    driver.findElement(By.id("get-coordinates")).click();

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
    WebElement coordinates = driver.findElement(By.id("coordinates"));
    wait.until(ExpectedConditions.visibilityOf(coordinates));
}

1

我们使用埃菲尔铁塔的坐标(法国巴黎)覆盖地理位置。

2

我们打开一个访问设备位置并向用户显示坐标的练习网页。

管理 cookies

CDP 还允许管理 Web cookies。示例 5-43 显示了读取管理一些 cookies 的练习页面的测试。

示例 5-43。管理 cookies 的测试
@Test
void testManageCookies() {
    devTools.send(Network.enable(Optional.empty(), Optional.empty(),
            Optional.empty()));
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/cookies.html");

    // Read cookies
    List<Cookie> cookies = devTools.send(Network.getAllCookies()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    cookies.forEach(cookie -> log.debug("{}={}", cookie.getName(),
            cookie.getValue()));
    List<String> cookieName = cookies.stream()
            .map(cookie -> cookie.getName()).sorted()
            .collect(Collectors.toList());
    Set<org.openqa.selenium.Cookie> seleniumCookie = driver.manage()
            .getCookies();
    List<String> selCookieName = seleniumCookie.stream()
            .map(selCookie -> selCookie.getName()).sorted()
            .collect(Collectors.toList());
    assertThat(cookieName).isEqualTo(selCookieName); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    // Clear cookies
    devTools.send(Network.clearBrowserCookies()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    List<Cookie> cookiesAfterClearing = devTools
            .send(Network.getAllCookies());
    assertThat(cookiesAfterClearing).isEmpty(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    driver.findElement(By.id("refresh-cookies")).click();
}

1

我们读取网页的所有 cookies。

2

我们验证使用 CDP 命令读取的 cookies 和使用 Selenium WebDriver API(使用 getCookies();)读取的 cookies 是否相同。

3

我们移除所有 cookies。

4

我们验证此时没有任何 cookies。

加载不安全的页面

CDP 还允许您加载不安全的网页(即,使用 HTTPS 的网页,但其证书无效)。示例 5-44 说明了这个功能。

示例 5-44。测试加载不安全的网页
@Test
void testLoadInsecure() {
    devTools.send(Security.enable()); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    devTools.send(Security.setIgnoreCertificateErrors(true)); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    driver.get("https://expired.badssl.com/");

    String bgColor = driver.findElement(By.tagName("body"))
            .getCssValue("background-color");
    Color red = new Color(255, 0, 0, 1);
    assertThat(Color.fromString(bgColor)).isEqualTo(red); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
}

1

我们启用跟踪安全性。

2

我们忽略证书错误。

3

我们验证页面是否正确加载。

位置上下文

Selenium WebDriver API 为模拟用户设备地理位置坐标提供了接口 LocationContext。此接口由 ChromeDriverEdgeDriverOperaDriver 实现。因此,这些驱动程序可以调用方法 setLocation() 来指定自定义坐标(纬度、经度和高度)。示例 5-45 展示了使用此功能的基本测试。

示例 5-45。通过 LocationContext 设置自定义地理位置坐标的测试
@Test
void testLocationContext() {
    LocationContext location = (LocationContext) driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    location.setLocation(new Location(27.5916, 86.5640, 8850)); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/geolocation.html"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    driver.findElement(By.id("get-coordinates")).click();

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
    WebElement coordinates = driver.findElement(By.id("coordinates"));
    wait.until(ExpectedConditions.visibilityOf(coordinates)); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们将驱动对象转换为LocationContext(仅适用于 Chrome、Edge 或 Opera)。

2

我们打开一个实践页面,显示地理位置坐标给最终用户。

3

我们设置自定义位置,即珠穆朗玛峰的坐标(位于尼泊尔与中国边境上)。

4

我们断言页面上可见坐标。

Web 认证

Web 认证 API(也称为WebAuthn)是一项W3C 规范,允许服务器使用公钥加密而不是密码来注册和认证用户。自 2019 年 1 月起,主要浏览器(Chrome、Firefox、Edge 和 Safari)已支持 WebAuthn。这些浏览器允许使用 U2F(Universal 2nd Factor)令牌进行凭据创建和断言,这些令牌是通用串行总线(USB)或近场通信(NFC)安全设备。

在传统的 Web 认证方法中,用户通过 Web 表单将其用户名和密码发送到服务器。在 WebAuthn 中,Web 服务器使用 Web 认证 API 提示用户创建私钥-公钥对(称为凭据)。私钥安全存储在用户设备上,而公钥则发送到服务器。然后,服务器可以使用该公钥验证用户身份。

从版本 4 开始,Selenium WebDriver 直接支持WebAuthn。为此,Selenium WebDriver API 提供了HasVirtualAuthenticator接口。这个接口允许我们使用虚拟认证器,而不是使用安全物理设备。虽然RemoteWebDriver类实现了这个接口,在撰写本文时,这种机制仅在基于 Chromium 的浏览器(例如 Chrome 和 Edge)中受支持。示例 5-46 展示了使用 Web 认证 API 的测试。

示例 5-46. 使用 WebAuthn 进行测试
@Test
void testWebAuthn() {
    driver.get("https://webauthn.io/"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    HasVirtualAuthenticator virtualAuth = (HasVirtualAuthenticator) driver; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    VirtualAuthenticator authenticator = virtualAuth
            .addVirtualAuthenticator(new VirtualAuthenticatorOptions()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    String randomId = UUID.randomUUID().toString();
    driver.findElement(By.id("input-email")).sendKeys(randomId); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
    driver.findElement(By.id("register-button")).click(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    wait.until(ExpectedConditions.textToBePresentInElementLocated(
            By.className("popover-body"), "Success! Now try logging in"));

    driver.findElement(By.id("login-button")).click(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    wait.until(ExpectedConditions.textToBePresentInElementLocated(
            By.className("main-content"), "You're logged in!")); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)

    virtualAuth.removeVirtualAuthenticator(authenticator); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

1

我们打开一个使用 Web 认证 API 保护的网站。

2

我们将驱动对象转换为HasVirtualAuthenticator

3

我们创建并注册一个新的虚拟认证器。

4

我们在 Web 表单中发送一个随机标识符。

5

我们提交该标识符并等待接收。

6

我们点击按钮登录。

7

我们验证认证已正确执行。

8

我们移除虚拟认证器。

打印页面

Selenium WebDriver 允许将网页打印为 PDF 文档。为此,Selenium WebDriver API 提供了PrintsPage接口。这个接口由类RemoteWebDriver继承,因此,它适用于 Selenium WebDriver 支持的所有浏览器。然而,当使用不同浏览器时存在细微差异。例如,只有在 Chrome 和 Edge 中以无头模式启动浏览器时才能打印页面。对于 Firefox,则不需要此限制,我们可以像平常一样使用 Firefox。示例 5-47 展示了将网页打印为 PDF 的测试逻辑。您可以在示例存储库中找到 Firefox 和无头 Chrome/Edge 的完整测试。

示例 5-47. 测试将网页打印为 PDF
@Test
void testPrint() throws IOException {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    PrintsPage pg = (PrintsPage) driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    PrintOptions printOptions = new PrintOptions();
    Pdf pdf = pg.print(printOptions); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    String pdfBase64 = pdf.getContent(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    assertThat(pdfBase64).contains("JVBER"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    byte[] decodedImg = Base64.getDecoder()
            .decode(pdfBase64.getBytes(StandardCharsets.UTF_8)); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    Path destinationFile = Paths.get("my-pdf.pdf");
    Files.write(destinationFile, decodedImg); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
}

1

我们将驱动对象转换为PrintsPage

2

我们使用默认配置将当前网页打印为 PDF。

3

我们获取 PDF 的内容并转换为 Base64。

4

我们验证这个内容是否包含文件签名(“魔术词”JVBER)。

5

我们将 Base64 转换为原始字节数组。

6

我们将 PDF 内容(字节数组)写入本地文件。

WebDriver BiDi

WebDriver BiDi 是一个 W3C 草案,定义了双向 WebDriver 协议。BiDi 引入了一个 WebSocket 连接,使驱动程序和浏览器之间可以进行双向通信,而不是 WebDriver 协议的严格命令/响应格式。这样,WebDriver BiDi 将允许使用快速的双向传输执行不同的操作(即,无需轮询浏览器以获取响应)。

在 Selenium WebDriver 中,BiDi 的目标是长期替代目前由 CDP 支持的高级操作。例如,Selenium WebDriver API 支持通过HasLog​E⁠vents接口实现事件监听器。在撰写本文时,该接口在 CDP 之上运行。然而,未来的 Selenium WebDriver 版本将在内部使用 BiDi,提供更强大的跨浏览器兼容性。HasLogEvents允许实现以下事件的监听器:

domMutation

为了捕获 DOM 中的变化事件。示例 5-48 展示了一个实现此类事件监听器的测试。

consoleEvent

为了捕获浏览器控制台的变化事件,比如 JavaScript 的跟踪。示例 5-49 展示了第二个实现这种类型监听器的测试。

示例 5-48. 测试实现 DOM 变异事件的监听器
@Test
void testDomMutation() throws InterruptedException {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");

    HasLogEvents logger = (HasLogEvents) driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    JavascriptExecutor js = (JavascriptExecutor) driver;

    AtomicReference<DomMutationEvent> seen = new AtomicReference<>();
    CountDownLatch latch = new CountDownLatch(1);
    logger.onLogEvent(CdpEventTypes.domMutation(mutation -> {
        seen.set(mutation);
        latch.countDown();
    })); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebElement img = driver.findElement(By.tagName("img"));
    String newSrc = "img/award.png";
    String script = String.format("arguments[0].src = '%s';", newSrc);
    js.executeScript(script, img); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    assertThat(seen.get().getElement().getAttribute("src"))
            .endsWith(newSrc); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们将驱动对象转换为HasLogEvents。此转换仅适用于 Chrome 和 Edge 浏览器。

2

我们创建了一个监听器以捕获 DOM 变化事件。这个测试期望仅捕获一个事件,使用倒计时锁定来同步。

3

我们通过执行 JavaScript 强制 DOM 变化来改变图像来源。

4

我们验证事件最多在 10 秒内发生。

5

我们检查了图片来源已经改变。

示例 5-49. 实现一个监听器以捕获控制台事件
@Test
void testConsoleEvents() throws InterruptedException {
    HasLogEvents logger = (HasLogEvents) driver;

    CountDownLatch latch = new CountDownLatch(4);
    logger.onLogEvent(CdpEventTypes.consoleEvent(consoleEvent -> {
        log.debug("{} {}: {}", consoleEvent.getTimestamp(),
                consoleEvent.getType(), consoleEvent.getMessages());
        latch.countDown();
    })); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/console-logs.html"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
}

1

我们创建了一个监听器来捕获控制台事件。这个测试期望使用倒计时锁定来同步捕获四个事件。

2

我们打开了实践网页,该网页在 JavaScript 控制台中记录了几条消息。

摘要与展望

本章介绍了 Selenium WebDriver API 的实际概述,这些功能在各种浏览器之间不兼容。首先,您了解到如何使用能力在无头模式下运行浏览器,更改页面加载策略,使用 Web 扩展或管理浏览器弹出窗口(例如地理位置、通知或获取用户媒体等),以及其他能力。然后,您了解到 Selenium WebDriver 提供了使用 CDP 与 Web 浏览器交互的不同方式。这种机制允许在 Selenium WebDriver 测试中集成许多强大的功能,如模拟网络条件、基本和摘要身份验证、网络监控、处理 HTTP 头部或阻止 URL 等。然后,您了解到其他浏览器特定的功能,如位置上下文、Web 认证(WebAuthn)和将网页打印成 PDF 文档。最后,您了解到 WebDriver BiDi,这是一个草案标准化,用于定义与浏览器的双向通信,用于自动化目的。在撰写本文时,BiDi 处于早期阶段。目标是在未来版本中,Selenium WebDriver 将在 BiDi 之上支持不同的标准功能。

下一章总结了我们与 Selenium WebDriver API 的旅程。该章节解释了如何使用这个 API 来控制远程浏览器。这些浏览器可以托管在 Selenium Grid 上,云提供商(例如 Sauce Labs、BrowserStack 或 CrossBrowserTesting),或者在 Docker 容器中执行。

第六章:远程 WebDriver

到目前为止,本书中解释的示例都是在执行测试的机器上使用本地安装的 web 浏览器。本章涵盖了 Selenium WebDriver API 的另一个重要特性,即使用远程浏览器的能力(即安装在其他主机上)。首先,我们回顾了允许在 Selenium WebDriver 中使用远程浏览器的架构。其次,我们研究了 Selenium Grid,这是一个为 Selenium WebDriver 测试提供远程浏览器的网络基础设施。第三,我们分析了一些最重要的云提供商,即提供自动化测试托管服务的公司。最后,我们探讨了如何使用 Docker 来支持 Selenium 的浏览器基础设施。

Selenium WebDriver 架构

如第一章中介绍的,Selenium WebDriver 是一个允许以编程方式控制 web 浏览器的库。自动化基于每个浏览器的本机功能。因此,我们需要在脚本(通常是测试)使用 Selenium WebDriver API 和浏览器之间放置一个称为 driver 的二进制文件。本书中到目前为止看到的示例使用本地浏览器,即在执行使用 Selenium WebDriver API 的测试的同一台机器上安装的浏览器。图 6-1 说明了这种方法。在这种情况下,当使用 Selenium WebDriver API 的 Java 语言绑定时,我们需要创建 ChromeDriver 实例来控制 Chrome,FirefoxDriver 来控制 Firefox 等。

hosw 0601

图 6-1. 使用本地浏览器的 Selenium WebDriver 架构

支持此过程的通信协议称为 W3C WebDriver。这一标准协议基于 HTTP 上的 JSON 消息。多亏了这一点,Selenium WebDriver 架构可以分布到不同的互联计算机(主机)。图 6-2 显示了远程架构的示意图。

hosw 0602

图 6-2. 使用远程浏览器的 Selenium WebDriver 架构

在这种情况下,Selenium WebDriver API 将 W3C WebDriver 消息发送到通常称为 Selenium Server 的服务器端组件。该服务器充当客户端请求到其他提供发生自动化的 web 浏览器的主机的代理。这种远程架构简化了跨浏览器测试(即在多种浏览器类型、版本或操作系统中验证 web 应用程序)和并行测试执行。

创建 RemoteWebDriver 对象

Selenium WebDriver API 提供了 RemoteWebDriver 类来控制远程浏览器。正如图 2-2 所示,该类是驱动本地浏览器的其他 WebDriver 类的父类(即 ChromeDriverFirefoxDriver 等)。通过这种方式,您可以像我们在本书中之前学习的那样使用 RemoteWebDriver 对象。

RemoteWebDriver 构造函数

实例化RemoteWebDriver对象的方法有多种。最常见的方法是通过传递两个参数调用其构造函数:Selenium 服务器的 URL 和所需的能力。正如图 5-1 所示,这些能力是从Capabilities接口继承的对象(例如ChromeOptionsFirefox​Op⁠tions等)。示例 6-1 展示了一个测试设置。您可以在本书的代码库中找到完整的测试。

注意

还有第二种RemoteWebDriver构造函数,只接受一个参数用于指定的能力。在这种情况下,Selenium 服务器的 URL 是从 Java 系统属性webdriver.remote.server中读取的。您可以在示例库中找到此功能的示例。

示例 6-1. 使用构造函数实例化 RemoteWebDriver 对象
@BeforeEach
void setup() throws MalformedURLException {
    URL seleniumServerUrl = new URL("http://localhost:4444/"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    assumeThat(isOnline(seleniumServerUrl)).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    ChromeOptions options = new ChromeOptions(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    driver = new RemoteWebDriver(seleniumServerUrl, options); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们创建一个 Java URL 对象,其地址为 Selenium 服务器地址。

2

我们假设此 URL 是在线的。为此,我们通过在 WebDriverManager 上调用静态方法isOnline来创建 AssertJ 假设。因此,当 Selenium 服务器脱机时,测试会被跳过。

3

我们实例化一个ChromeOptions对象来指定所需的能力。

4

我们使用 Selenium 服务器 URL 和 Chrome 选项作为参数调用RemoteWebDriver构造函数。

在前面的例子中,我们使用一个没有特别设置的ChromeOptions对象来指定所需的能力。换句话说,我们请求使用远程 Chrome 浏览器的默认行为。尽管如此,我们可以使用这个对象来配置特定的能力(例如无头浏览器、页面加载策略、设备仿真等),如第五章中所述。此外,由于能力内部使用键-值对来封装特定的浏览器方面,我们可以通过调用方法options.setCapability(key, value);来管理各个能力。Selenium WebDriver API 提供了CapabilityType类来指定这些能力的键。该类有一组公共属性,可以作为setCapability()方法中的key使用。表 6-1 展示了其中一些属性。

表 6-1. CapabilityType 属性

属性 能力名称 描述

|

BROWSER_NAME

|

browserName
浏览器名称(例如chromefirefoxmsedge

|

PLATFORM_NAME

|

platformName
平台名称(例如WINDOWSLINUXMACANDROIDIOS等)

|

BROWSER_VERSION

|

browserVersion
浏览器版本

|

SUPPORTS_JAVASCRIPT

|

javascriptEnabled
启用或禁用 JavaScript 执行

|

PAGE_LOAD_STRATEGY

|

pageLoadStrategy
页面加载策略(normaleagernone

RemoteWebDriver 对象中指定所需功能的另一种方法是使用 DesiredCapabilities 的实例。表 6-2 总结了这些对象提供的方法。

表 6-2. DesiredCapabilities 方法

方法 返回 描述

|

setBrowserName(String browserName)

|

void
指定浏览器名称

|

setVersion(String version)

|

void
指定浏览器版本

|

setPlatform(Platform platform)

|

void
指定平台名称

|

setJavascriptEnabled(boolean
    javascriptEnabled)

|

void
启用或禁用 JavaScript 执行

|

setAcceptInsecureCerts(boolean
    acceptInsecureCerts)

|

void
启用或禁用加载不安全页面

|

acceptInsecureCerts()

|

void
启用加载不安全页面

|

merge(Capabilities
    extraCapabilities)

|

DesiredCapabilities
与另一个功能对象合并
注意

由于 Selenium WebDriver 4 仍然支持 DesiredCapabilities,因为有很多现有的代码依赖于此功能。尽管如此,指定功能的推荐方式是使用特定于浏览器的选项(例如 ChromeOptionsFirefoxOptions 等)。

RemoteWebDriver 构建器

创建 RemoteWebDriver 对象的第二种方式是使用 Selenium WebDriver API 中提供的内置构建器。示例 6-2 演示了如何使用 Edge 作为远程浏览器。

示例 6-2. 使用构建器实例化 RemoteWebDriver 对象
@BeforeEach
void setup() throws MalformedURLException {
    URL seleniumServerUrl = new URL("http://localhost:4444/");
    assumeThat(isOnline(seleniumServerUrl)).isTrue();

    driver = RemoteWebDriver.builder().oneOf(new EdgeOptions())
            .address(seleniumServerUrl).build();
}

WebDriverManager 构建器

另外,我们还可以使用 WebDriverManager 来创建 RemoteWebDriver 的实例。为此,我们需要调用给定管理器的 remoteAddress() 方法以传递 Selenium 服务器 URL。示例 6-3 展示了使用此功能和 Firefox 作为远程浏览器的测试设置。

示例 6-3. 使用 WebDriverManager 实例化 RemoteWebDriver 对象
@BeforeEach
void setup() throws MalformedURLException {
    URL seleniumServerUrl = new URL("http://localhost:4444/");
    assumeThat(isOnline(seleniumServerUrl)).isTrue();

    driver = WebDriverManager.firefoxdriver()
            .remoteAddress(seleniumServerUrl).create();
}

Selenium-Jupiter

通常情况下,Selenium-Jupiter 使用 Jupiter 提供的参数解析功能。这样,就远程浏览器而言,您需要使用类型 RemoteWebDriver 声明测试(或构造函数)参数。然后,以下 Selenium-Jupiter 注解允许配置远程浏览器:

@DriverUrl

用于标识 Selenium 服务器 URL 的注解。另外,注解 @EnabledIfDriverUrlOnline 允许指定此 URL,并在该 URL 无响应时禁用测试。

@DriverCapabilities

用于配置所需功能的注解。

示例 6-4 展示了使用本地 Selenium 服务器提供的远程 Chrome 的 Selenium-Jupiter 测试。当 URL http://localhost:4444/ 离线时,此测试将被跳过。

示例 6-4. 在 Selenium-Jupiter 测试中使用 RemoteWebDriver 对象
@EnabledIfDriverUrlOnline("http://localhost:4444/")
@ExtendWith(SeleniumJupiter.class)
class RemoteChromeSelJupTest {

    @DriverCapabilities
    ChromeOptions options = new ChromeOptions();

    @Test
    void testRemote(RemoteWebDriver driver) {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}
注意

从功能角度来看,本节中描述的 RemoteWebDriver 实例化模式是等效的。换句话说,这些对象的工作方式相同。它们之间的区别在于提供的 语法糖(即,样式和表现力)。

Selenium Grid

如在 第一章 中介绍的,Selenium Grid 是 Selenium 套件的一个子项目,允许创建一个通过 W3C WebDriver 协议访问的远程浏览器的网络基础设施。 Selenium Grid 允许在不同的机器和不同的浏览器上并行运行测试。为此,Selenium Grid 提供了一个 Selenium 服务器,您可以使用 RemoteWebDriver 的实例进行控制。有三种运行 Selenium Grid 的方式:

独立运行

单个主机充当 Selenium 服务器,并在此模式下提供浏览器。它提供了一种在远程浏览器中运行 Selenium WebDriver 测试的简单方法。

Hub-nodes

独立模式的一个潜在问题是可伸缩性(因为 Selenium 服务器和浏览器在同一个主机上执行)。因此,hub-nodes 架构定义了两种组件类型来解决这个问题。首先,一个主机充当 hub(或 Selenium 服务器)。然后,在 hub 中注册一个或多个主机作为 node,提供浏览器以便使用 Selenium WebDriver 控制。这种架构在 第一章 中引入(参见 图 1-2)。

完全分布式

独立模式和 hub-nodes 方法是集中式架构,当请求增加时可能会降低性能。截至 Selenium 4,Selenium Grid 提供了一个完全分布式模式,实现了负载均衡机制以解决这个瓶颈。

下面的子章节提供了关于这些模式的更多详细信息,并解释了如何设置每种方法。

独立运行

独立模式是 Selenium Grid 基础架构的最简单方法。我们可以使用 shell 和 Java 代码执行此模式。

从 shell 开始

首先,我们可以使用 shell 和 Selenium Grid 二进制发行版来启动它。 Selenium Grid 是用 Java 开发的,每个发布版都作为一个自包含的 JAR 文件提供,包含了所有的依赖项(也称为 uber-JARfat-JAR)。您可以从Selenium 下载页面下载这个 fat-JAR。

在独立模式下,Selenium 服务器会自动检测系统中可用的驱动程序(例如 chromedriver、geckodriver 等)。为此,它会在 PATH 环境变量中查找这些驱动程序。通常情况下,我们可以手动管理这些驱动程序管理器。然而,建议使用 WebDriverManager 自动解析这些驱动程序。因此,正如在 附录 B 中解释的那样,WebDriverManager 可以用作 CLI 工具。WebDriverManager CLI 以 fat-JAR 的形式分发,并可在 GitHub 上下载。

为了说明这一点,示例 6-5 显示了在具有 WebDriverManager CLI 的 Linux 机器上解决 chromedriver 和 geckodriver 所需的 shell 命令。然后,我们使用 Selenium Grid 的 fat-JAR 来启动独立网格。请注意,这些命令是在相同的文件夹中执行的。这样,通过 WebDriverManager 下载的驱动程序可供 Selenium Grid 使用。

示例 6-5. 使用 WebDriverManager CLI 解决驱动程序并使用 shell 启动独立模式的 Selenium Grid 的命令
boni@linux:~/grid$ java -jar webdrivermanager-5.0.3-fat.jar resolveDriverFor chrome ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
[INFO] Using WebDriverManager to resolve chrome
[DEBUG] Detecting chrome version using online commands.properties
[DEBUG] Running command on the shell: [google-chrome, --version]
[DEBUG] Result: Version=94.0.4606.71
[DEBUG] Latest version of chromedriver according to
    https://chromedriver.storage.googleapis.com/LATEST_RELEASE_94 is 94.0.4606.61
[INFO] Using chromedriver 94.0.4606.61 (resolved driver for Chrome 94)
[INFO] Reading https://chromedriver.storage.googleapis.com/ to seek
    chromedriver
[DEBUG] Driver to be downloaded chromedriver 94.0.4606.61
[INFO] Downloading https://chromedriver.storage.googleapis.com/94.0.4606.61/
    chromedriver_linux64.zip
[INFO] Extracting driver from compressed file chromedriver_linux64.zip
[INFO] Driver location: /home/boni/grid/chromedriver

boni@linux:~/grid$ java -jar webdrivermanager-5.0.3-fat.jar resolveDriverFor firefox ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
[INFO] Using WebDriverManager to resolve firefox
[DEBUG] Detecting firefox version using online commands.properties
[DEBUG] Running command on the shell: [firefox, -v]
[DEBUG] Result: Version=92.0.0.7916
[DEBUG] Getting driver version for firefox92 from online versions.properties
[INFO] Using geckodriver 0.30.0 (resolved driver for Firefox 92)
[INFO] Reading https://api.github.com/repos/mozilla/geckodriver/releases to
    seek geckodriver
[DEBUG] Driver to be downloaded geckodriver 0.30.0
[INFO] Downloading https://github.com/mozilla/geckodriver/releases/download/
    v0.30.0/geckodriver-v0.30.0-linux64.tar.gz
[INFO] Extracting driver from compressed file geckodriver-v0.30.0-linux64.tar.gz
[INFO] Driver location: /home/boni/grid/geckodriver

boni@linux:~/grid$ java -jar selenium-server-4.0.0.jar standalone ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
INFO [LogManager$RootLogger.log] - Using the system default encoding
INFO [OpenTelemetryTracer.createTracer] - Using OpenTelemetry for tracing
INFO [NodeOptions.getSessionFactories] - Detected 8 available processors
INFO [NodeOptions.discoverDrivers] - Discovered 2 driver(s)
INFO [NodeOptions.report] - Adding Chrome for {"browserName": "chrome"} 8 times
INFO [NodeOptions.report] - Adding Firefox for {"browserName": "firefox"} 8 times
INFO [Node.<init>] - Binding additional locator mechanisms: name, id, relative
INFO [LocalDistributor.add] - Added node 41045bd8-ec7e-43c9-84bd-f63f7aca59ed
    at http://192.168.56.1:4444\. Health check every 120s
INFO [GridModel.setAvailability] - Switching node 41045bd8-ec7e-43c9-84bd-
    f63f7aca59ed (uri: http://192.168.56.1:4444) from DOWN to UP
INFO [Standalone.execute] - Started Selenium Standalone 4.0.0 (revision
    3a21814679): http://192.168.56.1:4444

1

我们使用 WebDriverManager CLI 来解决 chromedriver。

2

我们使用 WebDriverManager CLI 来解决 geckodriver(Firefox 所需的驱动程序)。

3

我们在包含 chromedriver 和 geckodriver 的同一文件夹中以独立模式启动 Selenium Grid。

执行这些命令后,独立的 Selenium 服务器将监听本地主机的端口 4444 上的传入 HTTP 请求。因此,我们可以使用该 URL(例如,如果测试在同一主机上执行,则为 http://localhost:4444/)和所需的能力(在本例中为 Chrome 或 Firefox)创建 RemoteWebDriver 的实例。例如,如下所示:

WebDriver driver = new RemoteWebDriver("http://localhost:4444/",
        new ChromeOptions());
注意

在 Selenium Grid 3 中,默认的 Selenium 服务器 URL 是 http://localhost:4444/wd/hub。在 Selenium Grid 4 中,尽管此 URL 也应该有效,但不再需要路径 /wd/hub

Selenium Grid 提供的另一个有用功能是其网页控制台。该控制台是通过 Selenium 服务器 URL 访问的 Web UI,允许监视网格中注册的可用浏览器和正在执行的会话。

图 6-3 显示了先前独立网格控制台的屏幕截图。请注意,在此情况下,独立的 Selenium 服务器可以为 Chrome 和 Firefox 提供最多八个并发会话(与运行网格的机器上可用处理器数量相同)。

hosw 0603

图 6-3. Selenium Grid 控制台

从 Java 代码

启动 Selenium Grid 的另一种方法是使用 Java。除了 fat-JAR 外,Selenium Grid 还使用 org.seleniumhq.selenium 作为 groupIdselenium-grid 作为 artifactId 在 Maven 中央仓库发布。因此,我们需要在项目设置(Maven 或 Gradle)中解析其坐标,以在我们的 Java 项目中使用它(有关配置详细信息,请参见 附录 B)。示例 6-6 演示了如何从 Java 测试用例中启动独立模式的 Selenium Grid。

示例 6-6. 在独立模式下启动 Selenium Grid 的测试
static URL seleniumServerUrl;

@BeforeAll
static void setupAll() throws MalformedURLException {
    int port = PortProber.findFreePort(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebDriverManager.chromedriver().setup(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    Main.main(
            new String[] { "standalone", "--port", String.valueOf(port) }); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    seleniumServerUrl = new URL(
            String.format("http://localhost:%d/", port)); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

@BeforeEach
void setup() {
    driver = new RemoteWebDriver(seleniumServerUrl, new ChromeOptions()); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
}

1

我们在本地主机上找到一个空闲端口。为此,我们使用 Selenium WebDriver API 中可用的 PortProber 类。

2

我们使用 WebDriverManager 来解决 chromedriver,因为要启动的独立网格将为 Chrome 浏览器提供服务。

3

我们以独立模式启动 Selenium Grid,调用其main方法。

4

我们使用之前选择的端口创建一个 Java URL

5

我们创建一个RemoteWebDriver的实例。像往常一样,我们在测试逻辑中使用这个对象来调用 Selenium WebDriver API 并控制浏览器(查看示例库以获取整个类的示例)。

Hub-nodes

Selenium Grid 的经典架构包括两种类型的主机:hub(即 Selenium 服务器)和一组节点。与独立模式类似,我们可以使用 Selenium Grid 的 fat-JAR 在 shell 中启动此模式。首先,在一个主机上启动 hub。然后,在同一主机或不同主机上注册一个或多个节点。示例 6-7 展示了如何在 Windows 控制台中执行这些命令。

示例 6-7. 使用 shell 启动 hub-nodes 模式的 Selenium Grid 命令
C:\grid>java -jar selenium-server-4.0.0.jar hub ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
INFO [LogManager$RootLogger.log] - Using the system default encoding
INFO [OpenTelemetryTracer.createTracer] - Using OpenTelemetry for tracing
INFO [BoundZmqEventBus.<init>] - XPUB binding to [binding to tcp://*:4442,
    advertising as tcp://192.168.56.1:4442], XSUB binding to [binding to
    tcp://*:4443, advertising as tcp://192.168.56.1:4443]
INFO [UnboundZmqEventBus.<init>] - Connecting to tcp://192.168.56.1:4442 and
    tcp://192.168.56.1:4443
INFO [UnboundZmqEventBus.<init>] - Sockets created
INFO [UnboundZmqEventBus.<init>] - Event bus ready
INFO [Hub.execute] - Started Selenium Hub 4.0.0 (revision 3a21814679):
    http://192.168.56.1:4444
INFO [Node.<init>] - Binding additional locator mechanisms: relative, name, id
INFO [LocalDistributor.add] - Added node 98c35075-e5f0-4168-be97-c277e4f40d8d
    at http://192.168.56.1:5555\. Health check every 120s
INFO [GridModel.setAvailability] - Switching node 98c35075-e5f0-4168-be97-
    c277e4f40d8d (uri: http://192.168.56.1:5555) from DOWN to UP

C:\grid>java -jar selenium-server-4.0.0.jar node ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
INFO [LogManager$RootLogger.log] - Using the system default encoding
INFO [OpenTelemetryTracer.createTracer] - Using OpenTelemetry for tracing
INFO [UnboundZmqEventBus.<init>] - Connecting to tcp://*:4442 and tcp://*:4443
INFO [UnboundZmqEventBus.<init>] - Sockets created
INFO [UnboundZmqEventBus.<init>] - Event bus ready
INFO [NodeServer.createHandlers] - Reporting self as: http://192.168.56.1:5555
INFO [NodeOptions.getSessionFactories] - Detected 8 available processors
INFO [NodeOptions.discoverDrivers] - Discovered 2 driver(s)
INFO [NodeOptions.report] - Adding Chrome for {"browserName": "chrome"} 8 times
INFO [NodeOptions.report] - Adding Firefox for {"browserName": "firefox"} 8
    times
INFO [Node.<init>] - Binding additional locator mechanisms: relative, name, id
INFO [NodeServer$1.start] - Starting registration process for node id
    98c35075-e5f0-4168-be97-c277e4f40d8d
INFO [NodeServer.execute] - Started Selenium node 4.0.0 (revision
    3a21814679): http://192.168.56.1:5555
INFO [NodeServer$1.lambda$start$1] - Sending registration event...
INFO [NodeServer.lambda$createHandlers$2] - Node has been added

1

我们启动 hub。默认情况下,此服务器在端口 4444 上监听 W3C WebDriver HTTP 请求,并在端口 4442 和 4443 上监听 TCP 端口以注册节点。

2

在第二个控制台中,我们注册节点(们)。在这个示例中,此命令在与 hub 相同的主机上执行。此外,假定所需的驱动程序(例如 chromedriver 和 geckodriver)已经解决(就像在示例 6-5 中一样)。要从另一个主机启动节点,我们需要调用以下命令:

java -jar selenium-server-4.0.0.jar node --hub http://<hub>:4444
注意

与独立模式相同,您可以使用 Java 代码启动 hub-nodes 网格。为此,您需要更改参数以调用 Selenium Grid 的主类,遵循 hub 和节点的 CLI 命令的相同语法。

完全分布式

从版本 4 开始,我们可以按照完全分布式的架构执行 Selenium Grid 基础架构。此方法的决定性方面是可伸缩性。专门的节点在此模式下处理不同的自动化和基础设施管理方面。这些节点包括:

路由器

作为 Grid 的单一入口点的节点。此组件监听来自 Selenium 脚本的 W3C WebDriver 命令。

会话队列

存储新会话请求的节点。这些传入会话等待由分发器读取。

分发器

节点了解所有节点及其能力。它定期向会话队列请求新的会话请求。

事件总线

在 Grid 架构的多个成员之间提供基于消息的通信渠道的组件。这种通信在图 6-4 和 6-5 中用虚线表示。

会话映射

它维护 WebDriver 会话和执行会话的节点之间的关系。

节点(们)

提供基于 Selenium WebDriver 进行自动化的网络浏览器的主机及其对应的驱动程序。

注意

在中心节点架构中,中心节点汇聚了完全分布模式下的路由器、会话队列、分发器、事件总线和会话映射的责任。

下面的小节详细介绍了完全分布式 Selenium Grid 中最相关的流程:节点注册、新会话以及其他 WebDriver 命令。

节点注册

操作分布式 Selenium Grid 所需的第一个过程是注册一个或多个节点。为此,节点需要在分发器中注册其能力。图 6-4 说明了此过程,包括三个步骤:

  1. 节点通过事件总线发送消息以宣布其能力。

  2. 此消息到达分发器,后者存储节点与能力之间的关系。

  3. 分发器通过与源节点交换 HTTP 消息(实线)来确保节点存在。

hosw 0604

图 6-4. Selenium Grid 分布式架构中的节点注册

新会话

在某个时刻,一个脚本(通常是一个测试用例)会尝试自动启动一个新的会话来驱动浏览器。图 6-5 描述了在完全分布式 Selenium Grid 中执行此过程所需的通信,即:

  1. 使用 Selenium WebDriver API 的脚本/测试向路由器发送请求以创建新会话(即通过编程方式驱动浏览器)。

  2. 路由器在会话队列中创建一个新条目以存储此新的会话请求。

  3. 分发器定期询问会话队列是否有新的会话请求。

  4. 一旦分发器发现新的会话请求,它会检查是否有节点能够支持此会话。如果会话是可能的(即分发器中之前注册的节点提供了所需的能力),分发器将与节点创建一个新会话。

  5. 分发器向会话映射发送 HTTP 消息以存储新会话。会话映射存储一个唯一的会话标识符(session id),该标识符唯一地关联执行浏览器会话的节点。

hosw 0605

图 6-5. Selenium Grid 分布式架构中的新会话

WebDriver 命令

一旦会话建立,Selenium WebDriver API 脚本将继续发送 W3C WebDriver 命令以控制远程节点中的网络浏览器。图 6-6 显示了在分布式 Selenium Grid 基础设施中执行这些通信步骤:

  1. 脚本/测试使用 W3C WebDriver 命令来驱动浏览器(例如打开网页、与网页元素交互等)当前会话中。

  2. 对同一浏览器会话的进一步请求使用相同的会话 ID。路由器通过读取会话映射来识别浏览器会话是否活动。

  3. 路由器直接将同一会话的后续命令转发到分配的节点。

hosw 0606

图 6-6. 在 Selenium Grid 分布式架构中的 WebDriver 命令

设置分布式网格

像独立模式和中心节点模式一样,我们可以使用 Selenium Grid 分发(作为一个 fat-JAR 或常规 Java 依赖项)来启动完全分布式的架构。示例 6-8 显示了使用命令行执行此操作所需的 shell 命令。

示例 6-8. 使用 shell 启动分布式模式下的 Selenium Grid 的命令
C:\grid>java -jar selenium-server-4.0.0.jar event-bus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
INFO [BoundZmqEventBus.<init>] - XPUB binding to [binding to tcp://*:4442,
    advertising as tcp://192.168.56.1:4442], XSUB binding to [binding to
    tcp://*:4443, advertising as tcp://192.168.56.1:4443]
...

C:\grid>java -jar selenium-server-4.0.0.jar sessions ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
...
INFO [SessionMapServer.execute] - Started Selenium SessionMap 4.0.0 (revision
    5fe1af712f): http://192.168.56.1:5556

C:\grid>java -jar selenium-server-4.0.0.jar sessionqueue ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
...
INFO [NewSessionQueueServer.execute] - Started Selenium SessionQueue 4.0.0
    (revision 5fe1af712f): http://192.168.56.1:5559

C:\grid>java -jar selenium-server-4.0.0.jar distributor --sessions
    http://<session_map>:5556 --sessionqueue http://<session_queue>:5559 --bind-bus
    false ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
...
INFO [DistributorServer.execute] - Started Selenium Distributor 4.0.0 (revision
    5fe1af712f): http://192.168.56.1:5553

C:\grid>java -jar selenium-server-4.0.0.jar router --sessions
    http://<session_map>:5556 --distributor http://<distributor_address>:5553
    --sessionqueue http://>session_queue>:5559 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
...
INFO [RouterServer.execute] - Started Selenium Router 4.0.0 (revision
    5fe1af712f): http://192.168.56.1:4444

C:\grid>java -jar selenium-server-4.0.0.jar node --publish-events
    tcp://<event_bus>:4442 --subscribe-events tcp://<event_bus>:4443 ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
...
INFO [NodeOptions.discoverDrivers] - Discovered 2 driver(s)
...
INFO [NodeServer$1.lambda$start$1] - Sending registration event...
INFO [NodeServer.lambda$createHandlers$2] - Node has been added

1

我们启动事件总线。默认情况下,事件总线监听 TCP 端口 4442 和 4443。

2

我们启动会话映射。默认情况下,此组件在端口 5556 上侦听传入的 HTTP 消息。

3

我们启动会话队列。默认情况下,此队列在端口 5559 上侦听 HTTP。

4

我们启动分发器。为此,我们需要指定会话映射和会话队列地址。此外,由于我们已经独立启动了事件总线,我们将 --bind-bus 标志设置为 false。默认情况下,分发器使用端口 5553 进行 HTTP 通信。

5

我们启动路由器。我们需要指定会话映射、会话队列和分发器的 URL。

6

我们启动节点。我们需要指定事件总线用于发布-订阅消息的端口。此外,在此示例中,相同文件夹中有几个驱动程序(chomedriver 和 geckodriver)可供使用。

可观测性

在软件工程中,可观测性 是根据软件系统的外部输出或信号确定其当前状态的一种度量。通过这种方式,可观测性允许利用其外部指标了解系统的内部状态。可观测性对于维护复杂的软件系统和确定任何问题的根本原因可能至关重要。为此目的,可观测性的三个支柱是:

指标

随时间变化的系统性能指标,例如响应时间、每秒事务数或内存使用量等等

日志

系统在运行代码时生成的文本行(通常带有时间戳)

跟踪

描述因果关系的分布式事件(例如选择的日志),其特征是给定操作在软件系统中的请求流

Selenium Grid 4 提供了不同的功能来测量观测性。首先,Selenium Grid 允许使用OpenTelemetry API 进行跟踪。其次,Selenium Grid 提供了一个 GraphQL 端点来针对网格运行查询。

使用 OpenTelemetry 进行跟踪

跟踪 是基于软件系统的日志和度量标准的重要观测测量方式。Selenium Grid 通过两种方式公开跟踪。首先,我们可以在 Shell 中执行网格时检查日志跟踪。默认情况下,显示INFO级别的日志。我们可以使用 Shell 命令中的参数 --log-level 来更改级别,例如:

java -jar selenium-server-4.0.0.jar standalone --log-level FINE

此外,Selenium Grid 支持通过 OpenTelemetry API 进行分布式跟踪。此功能允许跟踪通过 Selenium Grid 基础设施传递的命令。分布式跟踪需要按照以下顺序进行两项活动:

1. 代码仪器化

Selenium Grid 允许使用 OpenTelemetry API 导出跟踪信息。

2. 数据收集

例如,我们可以使用Jaeger,一个开源的分布式跟踪平台,它与 OpenTelemetry 提供无缝集成。它允许查询、可视化和收集跟踪数据。

以下命令展示了如何设置 Selenium Grid 以将数据导出到 Jaeger。首先,我们需要一个运行中的 Jaeger 后端。为此,我们可以从Jaeger 下载页面下载可执行二进制文件。或者,我们可以使用 Docker 启动服务器,如下所示:

docker run --rm -it --name jaeger \
  -p 16686:16686 \ ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
  -p 14250:14250 \ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
  jaegertracing/all-in-one:1.27

1

我们将使用 URL http://localhost:16686 来访问 Jaeger UI。

2

我们将使用 URL http://localhost:14250 来收集数据(由 Selenium Grid 导出)。

接着,我们按如下方式启动 Selenium Grid:

java -Dotel.traces.exporter=jaeger \
  -Dotel.exporter.jaeger.endpoint=http://localhost:14250 \ ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
  -Dotel.resource.attributes=service.name=selenium-standalone \ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
  -jar selenium-server-4.0.0.jar \
  --ext $(cs fetch -p \ ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
     io.opentelemetry:opentelemetry-exporter-jaeger:1.6.0 \
     io.grpc:grpc-netty:1.41.0) \
  standalone

1

我们使用 Jaeger 端点来导出跟踪数据。

2

我们指定服务名称为selenium-standalone。我们将在 Jaeger UI 中查找此名称,以可视化收集的数据(见图 6-7)。

3

我们使用Coursier来下载并生成两个所需依赖项(opentelemetry-exporter-jaegergrpc-netty)的类路径。

hosw 0607

图 6-7. Jaeger UI 显示从 Selenium Grid 收集的数据

GraphQL 查询

GraphQL 是一个用于 API 的开源数据查询和操作语言。GraphQL 定义了一种从在线服务请求数据的语法。Selenium Grid 4 提供了一个 GraphQL 端点(http://localhost:4444/graphql)。通过 shell 使用 curl 对此端点进行 GraphQL 查询是一个简单的方法。例如,假设我们在本地主机上运行了一个 Selenium Grid,我们可以向 GraphQL 端点发送以下命令来获取网格中的最大会话数和当前会话数:

curl -X POST -H "Content-Type: application/json" --data \
    '{"query": "{ grid {maxSession, sessionCount } }"}' -s \
    http://localhost:4444/graphql

配置

您可以在其官方文档中找到有关 Selenium Grid 的更多详细信息。对于高级配置,有两种方式可以为 Selenium Grid 指定自定义设置:

使用 Selenium Grid 不同方面的 CLI 选项

这些选项的一些示例是 --port 用于更改 Selenium Server 监听的默认端口(默认为 4444),或者 --session-timeout,即在没有活动时终止节点的超时时间(默认为三百秒)。

使用 TOML 文件

TOML(Tom’s Obvious Minimal Language)是一种设计成易于阅读的配置格式。与 CLI 选项一样,这些文件允许使用 TOML 表示法配置 Selenium Grid 参数。

云提供商

正如在第一章中介绍的,Selenium 生态系统中的云提供商是为自动化的 Web 和移动测试提供托管服务(通常是商业服务)的公司。云提供商通常提供的服务包括:

浏览器作为服务

可以请求由提供商托管的按需网页浏览器。这些浏览器通常是不同类型、版本和操作系统的。此功能通常用于跨浏览器的自动化或实时测试。

分析能力

为了监控和调试自动化测试。为此,云提供商通常支持会话录制或丰富的错误报告功能。

移动测试

可以请求在不同平台上仿真(和真实的)移动设备,例如 Android 和 iOS。

视觉测试

自动检查 UI 并确保最终用户拥有正确的视觉体验。

目前 Selenium 的云提供商示例包括 Sauce Labs, BrowserStack, LambdaTest, CrossBrowserTesting, Moon Cloud, TestingBot, PerfectoTestinium。所有这些公司都提供具有不同定价计划的特定服务。它们的共同点是每个云提供商维护一个 Selenium Server 终端节点,我们可以在 RemoteWebDriver 测试中使用。示例 6-9(见 saucelabs_setup)说明了如何使用其中之一(具体来说,是 Sauce Labs)创建 WebDriver 对象。您可以在 示例存储库 中找到其他云提供商(BrowserStack、LambdaTest、CrossBrowserTesting、Perfecto 和 Testinium)的等效测试。这些测试允许使用由云提供商管理的远程浏览器。

示例 6-9. 使用 Sauce Labs 的测试设置
@BeforeEach
void setup() throws MalformedURLException {
    String username = System.getProperty("sauceLabsUsername"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    String accessKey = System.getProperty("sauceLabsAccessKey");
    assumeThat(username).isNotEmpty(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    assumeThat(accessKey).isNotEmpty();

    MutableCapabilities capabilities = new MutableCapabilities();
    capabilities.setCapability("username", username); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    capabilities.setCapability("access_key", accessKey);
    capabilities.setCapability("name", "My SauceLabs test"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    capabilities.setCapability("browserVersion", "latest"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    ChromeOptions options = new ChromeOptions();
    options.setCapability("sauce:options", capabilities); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    URL remoteUrl = new URL(
            "https://ondemand.eu-central-1.saucelabs.com:443/wd/hub"); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)

    driver = new RemoteWebDriver(remoteUrl, options); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

1

要使用 Sauce Labs,我们需要一个有效的账户。换句话说,我们需要以用户名和访问密钥的形式拥有凭据。为了避免在测试逻辑中硬编码这些凭据,我在这个测试中使用了 Java 系统属性。这些属性可以在执行命令时指定(例如,mvn test -DsauceLabsUsername=myname -DsauceLabsAccessKey=mykey)。指定这些数据的另一种方式是使用环境变量(例如,String username = Sys⁠tem.getenv("SAU⁠CE⁠LABS_USERNAME");)。

2

当用户名或密钥不可用时,我们跳过此测试(使用假设)。

3

我们需要将用户名和密钥作为 Selenium 的能力包含在内。

4

我们可以指定一个自定义标签来在 Sauce Labs 仪表板中标识此测试(参见图 6-8)。

5

我们使用给定浏览器的最新版本(如下一行所指定的 Chrome)。

6

我们在 Sauce Labs 云中使用一个名为​​sauce:options的自定义标签来选择所需的能力。

7

我们将 Sauce Labs 的公共终端点用作远程 URL。Sauce Labs 在不同地区提供终端点。在此示例中,我使用的是 EU Central 数据中心。其他可能的包括 US WestUS EastAsia-Pacific Southeast

8

我们使用 URL 和能力来创建 RemoteWebDriver 的实例。

hosw 0608

图 6-8. Sauce Labs 仪表板显示的自动化测试结果

Docker 容器中的浏览器

Docker 是一个开源平台,允许创建、部署和运行轻量级、便携式容器化应用程序。Docker 平台由两个主要组件组成:

  • Docker 引擎,一个允许在主机中创建和运行容器的应用程序。Docker 引擎是一个客户端-服务器应用程序,由三个元素组成:

    • 作为守护进程实现的服务器(dockerd

    • 应用程序客户端用于指导守护程序的 REST API

    • 一个 CLI 工具(docker 命令)

  • Docker Hub,用于分发容器的云服务。

在 Selenium 中,Docker 可以是支持基于 Selenium WebDriver 的自动化测试所需的浏览器基础设施的相关技术。以下子章节解释了在 Docker 容器中执行浏览器的替代方案。

Selenium Grid 的 Docker 镜像

Selenium 套件的官方子项目是docker-selenium。该项目维护了不同 Selenium Grid 组件(如独立、中心、节点、路由器、分发器、会话队列等)和 Web 浏览器(Chrome、Firefox 和 Edge)的 Docker 镜像。这些 Docker 镜像是开源的,并在Docker Hub上发布。使用这些镜像的简单方法是通过 shell(使用 docker 命令)启动它们,并使用RemoteWebDriver的实例来驱动dockerized浏览器。以下子章节解释了如何执行这些操作。

注意

此节中展示的命令和测试假设您的系统中已安装了 Docker。换句话说,您需要在您的机器上安装 Docker 引擎,以便正确执行这些示例。

独立模式

我们可以在 Docker Hub 中找到独立浏览器(Chrome、Firefox 和 Edge)的 Selenium 镜像。以下命令展示了如何在 Docker 中使用 shell 启动 Chrome。

docker run -d -p 4444:4444 --shm-size="2g" selenium/standalone-chrome:latest

此命令启动 Docker 镜像 selenium/standalone-chrome:latest,即 Docker Hub 中可用的最新版本的 Chrome。或者,我们可以使用固定的 Chrome 版本(例如 selenium/standalone-chrome:94.0)。使用 -d 标志以分离模式启动 Docker 容器(使用共享内存 2 GB --shm-size="2g")。这个数值已知可用,但根据您的资源或特定需求,您可以更改它。最后,将容器的内部端口 4444 映射到执行命令的主机的同一端口(-p 4444:4444)。然后,我们可以使用以下 Java 命令来实例化一个使用这个 dockerized Chrome 的 WebDriver 对象:

WebDriver driver = new RemoteWebDriver("http://localhost:4444/",
        new ChromeOptions());

此外,当使用 Selenium Grid 时,我们可以使用 Docker 容器注册节点。以下命令展示了如何使用 Docker 中的 Firefox 启动独立模式的 Selenium Grid:

java -jar selenium-server-4.0.0.jar node -D selenium/standalone-firefox:latest
        '{"browserName": “firefox"}'

中心节点

我们可以轻松地使用官方 Selenium Docker 镜像在中心节点模式下启动 Selenium Grid。以下命令展示了如何在 shell 中执行此操作。

docker network create grid ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

docker run -d -p 4442-4444:4442-4444 --net grid \
    --name selenium-hub selenium/hub:4.0.0 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

docker run -d --net grid -e SE_EVENT_BUS_HOST=selenium-hub --shm-size="2g" \
    -e SE_EVENT_BUS_PUBLISH_PORT=4442  -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
    selenium/node-chrome:4.0.0 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

docker network rm grid ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

1

首先,我们创建一个名为 grid 的 Docker 网络。该网络允许 hub 和节点之间使用它们的主机名进行通信(例如,selenium-hub)。

2

我们启动 Selenium Hub。我们需要映射端口 4444(用于 Selenium 服务器 URL)和 4442-4443(用于注册节点)。

3

我们注册节点。在此命令中,我们使用 Chrome (selenium/node-chrome)。其他浏览器可以使用其他 Docker 镜像(例如,selenium/node-firefoxselenium/node-edge)在 hub 中注册。

4

如果不再需要,我们可以在最后删除 grid 网络。

更多功能

项目 docker-selenium 提供了各种功能。我建议您查看其 README 以获取更多详细信息。以下是这些功能的摘要:

Docker Compose 脚本

这些脚本可以轻松启动 Selenium Grid hub-nodes 和完全分布模式。

视频录制

我们可以使用另一个 Docker 容器记录节点中浏览器的桌面会话。

动态网格

这使我们能够按需启动 Docker 容器。

部署到 Kubernetes

Kubernetes 是一个开源的容器编排系统,自动化容器化应用程序的部署和管理。我们可以使用 Kubernetes 来部署 Selenium Docker 容器。

高级容器配置

这可以用来指定 Selenium 或 Java 自定义配置,例如。

访问远程会话

这可以通过使用虚拟网络计算(VNC)(一种图形桌面共享系统)和 noVNC(一个开源的基于 web 的 VNC 客户端)来实现。

Selenoid

Selenoid 是 Selenium Hub 的一个开源 Golang 实现。Selenoid 可以被视为基于 Docker 提供浏览器基础设施的轻量级 Selenium 服务器。Selenoid 团队还维护着 Selenoid 使用的 Docker 镜像。这些镜像包括多个 web 浏览器和 Android 设备,例如 Chrome、Firefox、Edge、Opera、Safari(WebKit 引擎)或 Chrome Mobile。

有不同的方式来使用 Selenoid 及其 Docker 镜像。一个简单的方法是使用项目提供的配置管理器(一个名为 cm 的二进制文件)。以下代码段显示了如何启动 Selenoid 及其 UI(一个基于 web 的仪表板,用于监控 Selenoid):

./cm selenoid start ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
./cm selenoid-ui start ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

1

我们启动 Selenoid。配置管理器下载 Selenoid 的 Docker 镜像和几个浏览器的最新两个版本(Chrome、Firefox 和 Opera)。一旦启动,Selenoid 会监听 Selenium WebDriver 请求的 URL http://localhost:4444/wd/hub

2

可选地,我们可以启动 Selenoid UI。此 UI 是一个 Web 应用程序,可通过 URL http://localhost:8080/ 访问。图 6-9 显示了执行 Selenium WebDriver 测试期间此 UI 的屏幕截图。示例 6-10 显示了使用 Selenoid 服务的 Chrome 浏览器的测试设置。

hosw 0609

图 6-9. 使用 VNC 运行测试时的 Selenoid UI
示例 6-10. 使用构造函数实例化 RemoteWebDriver 对象
@BeforeEach
void setup() throws MalformedURLException {
    URL seleniumServerUrl = new URL("http://localhost:4444/wd/hub");
    assumeThat(isOnline(seleniumServerUrl)).isTrue();

    ChromeOptions options = new ChromeOptions();
    Map<String, Object> selenoidOptions = new HashMap<>();
    selenoidOptions.put("enableVNC", true); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.setCapability("selenoid:options", selenoidOptions); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    driver = new RemoteWebDriver(seleniumServerUrl, options);
}

1

enableVNC 功能是 Selenoid 特有的,允许我们启动带有 VNC 支持的 dockerized 浏览器(这样,我们可以在 Selenoid UI 中可视化浏览器会话,如 图 6-9 所示)。

2

由于此功能是特定供应商的,设置此功能的 W3C WebDriver 兼容方式是使用自定义命名空间(在本例中为 selenoid:options)。

更多功能

Selenoid 提供不同的功能和配置能力。您可以查看其 文档 了解更多详情。这些功能包括视频录制、自定义配置、日志管理或访问浏览器开发者工具等。

WebDriverManager

截至版本 5,WebDriverManager 允许在 Docker 容器中轻松使用网页浏览器。为此,每个管理器(例如,chromedriver()firefoxdriver() 等)都提供了 browserInDocker() 方法。当调用 create() 方法时,WebDriverManager 内部会拉取 Docker 镜像并运行容器,在需要时创建一个 RemoteWebDriver 实例。WebDriverManager 使用由 Selenoid 团队维护的 Docker 镜像。通过 WebDriverManager,你可以直接使用 Chrome(桌面版和移动版)、Firefox、Edge、Opera 和 Safari 作为 Docker 容器。示例 6-11 展示了使用此功能进行基本测试的情况。

示例 6-11. 使用 WebDriverManager 和 Docker 中的 Chrome 进行完整测试
class DockerChromeJupiterTest {

    WebDriver driver;

    WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeEach
    void setupTest() {
        assumeThat(isDockerAvailable()).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        driver = wdm.create(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

    @AfterEach
    void teardown() {
        wdm.quit(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    @Test
    void testDockerChrome() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们获取 Chrome 的管理器实例(chromedriver())。然后,使用 WebDriverManager 流畅的 API,我们指定使用该实例创建的未来 WebDriver 对象(称为 wmd)将使用 Docker 执行相应的浏览器(在本例中为 Chrome)。

2

我们假设运行此测试的机器上有 Docker 引擎。为此,我们通过调用 WebDriverManager 的静态方法 isDockerAvailable 创建一个 AssertJ 断言。这样,当 Docker 不可用时,测试将被跳过。

3

在测试设置中,我们创建 WebDriver 实例。在内部,WebDriverManager 将连接到 Docker Hub,以发现可用作 Docker 镜像的最新版本的 Chrome。然后将该镜像拉取到本地机器,执行 Docker 容器,并将相应的 RemoteWebDriver 实例返回给测试逻辑。

4

WebDriverManager 允许通过 quit() 方法退出先前创建的 WebDriver 实例。这个方法的效果与直接退出实例相同(在这种情况下是 driver.quit()),并且使用的 Docker 容器会优雅地终止。

WebDriverManager 提供了一个流畅的 API 来配置 dockerized Web 浏览器的不同方面。以下代码片段展示了几个可能性。通常情况下,您可以在本书的示例仓库中找到使用这些功能的完整测试。

WebDriverManager wdm = WebDriverManager.firefoxdriver().browserInDocker(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker()
        .browserVersion("beta"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker()
        .enableVnc(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker()
        .enableRecording(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

1

我们选择一个指定的管理器来使用相应的 dockerized 浏览器(在本例中是 Firefox)。除了 Chrome 和 Firefox 外,其他可选项还包括 Edge、Opera、Safari 和 Chrome Mobile。

2

默认情况下,WebDriverManager 在 Docker Hub 上使用最新版本的 dockerized 浏览器。尽管如此,我们可以强制使用指定的版本(例如,94.0)。此外,还可以使用不同的通配符来指定以下版本:

latest

使用最新版本(默认选项)。

latest-N

要使用稳定发布的先前版本。例如,如果我们指定 latest-1(即最新版本减一),则使用稳定发布之前的版本。

beta

使用 beta 版本。此版本仅适用于 Chrome 和 Firefox,使用由 Twilio 维护的 Aerokube Docker 镜像分支。

dev

使用开发版本(同样适用于 Chrome 和 Firefox)。

3

使用 VNC 或 noVNC 连接到远程桌面会话。默认情况下,WebDriverManager 在日志跟踪中打印 noVNC URL。此外,可以通过调用 wdm.getDockerNoVncUrl() 方法访问此 URL。Figure 6-10 展示了一个允许观看和与 noVNC 远程会话进行交互的 Web 浏览器。

4

在测试结束时启用会话录制。您可以在项目根目录中找到录制文件(MP4 格式)。

hosw 0610

图 6-10. 使用 WebDriverManager 启动的 dockerized 浏览器的 noVNC 远程桌面

更多功能

正如其文档所述,您可以通过多种方式配置 WebDriverManager。例如,您可以指定 docker 化浏览器的细粒度方面,如时区、网络、共享内存、卷、环境变量、屏幕分辨率或录制输出等。此外,WebDriverManager 可以用作 Selenium 服务器。该服务器使用从 Docker Hub 拉取的容器映像来支持浏览器基础设施。

Selenium-Jupiter

Selenium-Jupiter 在 Docker 容器中使用 WebDriverManager 来管理和处理 Web 浏览器。对于 docker 化的浏览器,Selenium-Jupiter 提供了注解@DockerBrowser。您可以在测试方法中使用这个注解,配合WebDriverRemoteWebDriver参数使用。示例 6-12 演示了这个特性。在这个示例中,我们在 Docker 中使用 Chrome。

示例 6-12. 使用 Selenium-Jupiter 和 Docker 中的 Chrome 完成测试
@EnabledIfDockerAvailable ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@ExtendWith(SeleniumJupiter.class)
class DockerChromeSelJupTest {

    @Test
    void testDockerChrome(@DockerBrowser(type = CHROME) WebDriver driver) {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们用 Selenium-Jupiter 的注解@EnabledIf​Dock⁠erAvailable装饰测试类。此注解在运行测试的机器上未安装 Docker 时会禁用测试。

注解@DockerBrowser允许设置不同的方面和功能。下面的代码片段展示了其中一些。

@DockerBrowser(type = FIREFOX) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

@DockerBrowser(type = CHROME, version = "beta") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

@DockerBrowser(type = CHROME, vnc = true) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

@DockerBrowser(type = CHROME, recording = true) ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

1

我们可以使用type属性来更改浏览器。接受的值包括CHROMEFIREFOXOPERAEDGESAFARICHROME_MOBILE

2

我们可以使用version属性更改浏览器版本。与 WebDriverManager 类似,Selenium-Jupiter 允许指定固定版本值(例如94.0),并使用通配符latestlatest-N,以及betadev用于 Chrome 和 Firefox。

3

我们通过vnc属性启用对远程桌面会话的访问,使用 VNC 和 noVNC。

4

我们使用recording属性启用会话录制。

您可以在其文档中找到有关 Selenium-Jupiter 更多详细信息、示例和配置能力。

总结与展望

Selenium WebDriver 允许控制远程 Web 浏览器。这一特性是可行的,因为底层通信协议(W3C WebDriver)基于 HTTP 上的 JSON 消息。这样,Selenium WebDriver 架构的组件(如 Selenium Server、节点或客户端脚本)可以分布式部署(即在不同的主机上执行)。要在 Java 中使用这一特性,我们需要创建一个 RemoteWebDriver 实例,通常传递两个参数:Selenium Server 的 URL 和所需的能力。我们可以使用 Selenium Grid 启动 Selenium Server 基础设施(独立模式、集线器-节点模式或完全分布式模式)。另外,我们还可以使用云服务提供商提供的托管服务(例如 Sauce Labs、BrowserStack、LambdaTest 或 CrossBrowserTesting 等)。最后,我们可以使用 Docker 支持容器化的 Web 浏览器基础设施。

本章结束了本书的第二部分,其中您发现了 Selenium WebDriver API 的主要特性。本书的下一部分涵盖了使用 Selenium WebDriver API 开发端到端测试的不同方面,从页面对象模型(POM)开始,这是一种广泛使用的设计模式,用于增强测试维护和减少 Selenium WebDriver 中的代码重复。

第三部分:高级概念

这最后一部分涵盖了建立在 Selenium WebDriver API 之上的不同方面和用例。首先,您将学习页面对象模型(POM),这是一种广泛使用的设计模式,允许开发可重用和可维护的 WebDriver 测试。接下来的章节解释了强大的跨浏览器测试技术,如参数化测试、测试顺序或并行执行。接下来的章节描述了如何与 Selenium WebDriver 一起使用第三方库和框架,如 Cucumber 或 Spring Framework 等。最后一章总结了与 Selenium WebDriver 相辅相成的各种库,如 Appium 或 REST Assured。最后,您将了解当前 Selenium 的替代方案的主要特性,例如 Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。

第七章:页面对象模型(POM)

设计模式 是软件工程中解决重复问题的可重用解决方案。本章介绍了页面对象模型(Page Object Model,POM),这是一种常用的设计模式,用于开发 Selenium WebDriver 测试。使用 POM 有不同的好处,例如提高可重用性和避免代码重复。POM 基于为单个存储库建模 SUT UI 的 页面类,稍后从测试逻辑中使用。

动机

使用 Selenium WebDriver 开发端到端测试的一些最大挑战是 可维护性不可靠性。关于前者,问题可能发生在 SUT 的开发或演变过程中。对 UI 进行的更改可能导致现有的端到端测试失败。在具有大量测试用例且存在代码重复的大型测试套件中修复这些测试的维护成本可能是相关的(例如,在不同测试中重复使用相同的定位器时)。

关于不可靠性(即 flakiness),当测试在相同条件下(测试逻辑、输入数据、设置等)周期性地既通过又失败时,测试就是 flaky 的。在 Selenium WebDriver 测试中,测试不可靠性的两个主要原因是什么?首先,问题的根源可能是 SUT。例如,服务器端逻辑中的错误(例如,竞争条件)可能会在端到端测试中暴露出不稳定的行为。在这种情况下,开发人员和测试人员应该共同努力检测和解决问题,通常是修复服务器端错误。其次,问题可能出现在测试本身。这是测试人员应该避免的不良情况。有不同的策略可以预防 Selenium WebDriver 测试中的不可靠性,例如实施强大的定位器策略(以避免由于响应性或视口更改而导致的脆弱测试)或使用等待策略(以处理 Web 应用程序的分布式和异步特性,如 “等待策略” 中所解释的)。

利用像 POM 这样的设计模式可以帮助减少代码重复并增强可维护性问题。此外,我们可以使用 POM 来包含可重用的强大定位和等待策略。以下部分描述了如何执行 POM 设计模式。

注意

POM 设计模式本身并不严格是测试不可靠性的解决方案。然而,如下文所述,它使得能够封装可重用的代码,从而防止测试的不可靠性。

POM 设计模式

POM 设计模式的原则是将处理 UI 元素的逻辑与测试逻辑分离在单独的类(称为 页面类)中。换句话说,我们按照面向对象的范式对 SUT 的外观和行为进行建模,即 页面对象。然后,这些页面对象由 Selenium WebDriver 测试使用。

让我们看一个简单的例子来说明 POM。考虑包含登录表单的图 7-1,这个页面通常包含在练习站点上。示例 7-1 展示了使用普通 Selenium WebDriver 的测试用例。在编程中,我们使用“普通”一词来指代未经定制的原始形式技术。在这种情况下,我们使用标准的 Selenium WebDriver API,本书的第二部分有详细解释。

hosw 0701

图 7-1. 带有登录表单的练习网页
示例 7-1. 使用普通 Selenium WebDriver 实现成功登录的测试
@Test
void testVanillaBasicLogin() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");

    driver.findElement(By.id("username")).sendKeys("user"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    driver.findElement(By.id("password")).sendKeys("user"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    driver.findElement(By.cssSelector("button")).click(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    assertThat(driver.findElement(By.id("success")).isDisplayed()).isTrue(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们在网页表单中将单词user输入为用户名。

2

我们在网页表单中将相同的单词输入为密码。

3

我们点击提交按钮。

4

我们验证成功框是否显示。

这个测试是完全正确的,但是如果我们使用相同的网页实现额外的测试,可能会出现潜在的问题。例如,示例 7-2 展示了另一个使用普通 Selenium WebDriver 实现的测试用例,用于使用相同的网页表单执行负面测试(登录失败)。这个测试再次是正确的,但是与 示例 7-1 一起,我们重复了大部分定位网页元素的逻辑,只是使用了不同的输入数据和预期结果。这种做法违反了软件设计中最重要的原则之一:不要重复自己(DRY)。这是有问题的,因为在不同的地方使用相同的代码会使可维护性变得更加困难。

示例 7-2. 使用普通 Selenium WebDriver 实现失败登录的测试
@Test
void testVanillaBasicLoginFailure() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");

    driver.findElement(By.id("username")).sendKeys("bad-user");
    driver.findElement(By.id("password")).sendKeys("bad-password");
    driver.findElement(By.cssSelector("button")).click();

    assertThat(driver.findElement(By.id("invalid")).isDisplayed()).isTrue();
}

页面对象

页面对象类允许将专注于 UI 的代码(如定位器和页面布局)与测试逻辑分离。我们可以将页面类视为一个单一的存储库,封装了被测试应用程序提供的操作或服务。这些类在不同的测试用例中作为页面对象实例化。我们可以使用这些对象中公开的方法来实现端到端测试,同时避免代码重复。

下面是使用页面对象的基本示例。在下面的示例中,我们重构了上一节中解释的测试(即使用登录表单),使用页面对象而不是普通的 Selenium WebDriver。第一步是创建一个 Java 类来模拟登录页面。示例 7-3 展示了这个页面类的一个非常基本的实现。

示例 7-3. 基本页面类,用于模拟练习登录表单
public class BasicLoginPage {

    WebDriver driver; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    By usernameInput = By.id("username"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    By passwordInput = By.id("password");
    By submitButton = By.cssSelector("button");
    By successBox = By.id("success");

    public BasicLoginPage(WebDriver driver) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        this.driver = driver;

        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");
    }

    public void with(String username, String password) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        driver.findElement(usernameInput).sendKeys(username);
        driver.findElement(passwordInput).sendKeys(password);
        driver.findElement(submitButton).click();
    }

    public boolean successBoxPresent() { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        return driver.findElement(successBox).isDisplayed();
    }

}

1

我们声明了一个WebDriver类属性。这个变量用于在页面对象中实现与网页的交互。

2

我们将所有必需的定位器声明为额外的属性。在这种情况下,我们定位用户名和密码的文本输入框、提交按钮以及成功框。

3

此页面类定义的构造函数接受WebDriver对象。我们使用构造函数加载测试中的页面。

4

我们声明一个方法来模拟进行登录所需的交互,即输入用户名和密码,然后点击提交按钮。

5

声明另一个方法来检查成功框是否可见。

现在,我们可以在测试用例中使用这个页面类。示例 7-4 展示了如何使用。请注意,在每个测试之前,像往常一样创建WebDriver实例,并在每个测试结束后退出它。我们将这个驱动程序作为页面类构造函数的参数使用。

示例 7-4. 使用基本页面类实现成功的登录
class BasicLoginJupiterTest {

    WebDriver driver;
    BasicLoginPage login; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
        login = new BasicLoginPage(driver); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testBasicLoginSuccess() {
        login.with("user", "user"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        assertThat(login.successBoxPresent()).isTrue(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

}

1

我们在测试类中将页面类声明为一个属性。

2

我们创建页面对象,传递WebDriver实例。

3

我们调用页面类定义的方法with来执行登录操作。

4

我们使用页面对象提供的方法验证结果网页上的成功框是否可用。

这种方法是改进测试可维护性的一个方便的起点,因为现在登录网页相关的所有逻辑都集中在一个可重用的类中。然而,页面类中的代码仍然很脆弱。例如,假设我们需要为登录页面实现一个负面测试,即使用错误的凭据进行登录尝试。给定页面类的当前实现,示例 7-5 似乎是一个合理的方法。然而,如果你运行这个测试,你会发现由于NoSuchElementException异常而导致测试失败。下一节将解释如何通过创建更健壮的页面对象来解决这个潜在问题。

示例 7-5. 使用基本页面类实现失败的登录
@Test
void testBasicLoginFailure() {
    login.with("bad-user", "bad-password");
    assertThat(login.successBoxPresent()).isFalse();
}

健壮的页面对象

前文提到的示例提升了代码可维护性,因为页面操作封装在一个单独的类中,而不是散布在整个测试套件中。话虽如此,有多种方法可以增强之前的页面类实现。首先,我们的系统可能有多个网页,而不仅仅是一个。因此,一个常见的策略是采用面向对象的方法,创建一个基础页面类,封装所有页面类的通用逻辑。示例 7-6 展示了一个实现页面类典型基础的 Java 类。

示例 7-6. 页面类的基础类示例
public class BasePage {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;
    WebDriverWait wait;
    int timeoutSec = 5; // wait timeout (5 seconds by default) 
    public BasePage(WebDriver driver) {
        this.driver = driver;
        wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSec)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    public void setTimeoutSec(int timeoutSec) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        this.timeoutSec = timeoutSec;
    }

    public void visit(String url) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        driver.get(url);
    }

    public WebElement find(By element) {
        return driver.findElement(element);
    }

    public void click(By element) {
        find(element).click();
    }

    public void type(By element, String text) {
        find(element).sendKeys(text);
    }

    public boolean isDisplayed(By locator) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        try {
            wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
        } catch (TimeoutException e) {
            log.warn("Timeout of {} wait for {}", timeoutSec, locator);
            return false;
        }
        return true;
    }

}

1

我们在基础类中定义了一个显式等待(WebDriverWait)属性。我们在构造函数中使用默认的超时值(例如本例中的五秒)实例化此属性。

2

我们创建了一个 setter 方法来更改等待超时的默认值。例如,根据系统响应时间,我们可能需要调整此超时。

3

我们创建了几个常用方法,页面类可以重用,例如visit()(打开网页)、find()(定位网页元素)或type()(向可写元素发送数据,例如输入字段)。

4

我们实现了一个方法来检查网页元素是否显示。请注意,此方法隐藏了等待此元素的复杂性,返回一个简单的布尔值,测试可以使用该值。

我们使用之前的基础类作为特定页面类的父类。例如,示例 7-7 展示了一个 Java 类,它扩展了这个基础类以实现页面类,使用练习站点中的登录示例页面。

示例 7-7. 使用前述基础类的登录页面类
public class LoginPage extends BasePage {

    By usernameInput = By.id("username"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    By passwordInput = By.id("password");
    By submitButton = By.cssSelector("button");
    By successBox = By.id("success");

    public LoginPage(WebDriver driver, int timeoutSec) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        this(driver);
        setTimeoutSec(timeoutSec);
    }

    public LoginPage(WebDriver driver) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        super(driver);
        visit("https://bonigarcia.dev/selenium-webdriver-java/login-form.html");
    }

    public void with(String username, String password) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        type(usernameInput, username);
        type(passwordInput, password);
        click(submitButton);
    }

    public boolean successBoxPresent() { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        return isDisplayed(successBox);
    }

}

1

我们将页面定位器定义为类属性。

2

我们定义了一个构造函数,带有两个参数:WebDriver对象和超时值(以秒为单位)。

3

我们定义另一个构造函数,用于打开测试中的网页。

4

我们包括了一个方法,使用用户名和密码作为凭证进行登录。此方法使用了父类中定义的type()click()方法。

5

我们还包括了另一个方法来检查成功框是否可见(使用基础类中定义的isDisplayed()方法)。

最后,我们可以使用页面类来实现一个 Selenium WebDriver 测试。示例 7-8 展示了使用 JUnit 5 进行的测试(通常情况下,你可以在 示例库 中找到 JUnit 4、TestNG 和 Selenium-Jupiter 版本)。

示例 7-8. 使用页面类实现成功和失败登录的测试
class LoginJupiterTest {

    WebDriver driver;
    LoginPage login;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
        login = new LoginPage(driver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testLoginSuccess() {
        login.with("user", "user");
        assertThat(login.successBoxPresent()).isTrue();
    }

    @Test
    void testLoginFailure() {
        login.with("bad-user", "bad-password");
        assertThat(login.successBoxPresent()).isFalse(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

}

1

我们在每个测试之前实例化页面对象。

2

由于页面类逻辑健壮,我们可以调用 successBoxPresent() 来实现一个负面测试。此方法在内部实现了对网页元素的显式等待,当成功框未显示时最终返回 false

创建一个领域特定语言(DSL)

在我们模拟 SUT 的过程中,我们可以通过使用页面类创建一个完整的领域特定语言(DSL)。在计算中,DSL 是针对特定领域的专门语言。当使用 POM 和 Selenium WebDriver 时,我们可以将 DSL 视为封装所有 SUT 操作和服务的方法在页面类提供的简单易读的 API 中。这些类封装了与 SUT 交互的所有调用 Selenium WebDriver API 的操作。

在前面章节展示的示例继续进行,示例 7-9 展示了一个基于 DSL 方法的登录页面基础页面类。这个基础类与 示例 7-6 非常相似,但在这种情况下,该类还封装了创建 WebDriver 实例所需的逻辑。

示例 7-9. 遵循 DSL 方法的基础类示例
public class ExtendedBasePage {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;
    WebDriverWait wait;
    int timeoutSec = 5; // wait timeout (5 seconds by default) 
    public ExtendedBasePage(String browser) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        driver = WebDriverManager.getInstance(browser).create(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSec));
    }

    public void setTimeoutSec(int timeoutSec) {
        this.timeoutSec = timeoutSec;
    }

    public void quit() { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        if (driver != null) {
            driver.quit();
        }
    }

    // Rest of common methods: quit(), visit(URL url), find(By element), etc. 
}

1

我们在基础构造函数中声明了一个 String 参数。这个字符串将是测试中指定的浏览器名称。

2

我们使用 WebDriverManager 解析所需的驱动程序并创建 WebDriver 实例。如 “通用管理器” 所述,WebDriverManager 允许通过调用 getInstance() 方法使用参数化管理器。在这种情况下,我们使用浏览器名称(例如 chrome, firefox, 等)来选择管理器。

3

我们还封装了一个用于结束会话和关闭浏览器的方法。

示例 7-10 展示了扩展此基类的页面类。如你所见,与 示例 7-7 的唯一区别是,此页面类在构造函数中使用了一个字符串参数(浏览器名称)。

示例 7-10. 遵循 DSL 方法的登录页面类
public class ExtendedLoginPage extends ExtendedBasePage {

    By usernameInput = By.id("username");
    By passwordInput = By.id("password");
    By submitButton = By.cssSelector("button");
    By successBox = By.id("success");

    public ExtendedLoginPage(String browser, int timeoutSec) {
        this(browser);
        setTimeoutSec(timeoutSec);
    }

    public ExtendedLoginPage(String browser) {
        super(browser);
        visit("https://bonigarcia.dev/selenium-webdriver-java/login-form.html");
    }

    public void with(String username, String password) {
        type(usernameInput, username);
        type(passwordInput, password);
        click(submitButton);
    }

    public boolean successBoxPresent() {
        return isDisplayed(successBox);
    }

}

最后,示例 7-11 展示了结果测试。请注意,此测试不包含任何对 Selenium WebDriver 或 WebDriverManager 的单个调用。页面类封装了与浏览器交互的所有低级细节,并公开了测试中使用的高级可读 API。

示例 7-11. 使用 POM 并遵循 DSL 方法的测试用例
class ExtendedLoginJupiterTest {

    ExtendedLoginPage login;

    @BeforeEach
    void setup() {
        login = new ExtendedLoginPage("chrome"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @AfterEach
    void teardown() {
        login.quit(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @Test
    void testLoginSuccess() {
        login.with("user", "user");
        assertThat(login.successBoxPresent()).isTrue();
    }

    @Test
    void testLoginFailure() {
        login.with("bad-user", "bad-password");
        assertThat(login.successBoxPresent()).isFalse();
    }

}

1

我们实例化页面对象,只需指定要使用的浏览器类型(在本例中为chrome)。

2

每次测试后,我们通常会终止浏览器会话,但这次是使用页面对象提供的一个方法。

页面工厂

Page Factory 是 Selenium WebDriver API 提供的几个支持类的名称,以便于实现页面对象类。其中最相关的支持类包括:

FindBy

在属性级别使用的注解,用于识别页面中的网页元素。

FindAll

允许组合不同@FindBy定位器的注解。

PageFactory

用于使用@FindBy(和@FindAll)初始化之前声明的所有网页元素的类。

CacheLookup

使用@FindBy注解来定位网页元素的一个缺点是,每次使用定位器时,驱动程序都会尝试在当前页面上找到它。这个特性在动态网页应用程序中非常有用。但是,在静态网页应用程序中缓存网页元素将是有帮助的。因此,@CacheLookup注解允许在定位到网页元素后对其进行缓存,提高生成测试的性能。

示例 7-12 展示了一个使用这些 Selenium WebDriver 支持类的页面类。您可以在存储库对象中找到使用这个页面类的结果测试。这个测试等同于示例 7-11,但使用FactoryLoginPage而不是ExtendedLoginPage与登录页面进行交互。

示例 7-12. 使用 Selenium WebDriver 提供的 Page Factory 的类
public class FactoryLoginPage extends ExtendedBasePage {

    @FindBy(id = "username")
    @CacheLookup
    WebElement usernameInput; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @FindBy(id = "password")
    @CacheLookup
    WebElement passwordInput;

    @FindBy(css = "button")
    @CacheLookup
    WebElement submitButton;

    @FindBy(id = "success")
    @CacheLookup
    WebElement successBox;

    public FactoryLoginPage(String browser, int timeoutSec) {
        this(browser);
        setTimeoutSec(timeoutSec);
    }

    public FactoryLoginPage(String browser) {
        super(browser);
        PageFactory.initElements(driver, this); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        visit("https://bonigarcia.dev/selenium-webdriver-java/login-form.html");
    }

    // Same logic to the page class without using the page factory 
}

1

我们使用WebElement类型声明页面上的网页元素,并用两个注解修饰:

@FindBy

以指定的定位器(例如idcss)来说明。

@CacheLookup

用于缓存网页元素位置结果(因为网页是静态的,在不同调用中其内容不会改变)。

2

我们调用initElements方法,使用WebDriver实例来定位网页元素。

警告

当使用 Selenium WebDriver 测试的网页是静态的时,只建议使用 Page Factory 方法。当使用动态网页时,此方法可能会导致不良影响,如过时的网页元素(即旧的或不再可用的元素)。

摘要与展望

本章详细介绍了在 Selenium WebDriver 测试中使用的页面对象模型(Page Object Model,POM)。POM 是一种设计模式,通过它我们将与网页交互的逻辑和测试代码分开。这样,页面类 包含了与网页定位和页面布局相关的逻辑,而测试类则确定如何执行和验证系统的被测单元(SUT)。POM 模式提升了基于 Selenium WebDriver 的测试套件的可维护性,因为页面类存储在一个单一的库中,模拟了 SUT。这个库稍后可以在不同的测试用例中使用。我们可以通过适当的定位和等待策略创建健壮的网页。

下一章介绍了使用的单元测试框架(JUnit、TestNG 和 Selenium-Jupiter)的具体方面,以改进 Selenium WebDriver 的整体测试流程。这些特性允许创建参数化测试(用于跨浏览器测试)、对测试进行分类(用于测试过滤)、对测试进行排序和重试,或者并行执行测试。

第八章:测试框架具体细节

在本书中提供的示例中,我推荐在使用不同单元测试框架(JUnit 4、JUnit 5(单独或与 Selenium-Jupiter 扩展一起使用)或 TestNG)时,将对 Selenium WebDriver API 的调用嵌入到使用注解@Test装饰的 Java 方法中。在执行常规测试时,使用一个或另一个测试框架的差异是微小的。然而,每个测试框架都有特定的特性用于不同的用例。本章总结了一些用于实现 Selenium WebDriver 测试的这些特性。像往常一样,你可以在本书示例存储库中找到本章的源代码。你可以使用这些示例来比较和选择最适合你特定需求的单元测试框架。

参数化测试

单元测试框架通常支持的一个广泛特性是创建参数化测试。这个特性使得可以使用不同的参数多次执行测试。虽然我们可以在 JUnit(4 和 5)和 TestNG 中都实现参数化测试,但每种实现之间存在显著差异。

JUnit 4

我们需要在 JUnit 4 中使用一个名为Parameterized测试运行器来实现参数化测试。在 JUnit 4 中,测试运行器是负责运行测试的 Java 类。我们使用 JUnit 4 注解@RunWith来装饰一个 Java 类来指定测试运行器。然后,我们需要使用 JUnit 4 注解@Parameters来装饰提供测试参数的方法。有两种方法可以将这些参数注入到测试类中:在测试类构造函数中或作为使用注解@Parameter装饰的类属性。示例 8-1 展示了一个测试用例,其中使用第二种技术注入测试参数。这个示例使用不同的凭据(用户名和密码)执行相同的登录测试。因此,网页提供的消息是不同的(登录成功或无效凭据)。

示例 8-1。使用 JUnit 4 进行参数化测试
@RunWith(Parameterized.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class ParameterizedJUnit4Test {

    WebDriver driver;

    @Parameter(0) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public String username;

    @Parameter(1)
    public String password;

    @Parameter(2)
    public String expectedText;

    @Before
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @After
    public void teardown() {
        driver.quit();
    }

    @Parameters(name = "{index}: username={0} password={1} expectedText={2}") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    public static Collection<Object[]> data() {
        return Arrays
                .asList(new Object[][] { { "user", "user", "Login successful" },
                        { "bad-user", "bad-passwd", "Invalid credentials" } }); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    @Test
    public void testParameterized() { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");

        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.cssSelector("button")).click();

        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText).contains(expectedText); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
    }

}

1

我们为这个 Java 类指定了Parameterized测试运行器。

2

我们将三个测试参数注入为类属性:用户名(索引0)、密码(索引1)和预期文本(索引2)。

3

我们在一个返回泛型参数集合(Collection<Object[]>)的方法中指定测试参数。

4

我们返回一个包含三个String集合的集合,用作测试参数。每个条目的值将使用之前声明的三个参数(用户名、密码和预期文本)注入。

5

|

|

在参数化测试中,JUnit 4 和 TestNG 之间的一个显著差异是,在 TestNG 中,参数(例如本示例中的用户名、密码和预期测试)作为测试方法参数注入。

|

|

2

|

示例 8-2. 使用 TestNG 进行参数化测试
public class ParameterizedNGTest {

    WebDriver driver;

    @BeforeMethod
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterMethod
    public void teardown() {
        driver.quit();
    }

    @DataProvider(name = "loginData") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    public static Object[][] data() {
        return new Object[][] { { "user", "user", "Login successful" },
                { "bad-user", "bad-passwd", "Invalid credentials" } };
    }

    @Test(dataProvider = "loginData") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public void testParameterized(String username, String password,
            String expectedText) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        // Same test logic than the example before
    }

}

1

|

|

我们指定此测试将使用之前称为 loginData 的数据提供器。

6

我们可以使用注解 @DataProvider 来装饰提供 TestNG 参数化测试参数的方法。正如您在 示例 8-2 中所看到的,此方法返回一个通用 Java 对象的双数组。注解 @Data​Pro⁠vider 应该提供一个名称作为属性。稍后此名称将在 @Test 方法中用于指定数据提供器。最后,参数被注入到测试方法中。

我们断言预期数据(根据提供的凭据不同而异)在页面正文中是可用的。

|

  • |

  • |

|

Java 枚举的常量
实现 ArgumentsProvider 接口的类

TestNG

@ValueSource
字面值数组
@ParameterizedTest
@ValueSource(strings = { "Hi", "Bye" })
void test(String argument) {
    log.debug("arg: {}", argument);
}

|

arg: Hi
arg: Bye

|

|

@EnumSource
Jupiter(JUnit 5 的编程和扩展模型)为创建参数化测试提供了强大的机制。简言之,要在 JUnit 5 中实现这些测试,我们需要两个元素:
@ParameterizedTest
@EnumSource(TimeUnit.class)
void test(TimeUnit argument) {
    log.debug("{}", argument);
}

| 提供一个值流的类的静态方法 |

NANOSECONDS
MICROSECONDS
MILLISECONDS
SECONDS
MINUTES
HOURS
DAYS

在测试逻辑中(将根据每个数据输入执行两次),我们尝试使用作为参数提供的用户名和密码登录练习站点。

| 单个 null 参数 |

@MethodSource
注解 @ParameterizedTest(而不是通常的 @Test 注解),用于装饰注入参数的测试方法。
static IntStream intProvider() {
    return IntStream.of(0, 1);
}

@ParameterizedTest
@MethodSource("intProvider")
void test(int argument) {
    log.debug("arg: {}", argument);
    assertNotNull(argument);
}

|

arg: 0
arg: 1

|

|

@CsvSource
注解内的逗号分隔值(CSV)
@ParameterizedTest
@CsvSource({ "hello, 1", "world, 2"})
void test(String first, int second) {
    log.debug("{} and {} ", first,
            second);
}

表 8-1. JUnit 5 中的参数提供器

hello and 1
world and 2

| 单个空参数 |

|

@CsvFileSource
@ParameterizedTest
@CsvFileSource(resources =
            "/input.csv")
void test(String first, int second) {
    log.debug("{} and {} ", first,
            second);
}

|

hi and 3
there and 4

警告

3

@ArgumentsSource
@ParameterizedTest
@ArgumentsSource(MyArgs.class)
void test(String first, int second) {
    log.debug("{} and {} ", first,
            second);
}

public class MyArgs implements
            ArgumentsProvider {
  @Override
  public Stream<? extends
        Arguments> provideArguments(
        ExtensionContext context) {
     return Stream.of(Arguments.
        of("hi", 5), Arguments.
        of("there", 6));
  }
}

|

hi and 5
there and 6

我们创建一个作为数据提供器的方法。

参数提供器,用于参数化测试的数据源。表 8-1 提供了这些参数提供器的综合概述。

@NullSource
一个 null 加一个空参数
@ParameterizedTest
@ValueSource(strings = { "one",
            "two" })
@NullSource
void test(String argument) {
    log.debug("arg: {}", argument);
}

| 位于类路径中的文件中以 CSV 格式的值 |

arg: one
arg: two
arg: null

JUnit 4 的最显著限制之一是每个 Java 类只能使用一个测试运行器。换句话说,JUnit 4 中的测试运行器不可组合。为了克服这一限制(以及其他限制),JUnit 团队于 2017 年发布了 JUnit 5。

JUnit 5

@EmptySource
@ParameterizedTest
@ValueSource(strings = { "three",
            "four" })
@EmptySource
void test(String argument) {
    log.debug("arg: {}", argument);
}

|

arg: three
arg: four
arg:

| --- | --- | --- | --- |

|

@NullAndEmptySource
注解 描述 示例 示例输出
@ParameterizedTest
@ValueSource(strings = { "five",
            "six" })
@NullAndEmptySource
void test(String arg) {
    log.debug("arg: {}", arg);
}

|

arg: five
arg: six
arg: null
arg:

|

Example 8-3 展示了与前面示例相同的参数化测试的 Jupiter 版本。我们可以使用不同的参数提供者来实现此参数化测试。在这种情况下,我们使用@MethodSource返回参数流。适合这种测试的另一种选择是使用@CsvSource将输入数据和预期结果嵌入为 CSV 格式。

Example 8-3. 使用 JUnit 5 的参数化测试
class ParameterizedJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    static Stream<Arguments> loginData() { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        return Stream.of(Arguments.of("user", "user", "Login successful"),
                Arguments.of("bad-user", "bad-passwd", "Invalid credentials"));
    }

    @ParameterizedTest ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    @MethodSource("loginData") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    void testParameterized(String username, String password,
            String expectedText) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        // Same test logic than the examples before
    }

}

1

我们定义一个静态方法作为@MethodSource中的参数提供者。

2

而不是常规的@Test,我们实现了一个参数化测试。

3

参数提供者与loginData方法提供的数据相关联。

4

参数被注入到测试方法中。

Selenium-Jupiter

当使用 Selenium-Jupiter 时,你可以使用相同的方法来实现 JUnit 5 的参数化测试。唯一的区别在于,你通过 Selenium-Jupiter 委托创建和销毁WebDriver对象。Example 8-4 演示了如何实现前面章节中解释的相同测试(即参数化登录),但使用 Selenium-Jupiter。

Example 8-4. 使用 JUnit 5 和 Selenium-Jupiter 的参数化测试
@ExtendWith(SeleniumJupiter.class)
class ParameterizedSelJupTest {

    static Stream<Arguments> loginData() {
        return Stream.of(Arguments.of("user", "user", "Login successful"),
                Arguments.of("bad-user", "bad-passwd", "Invalid credentials"));
    }

    @ParameterizedTest
    @MethodSource("loginData")
    void testParameterized(String username, String password,
            String expectedText, ChromeDriver driver) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        // Same test logic than the examples before
    }

}

1

当在 Jupiter 测试中使用不同的参数解析器时,按照惯例,我们必须首先声明由@ParameterizedTest注入的参数,然后是由扩展(在本例中为 Selenium-Jupiter,用于WebDriver对象)注入的参数。

跨浏览器测试

跨浏览器测试是一种功能测试,通过使用不同类型的 Web 浏览器验证 Web 应用程序是否按预期工作。通过使用浏览器类型(例如 Chrome、Firefox、Edge 等)作为测试参数,可以实现参数化测试的可能方法。接下来的章节描述了如何使用单元测试框架的能力来进行适用于跨浏览器测试的参数化测试。在这些示例中,我们将使用本地浏览器(Chrome、Firefox 和 Edge)。进行跨浏览器测试的另一种方法是使用远程浏览器(从 Selenium 服务器、云提供商或 Docker 中),详见第六章。

JUnit 4

示例 8-5 展示了使用 JUnit 4 实现的跨浏览器测试。 我们使用 WebDriverManager 来简化参数化。 正如“通用管理器”中所解释的那样,WebDriverManager 可以根据参数的值使用一个或另一个管理器。 此参数可以是 WebDriver 类、枚举或浏览器名称。 在以下示例中我们使用了后者(虽然您可以在示例库中找到替代方法)。

示例 8-5. 使用 JUnit 4 进行跨浏览器测试
@RunWith(Parameterized.class)
public class CrossBrowserJUnit4Test {

    WebDriver driver;

    @Parameter(0)
    public String browserName;

    @Parameters(name = "{index}: browser={0}")
    public static Collection<Object[]> data() {
        return Arrays.asList(
                new Object[][] { { "chrome" }, { "edge" }, { "firefox" } }); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @Before
    public void setup() {
        driver = WebDriverManager.getInstance(browserName).create(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @After
    public void teardown() {
        driver.quit();
    }

    @Test
    public void testCrossBrowser() { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们使用它们的名称指定了三个浏览器。

2

我们使用 WebDriverManager 通用管理器,使用这些浏览器名称作为参数。 选择一种或另一种浏览器的替代方法是使用通用管理器而不带参数(即使用方法 .getInstance(),如“通用管理器”中所述),然后使用 Java 系统属性 wdm.defaultBrowser 来为测试(或测试套件)参数化(例如,在使用 Maven 或 Gradle 运行时)。

3

此测试执行三次,每次使用不同的浏览器(Chrome、Edge 和 Firefox)。

TestNG

示例 8-6 展示了相同的跨浏览器测试,这次使用 TestNG。 在这种情况下,测试参数(浏览器名称)被注入到测试方法中。

示例 8-6. 使用 TestNG 进行跨浏览器测试
public class CrossBrowserNGTest {

    WebDriver driver;

    @DataProvider(name = "browsers")
    public static Object[][] data() {
        return new Object[][] { { "chrome" }, { "edge" }, { "firefox" } };
    }

    @AfterMethod
    public void teardown() {
        driver.quit();
    }

    @Test(dataProvider = "browsers")
    public void testCrossBrowser(String browserName) {
        driver = WebDriverManager.getInstance(browserName).create(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们需要在测试逻辑中创建 WebDriver 实例,因为在使用 TestNG 时测试参数被注入到测试方法中。

JUnit 5

示例 8-7 展示了遵循 Jupiter 模型的同一跨浏览器测试。 再次使用 WebDriverManager 创建 WebDriver 实例,使用浏览器名称作为参数。 由于这些参数是字符串,我们使用 @ValueSource 作为参数提供程序。

示例 8-7. 使用 JUnit 5 进行跨浏览器测试
class CrossBrowserJupiterTest {

    WebDriver driver;

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @ParameterizedTest
    @ValueSource(strings = { "chrome", "edge", "firefox" })
    void testCrossBrowser(String browserName) {
        driver = WebDriverManager.getInstance(browserName).create();  ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

在 Jupiter 中,参数化测试中的参数被注入到测试方法中。 因此,我们需要在测试逻辑中创建驱动程序实例。

Selenium-Jupiter

Selenium-Jupiter 提供了一个补充功能,用于创建跨浏览器测试,称为测试模板。 测试模板是 Jupiter 支持的一种特殊的参数化测试,在这种测试中,扩展程序收集参数。 Selenium-Jupiter 使用这一特性以一种名为浏览器场景的自定义 JSON 符号的全面方式来指定不同的浏览器方面(如类型、版本、参数和能力)。 您可以在Selenium-Jupiter 文档中找到更多关于此功能的详细信息。

示例 8-8 展示了一个样本浏览器场景。这个 JSON 存储在名为browsers.json的文件中,这是模板测试使用的默认名称。示例 8-9 展示了使用这个浏览器场景的模板测试。

示例 8-8. Selenium-Jupiter 中用于测试模板的浏览器场景
{
   "browsers": 
      [
         {
            "type": "chrome" ![1
         }
      ],
      
         {
            "type": "edge", ![2
             "arguments" : [
                "--headless"
             ]
         }
      ],
      
         {
            "type": "firefox-in-docker", ![3
            "version": "93"
         }
      ]
   ]
}

1

这个浏览器场景包含三个浏览器。第一个是本地的 Chrome 浏览器。

2

第二个浏览器是本地的无头 Edge 浏览器。

3

第三个浏览器是在 Docker 容器中执行的 Firefox 93 版本。

示例 8-9. 使用 Selenium-Jupiter 在 JUnit 5 中进行跨浏览器测试模板
@EnabledIfDockerAvailable ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@ExtendWith(SeleniumJupiter.class)
class CrossBrowserJsonSelJupTest {

    @TestTemplate ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    void testCrossBrowser(WebDriver driver) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

当 Docker 不可用时(因为场景中的一个浏览器使用 Docker),我们使用 Selenium-Jupiter 注解来跳过这个测试。

2

我们需要使用@TestTemplate修饰测试方法,而不是通常的@Test注解。

3

我们使用通用的WebDriver来注入驱动实例。另外,RemoteWebDriver在测试模板中也是有效的。

测试分类和过滤

基于 Selenium WebDriver 构建测试套件时(特别是测试数量很多时),常见的需求是只执行一组测试。有多种方法可以实现单个或组测试的执行。在使用 IDE 运行测试时,可以选择要执行的具体测试。在使用命令行时,可以使用其他机制来选择这些测试。

乍一看,我们可以使用构建工具提供的过滤机制。例如,Maven 和 Gradle 允许基于测试类和方法名包含或排除特定测试。这些命令的基本语法在附录 C 中介绍。表 8-2 展示了使用这些命令的几个常见示例。请注意,通配符*在这些示例中用于匹配测试类名中的任意字符。

表 8-2. Maven 和 Gradle 命令示例,包括和排除测试

描述 Maven Gradle
运行以 Hello 开头的测试
mvn -B test
    -Dtest=Hello*

|

gradle test
    --tests Hello*

|

运行包含 BasicTimeout 的测试
mvn test
    -Dtest=*Basic*,*Timeout*

|

gradle test
    --tests *Basic* --tests *Timeout*

|

运行除了以 Firefox 开头的测试
mvn test
    -Dtest=!*Firefox*

|

gradle test
    -PexcludeTests=**/*Firefox*

|

运行除了以 Docker 开头或包含 Remote 的测试
mvn test
    -Dtest=!Docker*,!*Remote*

|

gradle test
    -PexcludeTests=**/Docker*,**/*Remote*

|

除了构建工具,我们还可以利用单元测试框架提供的内置功能,对测试进行分类(也称为分组或标记)并基于这些分类进行过滤。下面的子节将详细解释如何操作。

JUnit 4

JUnit 4 提供了@Category注解来对测试进行分组。我们需要在此注解中指定一个或多个 Java 类作为属性。然后,我们可以使用这些类来选择和执行属于一个或多个类别的测试。示例 8-10 展示了使用此功能的基本类。

示例 8-10. 使用类别和 JUnit 4 进行测试
public class CategoriesJUnit4Test {

    WebDriver driver;

    @Before
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @After
    public void teardown() {
        driver.quit();
    }

    @Test
    @Category(WebForm.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    public void testCategoriesWebForm() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
        assertThat(driver.getCurrentUrl()).contains("web-form");
    }

    @Test
    @Category(HomePage.class) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public void tesCategoriestHomePage() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getCurrentUrl()).doesNotContain("web-form");
    }

}

1

WebForm是示例库中的一个空接口。

2

HomePage是示例库中的另一个空接口。

然后我们可以根据其组执行测试。例如,以下命令展示了运行属于HomePage类别的测试的 Maven 和 Gradle 命令。

mvn test -Dgroups=
    io.github.bonigarcia.webdriver.junit4.ch08.categories.HomePage
gradle test -Pgroups=
    io.github.bonigarcia.webdriver.junit4.ch08.categories.HomePage

我们可以将此过滤与 Maven 和 Gradle 支持结合使用,根据类名选择测试。例如,以下命令执行属于HomePage类别的测试,但仅在测试类CategoriesJUnit4Test中。

mvn test -Dtest=CategoriesJUnit4Test -DexcludedGroups=
    io.github.bonigarcia.webdriver.junit4.ch08.categories.HomePage
gradle test --tests CategoriesJUnit4Test -PexcludedGroups=
    io.github.bonigarcia.webdriver.junit4.ch08.categories.HomePage

TestNG

TestNG 也允许对测试进行分组。示例 8-11 演示了此功能的基本用法。总之,@Test注解允许为这些组指定字符串标签。

示例 8-11. 使用组和 TestNG 进行测试
public class CategoriesNGTest {

    WebDriver driver;

    @BeforeMethod(alwaysRun = true) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterMethod(alwaysRun = true)
    public void teardown() {
        driver.quit();
    }

    @Test(groups = { "WebForm" }) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public void testCategoriesWebForm() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
        assertThat(driver.getCurrentUrl()).contains("web-form");
    }

    @Test(groups = { "HomePage" }) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    public void tesCategoriestHomePage() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getCurrentUrl()).doesNotContain("web-form");
    }

}

1

我们将属性alwaysRun设置为true,以指示在测试执行期间不过滤设置和拆卸方法。

2

我们将组名WebForm分配给该类的第一个测试。

3

我们将组名HomePage设置为第二个测试。

然后我们可以根据这些分类使用命令行过滤测试执行。以下片段首先展示了如何执行属于HomePage组的测试。第二个示例说明了如何将这种分组与基于类名的 Maven 和 Gradle 过滤机制结合使用。

mvn test -Dgroups=HomePage
gradle test -Pgroups=HomePage

mvn test -Dtest=CategoriesNGTest -DexcludedGroups=HomePage
gradle test --tests CategoriesNGTest -PexcludedGroups=HomePage

JUnit 5

Jupiter 编程模型提供了一种基于自定义标签(称为标签)对测试进行分组的方式。我们使用@Tag注解来实现这一目的。示例 8-12 说明了此功能。

示例 8-12. 使用标签和 JUnit 5 进行测试
class CategoriesJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    @Tag("WebForm") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    void testCategoriesWebForm() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
        assertThat(driver.getCurrentUrl()).contains("web-form");
    }

    @Test
    @Tag("HomePage") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    void testCategoriesHomePage() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getCurrentUrl()).doesNotContain("web-form");
    }

}

1

我们使用标签WebForm标记第一个测试。

2

我们使用HomePage标签对第二个测试进行分类。

我们可以使用这些标签在命令行执行测试时包含或排除测试。以下命令展示了 Maven 和 Gradle 的几个示例:

mvn test -Dgroups=HomePage
gradle test -Pgroups=HomePage

mvn test -Dtest=CategoriesNGTest -DexcludedGroups=HomePage
gradle test --tests CategoriesNGTest -PexcludedGroups=HomePage

测试排序

在本书中使用的单元测试框架中,测试执行顺序事先是未知的。尽管如此,仍然有机制可以选择给定的执行顺序。在 Selenium WebDriver 领域中使用此功能的一个可能用例是通过不同的测试以给定顺序(即使用相同的 WebDriver 实例)重用同一浏览器会话与 SUT 进行交互。以下示例演示了对 JUnit 4、TestNG、JUnit 5 和 JUnit 5 加 Selenium-Jupiter 使用此功能的情况。

JUnit 4

JUnit 4 提供了注解 @FixMethodOrder 来确定测试执行顺序。此注解接受一个名为 MethodSorters 的枚举,由以下值组成:

NAME_ASCENDING

按方法名按字典顺序对测试方法进行排序

JVM

将测试方法按 JVM 返回的顺序排序。

DEFAULT

将测试方法以确定性但不可预测的顺序排序

Example 8-13 显示了一个完整的测试用例,其中测试使用方法名执行。

示例 8-13. 使用 JUnit 4 对测试进行排序
@FixMethodOrder(MethodSorters.NAME_ASCENDING) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class OrderJUnit4Test {

    static WebDriver driver;

    @BeforeClass ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public static void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterClass ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    public static void teardown() {
        driver.quit();
    }

    @Test ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    public void testA() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/navigation1.html");
        assertBodyContains("Lorem ipsum");
    }

    @Test
    public void testB() {
        driver.findElement(By.linkText("2")).click();
        assertBodyContains("Ut enim");
    }

    @Test
    public void testC() {
        driver.findElement(By.linkText("3")).click();
        assertBodyContains("Excepteur sint");
    }

    void assertBodyContains(String text) {
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText).contains(text);
    }

}

1

我们在类级别使用注解 @FixMethodOrder 来确定此类中可用测试的顺序。

2

我们在所有测试之前创建驱动程序实例(因为我们希望在所有测试中使用 WebDriver 会话)。

3

我们在所有测试完成后退出驱动程序实例。因此,在该类的最后一个测试之后结束会话。

4

由于测试名称按字典顺序排序(testAtestBtestC),测试执行将遵循此顺序。

TestNG

在 TestNG 中按照每个测试使用增量优先级的简单方式对测试进行排序。Example 8-14 通过在 @Test 注解中使用 priority 属性来演示此功能。

示例 8-14. 使用 TestNG 对测试进行排序
public class OrderNGTest {

    static WebDriver driver;

    @BeforeClass
    public static void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterClass
    public static void teardown() {
        driver.quit();
    }

    @Test(priority = 1)
    public void testA() {
        // Test logic
    }

    @Test(priority = 2)
    public void testB() {
        // Test logic
    }

    @Test(priority = 3)
    public void testC() {
        // Test logic
    }

}

JUnit 5

Jupiter 提供了注解 @TestMethodOrder 来对测试进行排序。此注解可以使用以下排序实现进行配置:

DisplayName

根据显示名称按字母数字顺序排序测试方法。

MethodName

根据名称按字母数字顺序对测试方法进行排序。

OrderAnnotation

基于使用 @Order 注解指定的数字值对测试方法进行排序。Example 8-15 展示了使用此方法的测试。

Random

以伪随机方式对测试方法进行排序。

示例 8-15. 使用 JUnit 5 对测试进行排序
@TestMethodOrder(OrderAnnotation.class)
class OrderJupiterTest {

    static WebDriver driver;

    @BeforeAll
    static void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterAll
    static void teardown() {
        driver.quit();
    }

    @Test
    @Order(1)
    void testA() {
        // Test logic
    }

    @Test
    @Order(2)
    void testB() {
        // Test logic
    }

    @Test
    @Order(3)
    void testC() {
        // Test logic
    }

}

Selenium-Jupiter

和往常一样,使用 Selenium-Jupiter 的测试也使用 Jupiter 编程模型;因此,这些特性(如测试排序)对于 Selenium-Jupiter 测试同样有效。示例 8-16 展示了之前相同的测试,使用 Selenium-Jupiter 进行驱动程序实例化。默认情况下,驱动程序对象在每次测试之前创建,并在每次测试后终止。Selenium-Jupiter 提供了@SingleSession注解来改变这种行为,创建所有测试之前的驱动程序实例,并在所有测试之后关闭会话。

示例 8-16. 使用 JUnit 5 和 Selenium-Jupiter 排序测试
@ExtendWith(SeleniumJupiter.class)
@TestMethodOrder(OrderAnnotation.class)
@SingleSession
class OrderSelJupTest {

    WebDriver driver;

    OrderSelJupTest(ChromeDriver driver) {
        this.driver = driver;
    }

    @Test
    @Order(1)
    void testA() {
        // Test logic
    }

    @Test
    @Order(2)
    void testB() {
        // Test logic
    }

    @Test
    @Order(3)
    void testC() {
        // Test logic
    }

}

故障分析

故障分析(也称为故障排除)是收集和分析数据以发现故障原因的过程。对于 Selenium WebDriver 测试来说,这个过程可能具有挑战性,因为整个系统被测试,导致测试失败的根本原因可能有多个。例如,端到端测试失败的原因可能是客户端(前端)逻辑、服务器端(后端)逻辑,甚至是与其他组件的集成(例如数据库或外部服务)。

我们可以使用不同的技术来帮助开发人员和测试人员进行故障分析过程。这样做的典型方式是检测测试失败,并在终止驱动程序会话之前收集一些数据以发现原因。以下资产可以帮助此过程:

屏幕截图

测试失败后,Web 应用程序 UI 的图片可能有助于确定失败原因。“屏幕截图”解释了如何使用 Selenium WebDriver API 进行截图。

浏览器日志

当出现错误时,JavaScript 控制台可以是另一个潜在的信息来源。“日志收集”解释了如何进行这种日志收集。

会话记录

在使用 Docker 容器中的浏览器时,我们可以轻松记录浏览器会话。“Docker 容器中的浏览器”解释了如何使用 WebDriverManager 和 Selenium-Jupiter 实现这一点。

以下各小节提供了有关如何在测试失败时制作浏览器截图的基本示例。为此,我们需要依赖单元测试的特定功能来检测失败的测试。

JUnit 4

JUnit 允许通过使用规则来调整测试的默认行为。测试类通过使用@Rule注解修饰类属性来定义规则。表 8-3 总结了 JUnit 4 提供的默认规则。

表 8-3. JUnit 4 中的规则

规则 描述 示例

|

ErrorCollector
允许在发生异常时继续执行测试(同时收集这些异常)
@Rule
public ErrorCollector collector =
        new ErrorCollector();

@Ignore
@Test
public void test() {
    collector.checkThat("a", equalTo("b"));
    collector.checkThat(1, equalTo(2));
}

|

|

ExternalResource
提供一个基类,在每次测试之前设置和拆除外部资源
private Resource resource;

@Rule
public ExternalResource rule =
        new ExternalResource() {
    @Override
    protected void before() throws Throwable {
        resource = new Resource();
        resource.open();
    }

    @Override
    protected void after() {
        resource.close();
    }
};

|

|

TestName
使当前测试方法可用于测试名称
@Rule
public TestName name = new TestName();

@Test
public void testA() {
    assertThat("testA")
        .isEqualTo(name.getMethodName());
}

|

|

TemporaryFolder
允许创建临时文件和文件夹
@Rule
public TemporaryFolder folder =
        new TemporaryFolder();

@Test
public void test() throws IOException {
    File file = folder.newFile("myfile.txt");
}

|

|

Timeout
在类中对所有测试方法应用超时
@Rule
public Timeout timeout =
        new Timeout(10, SECONDS);

@Test
public void test() {
    while (true);
}

|

|

TestWatcher
允许捕获测试的多个执行阶段:startingsucceededfailedskippedfinished
@Rule
public TestWatcher watcher =
        new TestWatcher() {
    @Override
    protected void succeeded(Description d) {
        log.debug("Test succeeded: {}",
            d.getMethodName());
    }

    @Override
    protected void failed(Throwable e,
            Description d) {
        log.debug("Test failed: {}",
            d.getMethodName());
    }
};

|

我们可以使用 TestWatcher 规则来收集 JUnit 4 失败分析的数据。Example 8-17 展示了一个在测试失败时捕获截图的测试。Example 8-18 包含了此规则的实现。正如前面提到的,我们制作浏览器截图的逻辑在 Example 8-19 中。

Example 8-17. 使用 JUnit 4 分析失败的测试
public class FailureJUnit4Test {

    static WebDriver driver;

    @Rule
    public TestRule testWatcher = new FailureWatcher(driver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeClass
    public static void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterClass
    public static void teardown() {
        driver.quit();
    }

    @Test
    public void testFailure() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        fail("Forced error"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

}

1

我们在类级别定义规则,并将驱动程序实例作为参数传递。

2

我们强制此测试失败,以便使用规则来截取浏览器的截图。

Example 8-18. 使用 JUnit 4 分析失败的测试
public class FailureWatcher extends TestWatcher {

    FailureManager failureManager;

    public FailureWatcher(WebDriver driver) {
        failureManager = new FailureManager(driver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @Override
    public void failed(Throwable throwable, Description description) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        failureManager.takePngScreenshot(description.getDisplayName());
    }

}

1

我们将失败分析的逻辑封装在一个单独的类中。

2

我们重写了当测试失败时触发的方法。在这种情况下,我们简单地使用失败管理器实例来截取一张截图。

Example 8-19. 使用 JUnit 4 分析失败的测试
public class FailureManager {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;

    public FailureManager(WebDriver driver) {
        this.driver = driver;
    }

    public void takePngScreenshot(String filename) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        TakesScreenshot ts = (TakesScreenshot) driver;
        File screenshot = ts.getScreenshotAs(OutputType.FILE);
        Path destination = Paths.get(filename + ".png");

        try {
            Files.move(screenshot.toPath(), destination);
        } catch (IOException e) {
            log.error("Exception moving screenshot from {} to {}", screenshot,
                    destination, e);
        }
    }

}

1

我们将截图保存为一个 PNG 文件,并以作为参数传递的文件名命名。

TestNG

TestNG 提供了几个默认的 监听器。这些监听器是捕获测试生命周期中不同事件的类。例如,ITestResult 监听器允许您监控测试的状态和结果。如 Example 8-20 所示,我们可以轻松地在 Selenium WebDriver 测试中使用此监听器来实现失败分析。

Example 8-20. 使用 TestNG 分析失败的测试
public class FailureNGTest {

    WebDriver driver;

    @BeforeMethod
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterMethod
    public void teardown(ITestResult result) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        if (result.getStatus() == ITestResult.FAILURE) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
            FailureManager failureManager = new FailureManager(driver); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
            failureManager.takePngScreenshot(result.getName());
        }

        driver.quit();
    }

    @Test
    public void testFailure() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        fail("Forced error");
    }

}

1

我们在测试结束时的方法中声明了一个 ITestResult 参数。

2

我们读取测试的状态。

3

在失败的情况下,我们创建一个失败管理器的实例(我们使用与 Example 8-19 描述的相同逻辑)以创建一个截图。

JUnit 5

在 JUnit 5 中,Jupiter 扩展模型取代并改进了基于规则的 JUnit 4 测试生命周期管理。正如 Chapter 2 中介绍的,Jupiter 提供的扩展模型允许在 Jupiter 编程模型的基础上添加新功能。这样,Jupiter 扩展是实现一个或多个 扩展点 的 Java 类,这些接口允许在 Jupiter 编程模型中执行不同类型的操作。Table 8-4 总结了 Jupiter 提供的扩展点。

Table 8-4. Jupiter 扩展点

类别 描述 扩展点(s)
测试生命周期回调 在测试生命周期中包含自定义逻辑
BeforeAllCallback
BeforeEachCallback
BeforeTestExecutionCallback
AfterTestExecutionCallback
AfterEachCallback
AfterAllCallback

|

参数解析 在测试方法或构造函数中注入参数
ParameterResolver

|

测试模板 使用@TestTemplate实现测试
TestTemplateInvocationContextProvider

|

条件测试执行 根据自定义条件启用或禁用测试
ExecutionCondition

|

异常处理 处理测试及其生命周期中的异常
TestExecutionExceptionHandler
LifecycleMethodExecutionExceptionHandler

|

测试实例 创建和处理测试类实例
TestInstanceFactory
TestInstancePostProcessor
TestInstancePreDestroyCallback

|

拦截调用 拦截对测试代码的调用(并决定这些调用是否继续)
InvocationInterceptor

|

实施失败分析的一个便捷扩展点是AfterTestExecutionCallback,因为它允许在单个测试执行后立即包含自定义逻辑。示例 8-21 提供了使用自定义注解的 Jupiter 测试(参见示例 8-22)来实现此扩展点。

示例 8-21. 使用 JUnit 5 分析失败的测试
class FailureJupiterTest {

    static WebDriver driver;

    @RegisterExtension
    FailureWatcher failureWatcher = new FailureWatcher(driver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeAll
    static void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterAll
    static void teardown() {
        driver.quit();
    }

    @Test
    void testFailure() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        fail("Forced error"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

}

1

我们使用此类中提供的FailureWatcher扩展来进行测试。我们将驱动程序实例作为参数传递。

2

我们强制失败以使扩展获取浏览器截图。

示例 8-22. 使用 JUnit 5 分析失败的测试
public class FailureWatcher implements AfterTestExecutionCallback { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    FailureManager failureManager;

    public FailureWatcher(WebDriver driver) {
        failureManager = new FailureManager(driver);
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        if (context.getExecutionException().isPresent()) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
            failureManager.takePngScreenshot(context.getDisplayName()); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        }
    }

}

1

此扩展实现了一个单一的扩展点:AfterTestExecution​Call⁠back

2

此扩展点必须重写此方法,该方法在每个测试后立即执行。

3

我们检查执行异常是否存在。

4

如果是这样,我们使用WebDriver实例来截图。

Selenium-Jupiter

Selenium-Jupiter 是一个 Jupiter 扩展,除其他功能外,还允许轻松进行浏览器截图。示例 8-23 展示了这一特性。

示例 8-23. 使用 Selenium-Jupiter 分析 JUnit 5 的失败测试
class FailureSelJupTest {

    @RegisterExtension
    static SeleniumJupiter seleniumJupiter = new SeleniumJupiter();

    @BeforeAll
    static void setup() {
        seleniumJupiter.getConfig().enableScreenshotWhenFailure(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    }

    @Test
    void testFailure(ChromeDriver driver) {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        fail("Forced error");
    }

}

1

Selenium-Jupiter 通过使用此配置能力,在测试失败时获取浏览器截图。

重试测试

如第七章中所述,端到端测试中存在测试不稳定性(即可靠性不足)是一个众所周知的问题。作为测试人员,有时我们需要识别不稳定的测试(即在相同条件下通过或失败的测试),为此,我们重试给定的测试以检查其结果是否一致。因此,我们可能需要一种机制,在测试失败时重试测试。本节解释了如何使用不同的单元测试框架执行此过程。

JUnit 4

我们需要使用自定义的 JUnit 4 规则来重试失败的测试。示例 8-24 展示了使用这种规则的测试示例,而示例 8-25 包含了该规则的源代码。

示例 8-24. 使用 JUnit 4 重试测试
public class RandomCalculatorJUnit4Test {

    static WebDriver driver;

    @Rule
    public RetryRule retryRule = new RetryRule(5); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeClass
    public static void setup() {
        driver = WebDriverManager.chromedriver().create(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @AfterClass
    public static void teardown() {
        driver.quit();
    }

    @Test
    public void testRandomCalculator() {
        driver.get(
         "https://bonigarcia.dev/selenium-webdriver-java/random-calculator.html"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        // 1 + 3
        driver.findElement(By.xpath("//span[text()='1']")).click(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        driver.findElement(By.xpath("//span[text()='+']")).click();
        driver.findElement(By.xpath("//span[text()='3']")).click();
        driver.findElement(By.xpath("//span[text()='=']")).click();

        // ... should be 4
        String result = driver.findElement(By.className("screen")).getText();
        assertThat(result).isEqualTo("4"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    }

}

1

我们将重试规则声明为测试属性。

2

我们所有重复使用相同的浏览器。

3

我们打开一个名为随机计算器的实践网页。该页面被设计为在一定百分比的时间内(默认为 50%)生成错误结果。然后,计算器在配置的次数后(默认为五次)正常工作。

4

我们使用计算器 GUI 进行重要的算术操作。

5

我们验证结果。前五次尝试有 50% 的概率得到错误结果。

示例 8-25. 使用 JUnit 4 规则重试失败的测试
public class RetryRule implements TestRule { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    static final Logger log = getLogger(lookup().lookupClass());

    int maxRetries;

    public RetryRule(int maxRetries) {
        this.maxRetries = maxRetries; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @Override
    public Statement apply(Statement base, Description description) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Throwable throwable = null;
                for (int i = 0; i < maxRetries; i++) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
                    try {
                        base.evaluate();
                        return;
                    } catch (Throwable t) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
                        throwable = t;
                        log.debug("{}: run {} failed",
                                description.getDisplayName(), i + 1);
                    }
                }
                log.debug("{}: giving up after {} failures",
                        description.getDisplayName(), maxRetries);
                throw throwable; ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
            }
        };
    }
}

1

我们为 JUnit 4 规则实现了通用接口,即TestRule

2

此规则在其构造函数中接受一个整数值,用于确定最大重试次数。

3

我们需要重写apply方法,该方法允许操作测试的生命周期。

4

我们在循环中重复执行测试,重复次数最多等于重试次数。

5

在测试执行期间发生错误时,我们获取异常对象并重复执行测试。

6

如果达到此行,表示测试已重试最大次数。

TestNG

TestNG 提供了一个自定义功能来实现测试重试。如示例 8-26 所示,我们使用@Test注解的retryAnalyzer属性来启用此功能。示例 8-27 展示了重试分析器的实现。

示例 8-26. 使用 TestNG 重试测试
@Test(retryAnalyzer = RetryAnalyzer.class)
public void testRandomCalculator() {
    // Same logic than the example before
}
示例 8-27. TestNG 的测试分析器
public class RetryAnalyzer implements IRetryAnalyzer { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    static final int MAX_RETRIES = 5; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    int retryCount = 0;

    @Override
    public boolean retry(ITestResult result) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        if (retryCount <= MAX_RETRIES) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
            retryCount++;
            return true;
        }
        return false;
    }
}

1

我们需要实现一个称为IRetryAnalyzer的 TestNG 监听器以实现重试分析器。

2

我们无法为此类参数化;因此,我们在类内声明最大重试次数(在本例中作为常量)。

3

我们需要重写方法retry。该方法返回一个布尔值,用于确定在失败时是否重试测试。

4

确定此值的逻辑是一个累加器,检查是否达到重试阈值。

JUnit 5

我们需要使用先前解释的扩展模型(参见表 8-4)来重试失败的测试。我们可以利用现有的开源 Jupiter 扩展而不是重复造轮子。为了重试测试,正如在第二章中介绍的那样,有多种选择:JUnit Pioneerrerunner-jupiter。示例 8-28 展示了使用后者的测试。

示例 8-28. 使用 JUnit 5 重试测试
@RepeatedIfExceptionsTest(repeats = 5) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
void testRandomCalculator() {
    // Same logic as the example before }

1

通过简单地使用此注解装饰测试,在失败的情况下最多重复测试五次。

Selenium-Jupiter

使用 Selenium-Jupiter 的测试也可以使用其他扩展。示例 8-29 展示了如何在 Selenium-Jupiter 测试中使用 rerunner-jupiter。

示例 8-29. 使用 JUnit 5 和 Selenium-Jupiter 重试测试
@SingleSession ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@ExtendWith(SeleniumJupiter.class)
class RandomCalculatorSelJupTest {

    @RepeatedIfExceptionsTest(repeats = 5)
    void testRandomCalculator(ChromeDriver driver) {
        // Same logic than the example before
    }

}

1

我们重复所有可能的重试中都使用同一个浏览器。

并行测试执行

执行 Selenium WebDriver 测试套件所需的时间可能相当长(特别是如果测试数量很多)。这种缓慢的原因在于,常规的 Selenium WebDriver 测试每次启动一个新的浏览器,导致整体执行时间增加。解决此问题的一种可能方案是并行执行测试。有多种方法可以实现此并行化。首先,我们可以使用构建工具(如 Maven 或 Gradle)提供的内置并行执行功能。其次,我们可以利用单元测试框架(JUnit 4 或 5 以及 TestNG)提供的功能来实现。以下各小节详细解释了所有这些选项。

Maven

Maven 提供了不同的机制来进行并行执行。首先,Maven 允许并行构建多模块项目的模块。为此,我们需要从命令行使用选项-T调用 Maven 命令。此选项接受两种类型的参数进行并行化:使用固定数量的线程或使用系统中可用 CPU 核心数的乘数因子。以下代码段展示了每种类型的示例:

mvn test -T 4 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
mvn test -T 1C ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

1

它使用四个线程并行执行多模块项目(例如示例仓库)的测试。

2

它使用与 CPU 核心数量相同的线程数(例如,在四核系统中使用四个线程)并行执行多模块项目的测试。

此外,用于在 Maven 中执行单元测试的插件(称为 Surefire)提供了两种并行运行测试的方法。第一种是在单个 JVM 进程内进行多线程操作。要启用此模式,我们需要指定不同的配置参数,例如:

parallel

为并行执行配置并行度级别。该参数的可能值包括methods(在单独的线程中执行测试方法)、classes(测试类)、suites(测试套件)、suitesAndClasses(测试套件和类)、suitesAndMethods(测试套件和方法)以及all(在单独的线程中执行每个测试)。

threadCount

定义并行性的最大线程数。

useUnlimitedThreads

允许无限线程。

有两种方法可以指定这些配置参数。首先,我们可以直接在 Maven 配置文件(即pom.xml文件)中配置它们。示例 8-30 演示了如何进行配置。此外,我们还可以在使用命令行时将这些参数作为系统属性指定,例如:

mvn test -Dparallel=classesAndMethods -DthreadCount=4
示例 8-30. Maven Surefire 配置示例,用于并行执行
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <configuration>
                <parallel>classesAndMethods</parallel>
                <threadCount>4</threadCount>
            </configuration>
        </plugin>
    </plugins>
</build>

使用 Maven Surefire 实现并行的第二种方式是 forking,即创建多个 JVM 进程。如果需要防止线程级并发问题,这种选择很有帮助,因为不同的进程不共享内存空间,这与多线程不同。作为缺点,forking 消耗更多内存且性能较低。要启用 forking,我们需要将 forkCount 配置属性(再次在pom.xml或作为系统属性中)设置为大于一的值(即要创建的 JVM 进程数)。例如,以下命令使用四个 JVM 进程执行 Maven 项目的测试:

mvn test -DforkCount=4

Gradle

Gradle 也提供了几种执行测试的并行方式。首先,它允许在多模块项目中并行执行任务。有两种启用此模式的方法。首先,在配置文件gradle.properties中设置属性org.gradle.parallel=true。其次,使用命令中的选项--parallel,例如:

gradle test --parallel

此外,我们可以在 Gradle 配置文件中使用配置属性 maxParallelForks 来指定要并行启动的最大测试进程数。默认情况下,Gradle 一次只执行一个测试类。通过为此参数设置高于一的值,我们可以更改此默认行为。除了固定值外,我们还可以指定系统中可用 CPU 核心的数量:

maxParallelForks = Runtime.runtime.availableProcessors()

在示例存储库中,此属性通过名为parallel的配置文件条件性启用(见附录 C)。因此,我们可以使用以下命令行来使用此配置文件:

gradle test -Pparallel

JUnit 4

JUnit 通过类 Parallel​Com⁠puter 提供了一种基本的方式来并行执行测试。此类在其构造函数中接受两个布尔参数,以分别启用类和方法的并行测试执行。示例 8-31 展示了使用此类进行测试的示例。

示例 8-31. 使用 JUnit 4 进行并行测试执行
public class ParallelJUnit4Suite {

    @Test
    public void runInParallel() {
        Class<?>[] classes = { Parallel1JUnit4Test.class,
                Parallel2JUnit4Test.class }; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        JUnitCore.runClasses(new ParallelComputer(true, true), classes); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

}

1

我们指定要并行执行的测试类。

2

我们为测试类和方法启用并行测试执行。

TestNG

在 TestNG 中指定测试并行执行的常见方式是通过配置文件 testng.xml。在 TestNG 中启用此模式最相关的属性包括:

parallel

指定并行运行测试的模式。替代方案为 methodstestsclasses

threadcount

设置并行运行测试的默认最大线程数。

示例 8-32 显示了用于测试并行性的 testng.xml 的基本配置。

示例 8-32. TestNG 的并行测试配置
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="parallel-suite" parallel="classes" thread-count="2">
    <test name="parallel-tests">
        <classes>
            <class name=
              "io.github.bonigarcia.webdriver.testng.ch08.parallel.Parallel1NGTest"/>
            <class name=
              "io.github.bonigarcia.webdriver.testng.ch08.parallel.Parallel2NGTest"/>
        </classes>
    </test>
</suite>

我们可以在命令行中使用 Maven 或 Gradle 运行之前的并行测试套件:

mvn test -Dsurefire.suiteXmlFiles=src/test/resources/testng.xml
gradle test -Psuite=src/test/resources/testng.xml

JUnit 5

JUnit 5 允许以不同的方式并行执行测试。以下列表总结了此目的的最相关配置参数:

junit.jupiter.execution.parallel.enabled

启用测试并行性的布尔标志(默认为 false)。

junit.jupiter.execution.parallel.mode.classes.default

为了并行运行测试类。可能的值为 same_thread 表示单线程执行(默认),concurrent 表示并行执行。

junit.jupiter.execution.parallel.mode.default

为了并行运行测试方法。可能的值与之前的测试类相同。

有两种指定这些参数的方式。首先,在配置文件 junit-platform.properties 中(应该位于项目类路径中可用)。示例 8-33 显示了此文件的示例内容。其次,通过使用系统属性和命令行。以下命令(Maven/Gradle)展示了如何操作:

mvn test -Djunit.jupiter.execution.parallel.enabled=true
gradle test -Djunit.jupiter.execution.parallel.enabled=true
示例 8-33. 使用 JUnit 5 进行并行测试执行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = same_thread

此外,Jupiter 编程模型提供了注解 @Execution,用于更改测试类或方法的并行模式。该注解可以在类级别或方法级别使用,接受两个值:ExecutionMode.CONCURRENT(用于并行执行)和 ExecutionMode.SAME_THREAD(用于单线程执行)。示例 8-34 展示了示例仓库中包含的测试类的结构。假设启用了并行测试(如示例 8-33),此类将与其他允许并行化的测试一起并行执行。

示例 8-34. 使用 JUnit 5 进行并行测试执行
@Execution(ExecutionMode.CONCURRENT)
class Parallel1JupiterTest {

    // Test logic

}

测试监听器

在测试过程中通常需要跟踪不同阶段的测试执行。单元测试框架因此提供了称为测试监听器的功能。测试监听器可以被视为通过在测试执行周期的多个阶段执行自定义操作来修改默认测试行为的实用程序。通常情况下,每个单元测试框架都为这些测试监听器提供了自己的实现。

JUnit 4

在 JUnit 4 中,测试监听器包括在测试开始、通过、完成、失败、跳过或忽略时执行的自定义操作。实现 JUnit 4 监听器的第一步是创建一个扩展RunListener类的 Java 类。在这个类中,您可以重写几个方法(例如testRunStartedtestIgnoredtestFailure等),以在测试生命周期的不同步骤中包含额外的逻辑。示例 8-35 展示了一个基本的 JUnit 4 测试监听器实现。此监听器简单地在标准输出中显示有关测试阶段的消息。

示例 8-35. 使用 JUnit 4 测试监听器
public class MyTestListener extends RunListener {

    static final Logger log = getLogger(lookup().lookupClass());

    @Override
    public void testStarted(Description description) throws Exception {
        super.testStarted(description);
        log.debug("testStarted {}", description.getDisplayName());
    }

    @Override
    public void testFailure(Failure failure) throws Exception {
        super.testFailure(failure);
        log.debug("testFailure {} {}", failure.getException(),
                failure.getMessage());
    }

    // Other listeners

}

在 JUnit 4 中注册测试监听器的常见方法是创建一个自定义运行器,并在测试类中使用该运行器。示例 8-36 展示了注册前述监听器的自定义测试运行器。示例 8-37 展示了使用此运行器的测试框架。

示例 8-36. 使用 JUnit 4 测试监听器
public class MyTestRunner extends BlockJUnit4ClassRunner { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    public MyTestRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }

    @Override
    public void run(RunNotifier notifier) {
        notifier.addListener(new MyTestListener()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        super.run(notifier); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }
}

1

我们扩展Blockjunit4classrunner,这是 JUnit 4 中的默认测试运行器。

2

我们注册我们的自定义测试监听器。

3

我们使用默认的测试运行程序继续调用父级。

示例 8-37. 使用 JUnit 4 测试监听器
@RunWith(MyTestRunner.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class ListenersJUnit4Test {

    // Test logic 
}

1

我们使用 JUnit 4 注解@RunWith和我们的自定义运行器装饰测试类。

TestNG

TestNG 提供了接口ITestListener来实现测试监听器。实现此接口的类可以重写各个 TestNG 生命周期阶段的方法,如onTestSuccessonTestFailureonTestSkipped等。示例 8-38 展示了实现此接口的示例类。在这个示例中,监听器方法在标准输出中记录一条消息。示例 8-39 展示了使用此监听器的测试。

示例 8-38. 使用 TestNG 测试监听器
public class MyTestListener implements ITestListener {

    static final Logger log = getLogger(lookup().lookupClass());

    @Override
    public void onTestStart(ITestResult result) {
        ITestListener.super.onTestStart(result);
        log.debug("onTestStart {}", result.getName());
    }

    @Override
    public void onTestFailure(ITestResult result) {
        ITestListener.super.onTestFailure(result);
        log.debug("onTestFailure {}", result.getThrowable());
    }

    // Other listeners

}
示例 8-39. 使用 TestNG 测试监听器
@Listeners(MyTestListener.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class ListenersNGTest {

    // Test logic 
}

1

我们使用 TestNG 注解@Listeners来指定此类中的所有测试使用我们的自定义测试监听器。

JUnit 5

正如前面讨论的(见 表 8-4),Jupiter 提供了各种扩展点,用于在 JUnit 5 测试生命周期中包含自定义逻辑。除了这个扩展模型,JUnit 5 还允许实现测试监听器来跟踪几个测试执行阶段,如测试启动、跳过或完成。此功能通过 JUnit Launcher API 提供,该 API 用于发现、过滤和执行 JUnit 平台中的测试(见 图 2-4)。

要在 JUnit 5 中创建一个测试监听器,我们需要实现 TestExecutionListener 接口。实现这个接口的类可以重写不同的方法,以便在测试执行期间被通知发生的事件。示例 8-40 包含一个实现此接口的基本类。这类监听器通过标准的 Java 服务加载器机制在 JUnit 5 中注册。为此,我们需要在项目类路径中创建一个名为 /META-INF/services/org.junit.platform.launcher.TestExecutionListener 的文件,并写入要注册的测试监听器的完全限定名称(例如 io.github.bonigarcia.webdriver.jupiter.ch08.listeners.MyTestListener 对应 示例 8-40)。请注意,这个文件没有包含在示例存储库中,以避免侵入整个测试套件。

示例 8-40. 使用 JUnit 5 测试监听器
public class MyTestListener implements TestExecutionListener {

    static final Logger log = getLogger(lookup().lookupClass());

    @Override
    public void executionStarted(TestIdentifier testIdentifier) {
        TestExecutionListener.super.executionStarted(testIdentifier);
        log.debug("Test execution started {}", testIdentifier.getDisplayName());
    }

    @Override
    public void executionSkipped(TestIdentifier testIdentifier, String reason) {
        TestExecutionListener.super.executionSkipped(testIdentifier, reason);
        log.debug("Test execution skipped: {}", reason);
    }

    @Override
    public void executionFinished(TestIdentifier testIdentifier,
            TestExecutionResult testExecutionResult) {
        TestExecutionListener.super.executionFinished(testIdentifier,
                testExecutionResult);
        log.debug("Test execution finished {}",
                testExecutionResult.getStatus());
    }

}
注意

接口 TestExecutionListener 属于 JUnit 平台启动器 API;因此,要使用它,我们需要在项目中额外包含这个 API 作为依赖项。附录 C 解释了为此设置所需的 Maven 和 Gradle 配置。

禁用的测试

单元测试框架允许以编程方式禁用(即在测试执行中跳过)整个测试类或单个测试方法。下面的小节解释了 JUnit 4、TestNG、JUnit 5 和 Selenium-Jupiter 之间的区别。

JUnit 4

JUnit 4 提供了注解 @Ignore 来禁用测试。这个注解可以用在类级别或方法级别。可选地,我们可以在注解中包含消息,以说明禁用的原因。示例 8-41 包含一个被禁用的测试。

示例 8-41. 使用 JUnit 4 禁用测试
@Ignore("Optional reason for disabling")
@Test
public void testDisabled() {
    // Test logic
}

TestNG

TestNG 允许以两种方式禁用测试。首先,我们可以对测试类或方法使用注解 @Ignore。其次,我们可以使用 @Test 注解的 enabled 属性。示例 8-42 说明了这两种方法。

示例 8-42. 使用 TestNG 禁用测试
@Ignore("Optional reason for disabling")
@Test
public void testDisabled1() {
    // Test logic
}

@Test(enabled = false)
public void testDisabled2() {
    // Test logic
}

JUnit 5

Jupiter 编程模型提供了各种注解,用于根据不同条件禁用测试。表 8-5 总结了这些注解,而 示例 8-43 提供了一个使用其中一些注解的基本示例。

表 8-5. 用于禁用测试的 Jupiter 注解

注解 描述

|

@Disabled
禁用测试类或方法

|

@DisabledOnJre
@EnabledOnJre
根据 Java 版本来禁用/启用测试

|

@DisabledOnJreRange
@EnabledOnJreRange
根据 Java 版本范围来禁用/启用测试

|

@DisabledOnOs
@EnabledOnOs
根据操作系统(例如,Windows、Linux、macOS 等)来禁用/启用测试

|

@DisabledIfSystemProperty
@DisabledIfSystemProperties
@EnabledIfSystemProperty
@EnabledIfSystemProperties
根据系统属性的值来禁用/启用测试

|

@DisabledIfEnvironmentVariable
@DisabledIfEnvironmentVariables
@EnabledIfEnvironmentVariable
@EnabledIfEnvironmentVariables
根据环境变量的值来禁用/启用测试

|

@DisabledIf
@EnabledIf
根据自定义方法的布尔返回值来禁用/启用测试
示例 8-43. 使用 JUnit 5 禁用的测试
@Disabled("Optional reason for disabling") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@Test
public void testDisabled1() {
    // Test logic }

@DisabledOnJre(JAVA_8) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
@Test
public void testDisabled2() {
    // Test logic }

@EnabledOnOs(MAC) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
@Test
public void testDisabled3() {
    // Test logic }

1

总是跳过此测试。

2

在使用 Java 8 时跳过此测试。

3

除 macOS 外的任何操作系统均跳过此测试。

Selenium-Jupiter

Selenium-Jupiter 提供了额外的注解,可以根据 Selenium WebDriver 测试的特定条件有条件地禁用测试。这些条件包括浏览器可用性、Docker 可用性和 URL 在线性(即使用 GET HTTP 方法请求 URL 时返回 200 响应代码)。示例 8-44 展示了使用这些注解的几个测试。

示例 8-44. 使用 JUnit 5 和 Selenium-Jupiter 禁用的测试
@EnabledIfBrowserAvailable(SAFARI) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@Test
void testDisabled1(SafariDriver driver) {
    // Test logic }

@EnabledIfDockerAvailable ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
@Test
void testDisabled2(@DockerBrowser(type = CHROME) WebDriver driver) {
    // Test logic }

@EnabledIfDriverUrlOnline("http://localhost:4444/") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
@Test
void testDisabled3(
        @DriverCapabilities("browserName=chrome") WebDriver driver) {
    // Test logic }

1

如果系统中没有 Safari,则跳过此测试。

2

如果系统中没有安装 Docker,则跳过此测试。

3

如果 Selenium Server URL 不在线,则跳过此测试。如果在线,则执行该测试,并使用前述 URL 创建 RemoteWebDriver 的实例。在此测试中,我们使用 @DriverCapabilities 注解指定所需的能力(如 第六章 中所解释的)。

总结与展望

本章介绍了本书中使用的测试框架(即 JUnit 4、TestNG、JUnit 5 和 Selenium-Jupiter)的一些最相关的特定功能,用于开发 Selenium WebDriver 测试。首先,您学习了如何实现参数化测试。此功能对于跨浏览器测试(即在不同浏览器上进行网页测试)非常方便。然后,您学习了如何对测试进行分类,并使用这些分类来包含或排除它们以进行测试执行。您还学习了测试失败分析的机制(例如,测试失败时制作浏览器截图)、重试测试或并行执行测试。最后,您了解了如何实现测试监听器以及禁用测试的不同机制。

在下一章中,您将学习如何将 Selenium WebDriver 与不同的第三方工具集成,以实现高级端到端测试。您将了解如何从 Web 应用程序下载文件,在不使用 CDP 的情况下捕获流量(例如在 Firefox 中),测试非功能需求(如性能、安全性或可访问性),处理不同的输入数据,改进测试报告,并与现有框架(如 Spring 或 Cucumber)集成。

第九章:第三方集成

本章介绍了不同的第三方技术(如库或框架),我们可以与 Selenium WebDriver 结合使用。当 Selenium WebDriver API 不足以执行特定任务时,我们需要使用这些技术,比如文件下载,我们需要使用第三方工具来等待文件正确下载,或者使用 HTTP 客户端来控制下载。我们还可以使用第三方代理来捕获 HTTP 流量。

另一个场景是我们需要使用 Selenium WebDriver 与外部工具结合来实现非功能性测试,比如性能、安全性、可访问性或 A/B 测试。我们还可以使用第三方库开发 Selenium WebDriver 测试,使用流畅的 API,生成虚拟测试数据,或者改进测试报告。最后,我们可以集成相关框架如 Cucumber 用于行为驱动开发(BDD),或者 Spring Framework(用于开发 Web 应用)。本章节将详细介绍所有这些用途。

小贴士

要使用本章介绍的第三方工具,你必须首先在项目中包含所需的依赖项。你可以在 附录 C 中找到使用 Maven 和 Gradle 解决每个依赖项的详细信息。

文件下载

Selenium WebDriver 对文件下载的支持有限,因为其 API 不公开下载进度。换句话说,我们可以使用 Selenium WebDriver 下载来自 Web 应用的文件,但无法控制将这些文件复制到本地文件系统所需的时间。因此,我们可以使用第三方库来增强使用 Selenium WebDriver 进行 Web 下载的体验。有不同的替代方案来实现这个目标。下面的小节将详细解释如何做到这一点。

使用浏览器特定的能力

我们可以使用特定于浏览器的能力(就像我们在 第五章 中所做的那样)来配置文件下载的各种参数,例如目标文件夹。这种方法很方便,因为这些功能在 Selenium WebDriver API 中是开箱即用的,但它也有几个缺点。首先,它与不同的浏览器类型(如 Chrome、Firefox 等)不兼容。换句话说,每个浏览器的所需能力是不同的。其次,更重要的是,我们无法控制跟踪下载进度。为了解决这个问题,我们需要使用第三方库。在本书中,我建议使用开源库 Awaitility

Awaitility 是一个流行的库,提供处理异步操作的功能。它通过提供流畅的 API 来管理线程、超时和并发问题。在使用 Selenium WebDriver 下载文件的情况下,我们使用 Awaitility API 等待直到文件存储在文件系统中。示例 9-1 展示了使用 Chrome 和 Awaitility 的示例。示例 9-2 展示了使用 Firefox 时等效的测试设置。

Example 9-1. 使用 Chrome 和 Awaitility 测试下载文件
class DownloadChromeJupiterTest {

    WebDriver driver;

    File targetFolder;

    @BeforeEach
    void setup() {
        targetFolder = new File(System.getProperty("user.home"), "Downloads"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        Map<String, Object> prefs = new HashMap<>();
        prefs.put("download.default_directory", targetFolder.toString()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        ChromeOptions options = new ChromeOptions();
        options.setExperimentalOption("prefs", prefs);

        driver = WebDriverManager.chromedriver().capabilities(options).create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testDownloadChrome() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/download.html"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

        driver.findElement(By.xpath("(//a)[2]")).click(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        driver.findElement(By.xpath("(//a)[3]")).click();

        ConditionFactory await = Awaitility.await()
                .atMost(Duration.ofSeconds(5)); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        File wdmLogo = new File(targetFolder, "webdrivermanager.png");
        await.until(() -> wdmLogo.exists()); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)

        File wdmDoc = new File(targetFolder, "webdrivermanager.pdf");
        await.until(() -> wdmDoc.exists()); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    }

}

1

我们指定一个文件夹保存下载的文件。但是需要注意,Chrome 只允许特定的目录用于下载。例如,它允许下载目录(及其子文件夹),但禁止使用其他路径,如桌面文件夹或主目录。

2

我们使用 Chrome 首选项指定目标文件夹。

3

我们使用练习网站上提供的网页通过点击按钮下载不同的文件(见图 9-1)。

4

我们点击页面上的两个按钮。结果,浏览器开始下载两个文件:一个 PNG 图片和一个 PDF 文档。

5

我们使用 Awaitility 配置了五秒的等待超时。

6

我们等待直到第一个文件在文件系统中。

7

我们还等待第二个文件下载完成。

示例 9-2. 使用 Firefox 下载文件的测试设置
@BeforeEach
void setup() {
    FirefoxOptions options = new FirefoxOptions();
    targetFolder = new File("."); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    options.addPreference("browser.download.dir",
            targetFolder.getAbsolutePath()); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    options.addPreference("browser.download.folderList", 2); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    options.addPreference("browser.helperApps.neverAsk.saveToDisk",
            "image/png, application/pdf"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    options.addPreference("pdfjs.disabled", true); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)

    driver = WebDriverManager.firefoxdriver().capabilities(options)
            .create();
}

1

Firefox 允许指定任何文件夹来下载文件。在这种情况下,我们使用本地项目文件夹。

2

我们使用 Firefox 首选项指定自定义下载目录。

3

我们需要将首选项 browser.download.folderList 设置为 2 以选择自定义下载文件夹。其他可能的值为 0(将文件下载到用户桌面)和 1(使用下载文件夹,即默认值)。

4

我们指定 Firefox 不会要求保存在本地文件系统中的内容类型。

5

我们禁用 PDF 文件的预览。

hosw 0901

图 9-1. 用于下载文件的练习网页

使用 HTTP 客户端

使用 Selenium WebDriver 下载文件的另一种机制是使用 HTTP 客户端库。我建议使用Apache HttpClient,因为 WebDriverManager 内部使用了这个库,因此你可以在项目中作为传递依赖使用它。示例 9-3 展示了使用 Apache HttpClient 从实践站点下载多个文件的完整测试用例。注意,在这种情况下,不需要显式等待文件下载完成,因为 Apache HttpClient 同步处理 HTTP 响应。

示例 9-3. 使用 HTTP 客户端下载文件的测试
class DownloadHttpClientJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @Test
    void testDownloadHttpClient() throws IOException {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/download.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

        WebElement pngLink = driver.findElement(By.xpath("(//a)[2]")); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        File pngFile = new File(".", "webdrivermanager.png");
        download(pngLink.getAttribute("href"), pngFile); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        assertThat(pngFile).exists();

        WebElement pdfLink = driver.findElement(By.xpath("(//a)[3]")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        File pdfFile = new File(".", "webdrivermanager.pdf");
        download(pdfLink.getAttribute("href"), pdfFile);
        assertThat(pdfFile).exists();
    }

    void download(String link, File destination) throws IOException {
        try (CloseableHttpClient client = HttpClientBuilder.create().build()) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
            HttpUriRequestBase request = new HttpGet(link);
            try (CloseableHttpResponse response = client.execute(request)) { ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
                FileUtils.copyInputStreamToFile(
                        response.getEntity().getContent(), destination); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
            }
        }
    }

}

1

我们再次使用实践网页下载文件。

2

我们点击一个按钮来下载一个文件。

3

我们重构了类方法download中下载文件的通用逻辑。

4

我们为第二个要下载的文件重复操作。

5

我们在 try-with-resources 中创建了一个 Apache HTTPClient 实例。这个客户端在语句作用域结束时会自动关闭。

6

我们使用另一个 try-with-resources 语句向提供的 URL 发送 HTTP 请求,并获得 HTTP 响应。

7

我们将生成的文件复制到本地文件系统中。

捕获网络流量

“网络监控” 和 “网络拦截器” 解释了如何使用特定于浏览器的功能来捕获 Selenium WebDriver 和测试中的 Web 应用程序之间的 HTTP 流量。这种机制的缺点是只有支持 CDP 的浏览器才能使用。然而,我们可以为其他浏览器使用第三方代理。在本书中,我建议您为此目的使用BrowserMob代理。

BrowserMob 是一个开源代理,允许使用 Java 库操纵 HTTP 流量。示例 9-4 展示了在 Selenium WebDriver 测试中使用此代理的完整测试。在此示例中,我们使用 BrowserMob 代理拦截测试和目标网站之间的 HTTP 流量,并跟踪这些流量(请求-响应)作为日志跟踪。

示例 9-4. 通过 BrowserMob 代理捕获网络流量的测试
class CaptureNetworkTrafficFirefoxJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    WebDriver driver;

    BrowserMobProxy proxy;

    @BeforeEach
    void setup() {
        proxy = new BrowserMobProxyServer(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        proxy.start(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        proxy.newHar(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
                CaptureType.RESPONSE_CONTENT); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        FirefoxOptions options = new FirefoxOptions();
        options.setProxy(seleniumProxy); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
        options.setAcceptInsecureCerts(true); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)

        driver = WebDriverManager.firefoxdriver().capabilities(options)
                .create();
    }

    @AfterEach
    void teardown() {
        proxy.stop(); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
        driver.quit();
    }

    @Test
    void testCaptureNetworkTrafficFirefox() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");

        List<HarEntry> logEntries = proxy.getHar().getLog().getEntries();
        logEntries.forEach(logEntry -> { ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png)
            log.debug("Request: {} - Response: {}",
                    logEntry.getRequest().getUrl(),
                    logEntry.getResponse().getStatus());
        });
    }

}

1

我们创建一个 BrowserMob 的实例。

2

我们启动这个代理。

3

我们使用 HAR(HTTP 存档)来捕获 HTTP 流量,这是一种基于 JSON 的文件格式,用于捕获和导出这些流量。

4

我们启用捕获交换的 HTTP 请求和响应。

5

我们将 BrowserMob 服务器转换为 Selenium WebDriver 代理。

6

我们将这个代理设置为浏览器选项(在这种情况下,用于 Firefox)。

7

我们需要允许不安全的证书,因为与代理的通信是使用 HTTP(而不是 HTTPS)完成的。

8

我们在测试后停止代理。

9

我们使用代理实例来收集 HTTP 流量(请求和响应)。在这个基本示例中,我们使用记录器将这些信息写入标准输出。

非功能测试

如第一章所述,Selenium WebDriver 主要用于评估 Web 应用程序的功能需求。换句话说,测试人员使用 Selenium WebDriver API 来验证测试中的 Web 应用程序是否按预期行为。然而,我们可以利用这个 API 来测试非功能需求,即性能、安全性、可访问性等质量属性。实现这一目标的常见策略是与特定的第三方实用程序集成。以下各小节解释了用于非功能测试的 Selenium WebDriver 的不同集成。

性能

性能测试评估了特定工作负载下系统在响应速度和稳定性方面的表现。与 Selenium WebDriver 不同,测试人员通常采用专门的工具,如Apache JMeter进行性能测试。 Apache JMeter 是一个开源工具,允许向给定的 URL 端点发送多个 HTTP 请求,同时测量响应时间和其他指标。尽管 Selenium WebDriver 与 Apache JMeter 之间的直接集成并不容易,但我们可以将现有的 Selenium WebDriver 测试用作 JMeter 测试计划(即 JMeter 执行的一系列步骤)。这种方法的好处在于,生成的 JMeter 测试计划将模仿 Selenium WebDriver 测试中使用的相同用户工作流程,重用浏览器进行的相同 HTTP 流量(例如,用于 JavaScript 库、CSS 等)。为此,我提出以下程序:

  1. 使用 BrowserMob 代理(在前一节中介绍)将 Selenium WebDriver 中交换的网络流量捕获为 HAR 文件。

  2. 将生成的 HAR 文件转换为 JMeter 测试计划。 JMeter 中的测试计划存储为扩展名为 JMX 的基于 XML 的文件。

  3. 在 JMeter 中加载 JMX 测试计划并进行调整以模拟并发用户并包括结果监听器

  4. 运行测试计划并评估结果。

示例 9-5 展示了实现第一步的完整测试案例。正如您所见,需要登录才能开始并创建 HAR 文件的步骤在每次测试前后都已完成。您可以使用此方法将现有的功能测试(即 @Test 方法中的逻辑)作为性能测试来执行(在 JMeter 中执行)。

示例 9-5. 创建 HAR 文件的测试。
class HarCreatorJupiterTest {

    WebDriver driver;

    BrowserMobProxy proxy;

    @BeforeEach
    void setup() {
        proxy = new BrowserMobProxyServer(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        proxy.start();
        proxy.newHar();
        proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
                CaptureType.RESPONSE_CONTENT);

        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
        ChromeOptions options = new ChromeOptions();
        options.setProxy(seleniumProxy);
        options.setAcceptInsecureCerts(true);

        driver = WebDriverManager.chromedriver().capabilities(options).create();
    }

    @AfterEach
    void teardown() throws IOException {
        Har har = proxy.getHar(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        File harFile = new File("login.har");
        har.writeTo(harFile);

        proxy.stop();
        driver.quit();
    }

    @Test
    void testHarCreator() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/login-form.html");

        driver.findElement(By.id("username")).sendKeys("user");
        driver.findElement(By.id("password")).sendKeys("user");
        driver.findElement(By.cssSelector("button")).click();
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText).contains("Login successful");
    }

}

1

我们在测试前启动 BrowserMob 并在 WebDriver 会话中配置它。

2

在测试后,我们获取 HAR 文件并将其写入本地文件。

运行前面的测试后,我们获得一个名为 login.har 的 HAR 文件。现在,我们需要将其转换为 JMeter 测试计划。有多种替代方案可供选择。您可以找到多个程序(例如 Ruby 或 Java)在网上免费提供此服务。此外,您还可以使用在线转换服务,如 BlazeMeter JMX Converter。在本示例中,我使用了这个在线服务,并在 JMeter 中打开生成的 JMX 测试计划。此时,您可以根据需要调整 JMeter 配置(您可以在官方的 用户手册 中找到有关 JMeter 的更多信息)。例如,图 9-2 显示了加载生成的 JMX 测试计划后 JMeter GUI 的更改情况:

  • 将并发用户数增加到一百(在“Thread Group”选项卡中)

  • 包括一些结果监听器,例如“聚合图”和“图形结果”。

hosw 0902

图 9-2. JMeter GUI 加载生成的测试计划。

现在,我们可以使用 JMeter 运行测试计划(例如,在 JMeter GUI 中单击绿色三角形按钮)。结果是,根据最初开发的互动,生成了一百个并发用户的负载,作为 Selenium WebDriver 测试的一部分(示例 9-5)。图 9-3 显示了之前添加的监听器的结果。

hosw 0903

图 9-3. JMeter 结果。

使用浏览器生成负载

对于许多 Web 应用性能测试场景,使用像 JMeter 这样的工具非常方便。然而,在需要实际浏览器重现完整用户工作流程(例如视频会议 Web 应用程序)时,此方法不适合。在这种情况下,一种可能的解决方案是与 Docker 一起使用 WebDriverManager。示例 9-6 展示了这种用法。正如您在本测试中所见,WebDriverManager 允许通过在 create() 方法中指定大小参数来简单地创建 WebDriver 实例列表。然后,例如,我们可以使用标准的 Java 使用线程池并行执行测试的 Web 应用程序。

示例 9-6. 使用 WebDriverManager 和 Docker 进行负载测试。
class LoadJupiterTest {

    static final int NUM_BROWSERS = 5;

    final Logger log = getLogger(lookup().lookupClass());

    List<WebDriver> driverList;

    WebDriverManager wdm = WebDriverManager.chromedriver().browserInDocker(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    @BeforeEach
    void setupTest() {
        assumeThat(isDockerAvailable()).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        driverList = wdm.create(NUM_BROWSERS); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

    @AfterEach
    void teardown() {
        wdm.quit();
    }

    @Test
    void testLoad() throws InterruptedException {
        ExecutorService executorService = newFixedThreadPool(NUM_BROWSERS); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        CountDownLatch latch = new CountDownLatch(NUM_BROWSERS);

        driverList.forEach((driver) -> { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
            executorService.submit(() -> {
                try {
                    checkHomePage(driver);
                } finally {
                    latch.countDown();
                }
            });
        });

        latch.await(60, SECONDS); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
        executorService.shutdown();
    }

    void checkHomePage(WebDriver driver) {
        log.debug("Session id {}", ((RemoteWebDriver) driver).getSessionId());
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们创建一个 Chrome 管理器实例,使用 Docker 将浏览器作为容器执行。

2

我们假设运行此测试的机器上已安装 Docker。否则,将跳过此测试。

3

我们创建一个 WebDriver 列表(本示例中包含五个实例)。

4

我们使用与 WebDriver 列表相同大小的线程池。

5

我们使用线程池来并行执行 SUT 评估。

6

我们等待每个并行评估完成。我们使用基于计数器门闩的同步方法来实现。

安全性

软件安全领域的一个相关组织是 OWASP(开放网络应用安全项目),这是一个促进开放解决方案以提高软件安全性的非营利基金会。其中最流行的 OWASP 项目之一是 Zed Attack Proxy(ZAP)。OWASP ZAP 是一个开源的 Web 应用安全性扫描工具,用于实施漏洞评估(即寻找安全问题)或渗透测试(即模拟的网络攻击)以发现可利用的 Web 应用漏洞。

我们可以将 OWASP ZAP 作为独立的桌面应用程序使用。Figure 9-4 显示了其 GUI 的截图。

hosw 0904

图 9-4. OWASP ZAP GUI

此 GUI 提供不同的功能以自动化扫描,以检测 Web 应用可能面临的安全威胁,如 SQL 注入、跨站点脚本(XSS)或跨站点请求伪造(CSRF)等。

除了独立应用程序外,我们还可以将 Selenium WebDriver 测试与 ZAP 集成。示例 9-7 提供了一个说明此集成的测试用例。执行此测试所需的步骤包括:

  1. 在本地主机上启动 OWASP ZAP。默认情况下,OWASP 启动一个代理,监听端口 8080。您可以使用 OWASP GUI 中的菜单选项 Tools → Options → Local Proxies 更改此端口。

  2. 禁用 API 密钥(或在 Selenium WebDriver 测试中复制其值)。您可以在菜单选项 Tools → Options → API 中更改此值。

  3. 实现一个使用 OWASP ZAP 作为代理的 Selenium WebDriver 测试(类似于 示例 9-7)。

  4. 执行 Selenium WebDriver 测试。此时,您应该在 ZAP GUI 中看到生成的漏洞报告。

示例 9-7. 使用 OWASP ZAP 作为安全扫描器的测试
class SecurityJupiterTest {

    static final Logger log = getLogger(lookup().lookupClass());

    static final String ZAP_PROXY_ADDRESS = "localhost"; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    static final int ZAP_PROXY_PORT = 8080;
    static final String ZAP_API_KEY = "<put-api-key-here-or-disable-it>"; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    WebDriver driver;

    ClientApi api;

    @BeforeEach
    void setup() {
        String proxyStr = ZAP_PROXY_ADDRESS + ":" + ZAP_PROXY_PORT;
        assumeThat(isOnline("http://" + proxyStr)).isTrue();

        Proxy proxy = new Proxy(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        proxy.setHttpProxy(proxyStr);
        proxy.setSslProxy(proxyStr);

        ChromeOptions options = new ChromeOptions();
        options.setAcceptInsecureCerts(true);
        options.setProxy(proxy);

        driver = WebDriverManager.chromedriver().capabilities(options).create();

        api = new ClientApi(ZAP_PROXY_ADDRESS, ZAP_PROXY_PORT, ZAP_API_KEY); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

    @AfterEach
    void teardown() throws ClientApiException {
        if (api != null) {
            String title = "My ZAP report";
            String template = "traditional-html";
            String description = "This is a sample report";
            String reportfilename = "zap-report.html";
            String targetFolder = new File("").getAbsolutePath();
            ApiResponse response = api.reports.generate(title, template, null,
                    description, null, null, null, null, null, reportfilename,
                    null, targetFolder, null); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
            log.debug("ZAP report generated at {}", response.toString());
        }
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void testSecurity() {
        driver.get(
                "https://bonigarcia.dev/selenium-webdriver-java/web-form.html");
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们配置 ZAP 本地代理监听的地址和端口。

2

如果未禁用 ZAP API 密钥,则需要在这里设置其值。

3

我们将 ZAP 配置为 Selenium WebDriver 代理。

4

我们使用其 API 与 ZAP 进行交互。

5

在测试结束后,我们生成一个 HTML 报告,报告中包含在执行 Selenium WebDriver 测试期间发现的漏洞。图 9-5 显示了此报告的屏幕截图。

注意

我们还可以作为独立的 GUI 使用 OWASP ZAP,如前所介绍。与 Selenium WebDriver 集成的潜在好处可能是重用现有的功能测试以评估安全性或自动化安全评估(例如,由 CI 服务器执行的回归测试套件)。

hosw 0905

图 9-5. ZAP 在执行 Selenium WebDriver 测试后生成的报告

可访问性

数字可访问性指的是残障用户有效使用软件系统(如网站、移动应用等)的能力。在这一领域的一个重要参考是 Web 内容可访问性指南(WCAG),这是由 W3C Web 可访问性倡议(WAI)制定的一套标准建议,解释如何使网页内容对残障人士更易访问。

有几种方法可以测试 Web 应用的可访问性。最常见的方法是检查 WCAG 建议。为此,我们可以使用自动化可访问性扫描器,如 Axe,这是一个开源引擎,用于按照 WCAG 规则自动测试 Web 应用的可访问性。Axe 通过 Selenium WebDriver 的 Java 绑定与一个 辅助库 实现无缝集成。示例 9-8 展示了使用此库进行测试。

示例 9-8. 使用 Axe 生成可访问性报告的测试
@Test
void testAccessibility() {
    driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
    assertThat(driver.getTitle()).contains("Selenium WebDriver");

    Results result = new AxeBuilder().analyze(driver); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    List<Rule> violations = result.getViolations(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    violations.forEach(rule -> {
        log.debug("{}", rule.toString()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    });
    AxeReporter.writeResultsToJsonFile("testAccessibility", result); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们使用 Axe 分析当前的 WebDriver 会话。这样,浏览器中加载的所有页面都将由 Axe 扫描。

2

我们获得了可访问性违规的报告。

3

我们将每个违规记录在标准输出中。在此示例中,发现的问题包括:

色彩对比

元素必须具有足够的颜色对比度。

标题顺序

标题级别应仅增加一级。

图片替代文本

图像必须具有替代文本。

链接名称

链接必须具有可识别的文本。

4

我们将结果写入本地的 JSON 文件中。

A/B 测试

A/B 测试是一种评估可用性的形式,它比较同一应用程序的不同变体,以发现哪一个对最终用户更有效。不同的商业产品为 Selenium WebDriver 测试提供了 A/B 测试的高级功能。例如,Applitools Eyes 提供了多个网页变体的自动视觉比较。另一个选择是 Optimizely,这是一家提供定制和实验 A/B 测试工具的公司。

另一种进行 A/B 测试的方法是使用原始的 Selenium WebDriver API 和自定义条件来处理网页的不同变体。示例 9-9 展示了一个基于手动方法实现多变体网页的基本测试。请注意,这个测试展示了一种基于评估不同页面变体的 A/B 测试的简单方法。

示例 9-9. 使用 Selenium WebDriver 的基本 A/B 测试
@Test
void testABTesting() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/ab-testing.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    WebElement header = wait.until(
            ExpectedConditions.presenceOfElementLocated(By.tagName("h6")));

    if (header.getText().contains("variation A")) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        assertBodyContains(driver, "Lorem ipsum");
    } else if (header.getText().contains("variation B")) {
        assertBodyContains(driver, "Nibh netus");
    } else {
        fail("Unknown variation");
    }
}

void assertBodyContains(WebDriver driver, String text) {
    String bodyText = driver.findElement(By.tagName("body")).getText();
    assertThat(bodyText).contains(text);
}

1

我们打开一个多变体练习网页。该页面的内容每次随机加载的概率为 50%。

2

我们检查页面的变体是否符合预期。

流畅 API

如在 第一章 中介绍的,Selenium 是其他框架和库的基础技术。例如,我们可以找到几个封装了 Selenium WebDriver 的库,以便为 Web 应用程序创建端到端测试的流畅 API。这类库的一个例子是 Selenide,这是一个开源(MIT 许可证)的库,它在 Selenium WebDriver 之上定义了一个简洁的流畅 API。Selenide 提供多种好处,例如自动等待 Web 元素或支持 AJAX 应用程序。

与 Selenium WebDriver 相比,Selenide 的一个显著区别在于它在内部处理 WebDriver 对象。为此,它使用 WebDriverManager 来解析所需的驱动程序(例如 chromedriver、geckodriver 等),将 WebDriver 实例保持在一个单独的线程中,在测试结束时关闭。因此,不需要创建和终止 WebDriver 对象所需的测试样板代码。示例 9-10 通过展示一个基本的 Selenide 测试演示了这个特性。

示例 9-10. 使用 Selenide 进行测试
class SelenideJupiterTest {

    @Test
    void testSelenide() {
        open("https://bonigarcia.dev/selenium-webdriver-java/login-form.html"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

        $(By.id("username")).val("user"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        $(By.id("password")).val("user");
        $("button").pressEnter(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        $(By.id("success")).shouldBe(visible)
                .shouldHave(text("Login successful")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    }

}

1

我们使用 Selenide 提供的 open 静态方法来访问给定的 URL。默认情况下,Selenide 使用本地的 Chrome 浏览器,尽管可以通过配置类(例如 Configuration.browser = "firefox";)或使用 Java 系统属性(例如 -Dselenide.browser=firefox)来更改浏览器。

2

Selenide 的 $ 方法允许您通过 CSS 选择器或使用 Selenium WebDriver 的 By 定位器定位 Web 元素。此行代码使用后者向输入字段输入文本。

3

我们通过 CSS 选择器定位另一个网页元素,并单击它。

4

我们验证登录成功的网页元素是否存在,并包含预期的文本。

测试数据

任何测试用例的相关部分都是测试数据,即用于执行 SUT 的输入数据。选择适当的测试数据对于实施有效的测试至关重要。经典测试理论中用于测试数据选择的不同技术包括:

等价分区

通过将所有可能的输入测试数据分成我们假定以相同方式处理的值集的过程来进行测试。

边界测试

在输入值的极端端点或分区之间进行测试的过程。这种方法的基本思想是在输入域中选择代表性极限值(例如,最小值以下、最小值、略高于最小值、标称值、略低于最大值、最大值和超过最大值的值)。

这些方法在端到端测试中可能并不实用,因为执行这些策略所需的测试数目(以及随之而来的执行时间)可能是巨大的。相反,我们通常手动选择一些代表性测试数据来验证快乐路径(即正向测试),并可选择一些用于意外情况的测试数据(负向测试)。

选择测试数据的另一种选择是使用数据,即不同域的随机数据,如个人姓名、姓氏、地址、国家、电子邮件、电话号码等。实现这一目标的一个简单替代方法是使用Java Faker,这是流行的Ruby faker库的 Java 移植版。示例 9-11 展示了使用该库进行测试的示例。此测试使用假数据提交了一个练习站点上可用的 Web 表单。图 9-6 展示了在使用该假数据提交表单后的网页。

示例 9-11. 使用 Java Faker 生成不同类型的假数据进行测试
@Test
void testFakeData() {
    driver.get(
            "https://bonigarcia.dev/selenium-webdriver-java/data-types.html");

    Faker faker = new Faker(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

    driver.findElement(By.name("first-name")) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
            .sendKeys(faker.name().firstName());
    driver.findElement(By.name("last-name"))
            .sendKeys(faker.name().lastName());
    driver.findElement(By.name("address"))
            .sendKeys(faker.address().fullAddress());
    driver.findElement(By.name("zip-code"))
            .sendKeys(faker.address().zipCode());
    driver.findElement(By.name("city")).sendKeys(faker.address().city());
    driver.findElement(By.name("country"))
            .sendKeys(faker.address().country());
    driver.findElement(By.name("e-mail"))
            .sendKeys(faker.internet().emailAddress());
    driver.findElement(By.name("phone"))
            .sendKeys(faker.phoneNumber().phoneNumber());
    driver.findElement(By.name("job-position"))
            .sendKeys(faker.job().position());
    driver.findElement(By.name("company")).sendKeys(faker.company().name());

    driver.findElement(By.tagName("form")).submit();

    List<WebElement> successElement = driver
            .findElements(By.className("alert-success"));
    assertThat(successElement).hasSize(10); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

    List<WebElement> errorElement = driver
            .findElements(By.className("alert-danger"));
    assertThat(errorElement).isEmpty(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
}

1

我们创建一个 Java Faker 实例。

2

我们发送不同类型的随机数据(姓名、地址、国家等)。

3

我们验证数据是否被正确提交。

4

我们检查页面上是否没有错误。

hosw 0906

图 9-6. 使用假数据的实践页面

报告

测试报告是在执行测试套件后总结结果的文档。这份文档通常包括执行的测试数量及其判定结果(通过、失败、跳过)和执行时间。在我们的 Java 项目中,有不同的方法可以获得测试报告。例如,在使用 Maven 时,我们可以使用以下命令创建基本的测试报告:

mvn test ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
mvn surefire-report:report-only ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
mvn site -DgenerateReports=false ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

1

我们使用 Maven 执行测试。因此,Maven Surefire 插件在target文件夹中生成了一组 XML 文件。这些文件包含了测试执行的结果。

2

我们将 XML 报告转换为 HTML 报告。您可以在项目文件夹target/site中找到此 HTML 报告。

3

我们强制复制 CSS 和 HTML 报告中所需的图像。图 9-7 显示了这份报告的屏幕截图。

hosw 0907

图 9-7。使用 Maven 生成的测试报告

我们还可以使用 Gradle 生成等效的报告。在使用此构建工具执行测试套件后,Gradle 会自动在文件夹build/reports中生成一个 HTML 报告。图 9-8 展示了执行一组测试(使用 shell 命令gradle test --tests Hello*)时生成的测试报告示例。

hosw 0908

图 9-8。使用 Gradle 生成的测试报告

除了 Maven 和 Gradle,我们还可以使用现有的报告库来创建更丰富的报告。一个可能的替代方案是Extent Reports,一个用于创建交互式测试报告的库。Extent Reports 提供专业(商业)和社区(开源)版本。示例 9-12 展示了使用后者的测试。

示例 9-12。使用 Extent Reports 生成 HTML 报告的测试
class ReportingJupiterTest {

    WebDriver driver;

    static ExtentReports reports;

    @BeforeAll
    static void setupClass() {
        reports = new ExtentReports(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        ExtentSparkReporter htmlReporter = new ExtentSparkReporter(
                "extentReport.html");
        reports.attachReporter(htmlReporter); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    }

    @BeforeEach
    void setup(TestInfo testInfo) {
        reports.createTest(testInfo.getDisplayName()); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        driver = WebDriverManager.chromedriver().create();
    }

    @AfterEach
    void teardown() {
        driver.quit();
    }

    @AfterAll
    static void teardownClass() {
        reports.flush();
    }

    // Test methods ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

}

1

我们创建了一个测试报告生成器的实例。

2

我们对其进行配置,以生成一个 HTML 报告。

3

在每次测试后,我们使用测试名称作为标识符在测试报告中创建一个条目。在 JUnit 5 中,我们使用TestInfo,这是一个内置的参数解析器,允许检索关于当前测试的信息。

4

和往常一样,您可以在示例仓库中找到完整的源代码。特别是,这个类有两个测试方法。图 9-9 展示了当执行这个测试类时生成的测试报告的结果。

hosw 0909

图 9-9。使用 Extent Reports 生成的测试报告

Extent Reports 的一个不便之处是我们需要显式地将每个测试添加到报告器中。解决此问题的一个可能方法是使用自定义测试监听器(如“测试监听器”中所述)来组织报告的公共逻辑。

另一个生成丰富测试报告的可能库是Allure,一个开源报告框架,用于生成 Java、Python 和 JavaScript 等不同编程语言的测试报告。Allure 和 Extent Reports 之间一个显著的区别在于 Allure 使用了在构建工具 Maven 或 Gradle 中配置的测试监听器(详见附录 C 了解有关此配置的详细信息)。因此,我们无需更改测试套件即可生成 Allure 报告。表格 9-1 总结了使用 Maven 和 Gradle 创建 Allure 报告所需的命令。

表格 9-1. 使用 Allure 生成测试报告的 Maven 和 Gradle 命令

Maven Gradle 描述

|

mvn test
mvn allure:report
mvn allure:serve

|

gradle test
gradle allureReport
gradle allureServe

| 运行测试用例,生成目标文件夹中的报告

使用本地 Web 服务器打开 HTML 报告(如图 9-10 所示) |

hosw 0910

图 9-10. 使用 Allure 生成并本地展示的测试报告

行为驱动开发

如第一章介绍的,行为驱动开发(BDD)是一种软件方法论,促进使用高级用户场景开发和测试软件系统。不同的工具实现了 BDD 方法论。其中最流行的之一是Cucumber。Cucumber 根据用Gherkin语言编写的用户故事执行测试,这种语言是一种基于自然语言(如英语和其他语言)的人类可读标记。Gherkin 旨在供非程序员(例如客户或最终用户)使用,其主要关键字如下(详见Gherkin 用户手册获取更多信息):

特性

被测试的软件功能的高级描述。

场景

描述业务规则的具体测试。场景描述了 Gherkin 行话中称为步骤的不同信息,例如:

给定

前提条件和初始状态

用户动作

并且

附加用户操作

然后

预期结果

示例 9-13 展示了一个包含两个场景(登录成功和失败)的 Gherkin 特性。

示例 9-13. 用于登录实践站点的 Gherkin 场景
Feature: Login in practice site ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

  Scenario: Successful login ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    Given I use "Chrome" ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    When I navigate to
        "https://bonigarcia.dev/selenium-webdriver-java/login-form.html" ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    And I log in with the username "user" and password "user" ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    And I click Submit
    Then I should see the message "Login successful" ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)

  Scenario: Failure login ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    Given I use "Chrome"
    When I navigate to
        "https://bonigarcia.dev/selenium-webdriver-java/login-form.html"
    And I log in with the username "bad-user" and password "bad-password"
    And I click Submit
    Then I should see the message "Invalid credentials"

1

功能描述

2

第一个场景(登录成功)

3

要使用的浏览器

4

网页 URL

5

一组动作(输入凭据并点击提交按钮)

6

预期的消息

7

第二个场景(登录失败)

要将 Gherkin 场景作为测试用例运行,我们首先必须创建相应的步骤定义。步骤定义是使用在场景中指定信息的glue code来执行 SUT。在 Java 中,我们使用注解(如@Given@Then@When@And)来装饰实现每个步骤的方法。这些注解包含一个字符串值,用于映射每个步骤定义和参数。Example 9-14 展示了用于 Gherkin 场景的步骤定义,该场景在 Example 9-13 中定义。我们使用 Selenium WebDriver API 来执行导航、网页元素交互等所需的操作。

Example 9-14. 使用 Cucumber 登录练习站点的步骤
public class LoginSteps {

    private WebDriver driver;

    @Given("I use {string}") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    public void iUse(String browser) {
        driver = WebDriverManager.getInstance(browser).create();
    }

    @When("I navigate to {string}") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    public void iNavigateTo(String url) {
        driver.get(url);
    }

    @And("I log in with the username {string} and password {string}") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    public void iLogin(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);

    }

    @And("I click Submit") ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    public void iPressEnter() {
        driver.findElement(By.cssSelector("button")).click();
    }

    @Then("I should see the message {string}") ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
    public void iShouldSee(String result) {
        try {
            String bodyText = driver.findElement(By.tagName("body")).getText();
            assertThat(bodyText).contains(result);
        } finally {
            driver.quit();
        }
    }

}

1

我们使用第一步创建一个WebDriver实例。

2

我们打开 URL。

3

我们输入凭据。

4

我们点击提交按钮。

5

我们验证页面上是否有预期的消息。

最后,我们需要将步骤定义作为测试用例运行。通常情况下,我们使用单元测试框架创建该测试。这个测试在与 Cucumber 集成时依赖于单元测试框架,换句话说,不同的是在 JUnit 4 中(见 Example 9-15),JUnit 5 中(见 Example 9-16)和 TestNG 中(见 Example 9-17)。生成的测试以通常的方式执行(即使用 Shell 或 IDE)。

注意

Selenium-Jupiter 在与 Cucumber 集成时并未提供任何额外功能,因此在示例存储库中的 Selenium-Jupiter 项目中,使用的是默认的 JUnit 5 过程。

Example 9-15. 使用 JUnit 4 进行 Cucumber 测试
@RunWith(Cucumber.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@CucumberOptions(features = "classpath:io/github/bonigarcia", glue = {
        "io.github.bonigarcia" }) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
public class CucumberTest {

}

1

我们使用 Cucumber 运行器来执行步骤定义作为测试用例。

2

我们指定了 Gherkin 场景的位置。在这种情况下,特性位于项目类路径中的文件夹io/github/bonigarcia(具体而言,在src/test/resources文件夹中)。此注解还指定了要搜索 glue code(即步骤定义)的初始包。

Example 9-16. 使用 JUnit 5 进行 Cucumber 测试
@Suite ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@IncludeEngines("cucumber") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
@SelectClasspathResource("io/github/bonigarcia") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.github.bonigarcia") ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
public class CucumberTest {

}

1

我们需要使用 JUnit 5 套件模块来运行 Cucumber 测试。

2

我们在 JUnit 平台中包含了 Cucumber 引擎。

3

我们指定了项目类路径中特性文件的路径。

4

我们将初始包设定为搜索粘合代码。

例 9-17。使用 TestNG 进行 Cucumber 测试
@CucumberOptions(features = "classpath:io/github/bonigarcia", glue = {
        "io.github.bonigarcia" }) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class CucumberTest extends AbstractTestNGCucumberTests { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

}

1

我们指定 Gherkin 场景的位置和搜索粘合代码的包的位置。

2

我们扩展 TestNG 父类以运行 Cucumber 测试。

Web 框架

Web 框架是旨在支持 Web 应用程序和服务开发的软件框架。Java 语言中最流行的框架之一是 Spring Framework。Spring 是一个用于构建 Java 应用程序(包括企业 Web 应用程序和服务)的开源框架。Spring 的核心技术被称为控制反转(IoC),它是一种在使用这些对象的类之外创建实例的过程。Spring 术语中称这些对象为beancomponent,它们稍后按需作为依赖项由 Spring IoC 容器注入。

以下示例展示了用 Spring-Boot 创建的本地 Web 应用程序的基本测试,Spring-Boot 是 Spring 系列的一个子项目,通过惯例优于配置和自动发现功能简化了基于 Spring 的应用程序的开发。此外,Spring-Boot 提供了一个嵌入式 Web 服务器,以便轻松开发 Web 应用程序。在这种项目中与 Selenium WebDriver 的集成有助于通过在每个测试案例中自动部署在嵌入式 Web 服务器中来简化 Spring 基础 Web 应用程序的测试过程。

用于集成 Spring-Boot 与 Selenium WebDriver 和本书中使用的单元测试框架的代码是不同的。示例 9-18 显示了一个集成 Spring-Boot 和 JUnit 4 的测试。示例 9-19 展示了使用 TestNG 时的差异,示例 9-20 说明了如何使用 JUnit 5,最后,示例 9-21 展示了一个基于 JUnit 5 和 Selenium-Jupiter 的 Spring 测试。

例 9-18。使用 Spring-Boot 和 JUnit 4 进行测试
@RunWith(SpringRunner.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
public class SpringBootJUnit4Test {

    private WebDriver driver;

    @LocalServerPort ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    protected int serverPort;

    @Before
    public void setup() {
        driver = WebDriverManager.chromedriver().create();
    }

    @After
    public void teardown() {
        driver.quit();
    }

    @Test
    public void testSpringBoot() {
        driver.get("http://localhost:" + serverPort);
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText)
                .contains("This is a local site served by Spring-Boot");
    }

}

1

我们在 JUnit 4 中使用 Spring 运行器。

2

我们使用 Spring-Boot 的测试注解来定义 Spring-Boot 类名。此外,我们指定 Web 应用程序使用随机可用端口部署。

3

我们将 Web 应用程序端口注入为类属性。

例 9-19。使用 Spring-Boot 和 TestNG 进行测试
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
public class SpringBootNGTest extends AbstractTestNGSpringContextTests { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

    // Same logic as the previous test 
}

1

我们在 JUnit 4 中也同样使用 @SpringBootTest 注解。

2

我们扩展 TestNG 父类以使用 Spring 上下文运行此测试。

例 9-20。使用 Spring-Boot 和 JUnit 5 进行测试
@ExtendWith(SpringExtension.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
class SpringBootJupiterTest {

    // Same logic as the previous test 
}

1

我们使用 JUnit 5 的 Spring 扩展将 Spring 上下文集成到 Jupiter 测试中。

2

我们使用 Spring-Boot 来启动我们的 Spring 应用程序上下文,就像之前的例子中一样。

示例 9-21. 使用 Spring-Boot 和 JUnit 5 加上 Selenium-Jupiter 进行测试
@ExtendWith({ SeleniumJupiter.class, SpringExtension.class }) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
@SpringBootTest(classes = SpringBootDemoApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SpringBootSelJupTest {

    @LocalServerPort
    protected int serverPort;

    @Test
    void testSpringBoot(ChromeDriver driver) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        driver.get("http://localhost:" + serverPort);
        String bodyText = driver.findElement(By.tagName("body")).getText();
        assertThat(bodyText)
                .contains("This is a local site served by Spring-Boot");
    }

}

1

在 Selenium-Jupiter 的情况下,我们使用了两个 JUnit 5 扩展(用于 Spring 和 Selenium WebDriver)。

2

如同在 Selenium-Jupiter 中一贯的做法,我们使用了 JUnit 5 的参数解析机制来声明在本测试中使用的 WebDriver 实例的类型。

总结与展望

本章为使用 Selenium WebDriver 进行端到端测试的不同技术(如工具、库和框架)集成提供了实际概述。首先,我们使用 Awaitility(处理异步操作的库)等待 Selenium WebDriver 下载文件。执行相同用例(即下载文件)的另一种库是 Apache HttpClient。然后,我们使用 BrowserMob 代理拦截 Selenium WebDriver 测试交换的 HTTP 流量。接下来的技术组集中于使用 Selenium WebDriver 进行非功能测试:BrowserMob(为性能测试创建 JMeter 测试计划)、OWASP ZAP(安全测试)和 Axe(可访问性测试)。然后,我们使用 Selenide 提供的流畅 API 和 Java Faker 为 Selenium WebDriver 测试创建虚拟测试数据,并使用 Extent Reports 和 Allure 生成丰富的测试报告。最后,我们了解了如何将 Cucumber(BDD 框架)和 Spring(Java 和 Web 框架)与 Selenium WebDriver 集成。

下一章通过介绍 Selenium WebDriver 的补充框架来总结本书,即用于测试 REST 服务的 REST Assured 和用于测试移动应用程序的 Appium。最后,本章介绍了几个流行的 Selenium WebDriver 浏览器自动化空间的替代品:Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。

第十章:超越 Selenium

本章通过介绍几种与 Selenium 互补的技术来结束本书。首先,我们分析了移动应用程序的基础并介绍了 Appium,这是一个流行的用于移动测试的测试框架。然后,您将学习如何使用名为 REST Assured 的开源 Java 库测试 REST(表现状态转移)服务。最后,您将介绍用于实施 Web 应用程序端到端测试的 Selenium WebDriver 的替代工具,即:Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。

移动应用程序

移动应用程序(通常称为移动应用程序或简称为应用程序)是专为运行在移动设备上设计的软件应用程序,例如智能手机、平板电脑或可穿戴设备。移动设备的两个主要操作系统为:

安卓

基于修改版 Linux 的开源(Apache 2.0 许可)移动操作系统。最初由名为 Android 的初创公司开发,于 2005 年被 Google 收购。

iOS

由苹果专门为其硬件(例如 iPhone、iPad 或 Watch)创建的专有移动操作系统。

对移动应用程序进行分类的一种常见方式如下:

本地应用程序

针对特定移动操作系统(例如 Android 或 iOS)开发的移动应用程序。

基于 Web 的应用程序

在移动浏览器(例如 Chrome、Safari 或 Firefox Mobile)中渲染的 Web 应用程序。这些应用程序通常设计为 响应式(即可适应不同的屏幕大小和视口)。

混合应用程序

使用客户端 Web 标准(即 HTML、CSS 和 JavaScript)开发的移动应用程序,并使用称为 webview 的本地容器部署到移动设备上。支持混合应用程序开发的框架示例包括 IonicReact NativeFlutter

渐进式 Web 应用程序(PWA)

使用现代 Web 标准 API 构建的 Web 应用程序(用于可安装性、响应性等),旨在在多个平台上运行,包括桌面和移动设备。

移动测试

测试是移动应用程序开发中的重要流程。移动测试涉及诸如硬件兼容性、网络连接性或操作系统特定性等不同挑战。执行移动测试的不同方法包括:

使用桌面浏览器进行移动仿真

我们可以使用 Selenium WebDriver 进行此类移动测试。为此,您可以使用特定于浏览器的功能(如“设备仿真”中所述),或者使用基于 Chromium 的浏览器中的 CDP(如“设备仿真”中所述)。

使用虚拟设备

有两种类型的虚拟移动设备:

模拟器

桌面应用程序,虚拟化移动设备的所有方面,包括硬件和操作系统。

模拟器

模仿移动操作系统某些功能的桌面应用程序。它们主要用于 iOS,因为 Android 设备很容易模拟。

使用真实设备

使用实际设备及其在真实条件下的本机 Android 或 iOS API。

Appium

Appium 是一个用于移动应用的开源测试自动化框架。Appium 提供了一个跨平台的 API,允许在虚拟或真实设备上测试 iOS 和 Android 的本地、混合和移动 Web 应用程序。此外,Appium 还能够在 Windows 和 macOS 上对桌面应用程序进行自动化测试。

Appium 的故事始于 2011 年,当时 Dan Cuellar 创建了一个名为 iOSAuto 的用于 C# 开发的 iOS 应用程序自动化工具。他在 2012 年的伦敦 SeleniumConf 上遇到了 Selenium 的共同创始人 Jason Huggins。Jason 通过添加一个 Web 服务器,并使用基于 HTTP 的 WebDriver wire 协议,使 iOSAuto 兼容任何 Selenium WebDriver 客户端。他们将项目更名为 Appium(适用于应用程序的 Selenium)。2013 年 1 月,Sauce Labs 决定支持 Appium 并提供更多开发者力量。新团队使用 Node.js 重写了 Appium,因为它是服务器端的知名高效框架。

如 图 10-1 所示,Appium 遵循客户端-服务器架构。Appium 是一个 Web 服务器,暴露了一个 REST API,用于在移动或桌面应用上执行自动化会话。因此,Appium 服务器接收来自客户端的传入请求,在目标设备/应用上执行这些命令,并以 HTTP 响应形式返回代表命令执行结果的信息。Appium 客户端库使用移动 JSON Wire Protocol(作为原始 WebDriver 协议的官方扩展草案)。Appium 服务器及其客户端还使用 W3C WebDriver 规范。Appium 项目和社区维护了不同的 Appium 客户端库。表 10-1 总结了这些官方维护的和社区维护的库。

hosw 1001

图 10-1. Appium 架构

表格 10-1. Appium 客户端库

名称 语言 许可证 维护者 网站
Appium java-client Java Apache 2.0 Appium 团队 https://github.com/appium/java-client
Appium ruby_lib Ruby Apache 2.0 Appium 团队 https://github.com/appium/ruby_lib
Appium Python 客户端 Python Apache 2.0 Appium 团队 https://github.com/appium/python-client
appium-dotnet-driver C# Apache 2.0 Appium 团队 https://github.com/appium/appium-dotnet-driver
WebdriverIO JavaScript(Node.js) MIT WebdriverIO 团队 https://webdriver.io
web2driver JavaScript(浏览器) Apache 2.0 HeadSpin https://github.com/projectxyzio/web2driver
RobotFramework 的 Appium 库 Python Apache 2.0 Serhat Bolsu https://github.com/serhatbolsu/robotframework-appiumlibrary

在 Appium 中,对特定平台的自动化支持是由 Appium 行话中称为 driver 的组件提供的。这些驱动程序在版本 1 中与 Appium 服务器紧密耦合。然而,在本文撰写时的最新版本 Appium 2 中,这些驱动程序与 Appium 服务器分开(参见 图 10-1)并单独安装。

Appium 驱动程序表格 10-2

名称 目标 描述 存储库
XCUITest 驱动程序 iOS 和 tvOS 应用程序 利用 Apple 的 XCUITest 库实现自动化 https://github.com/appium/appium-xcuitest-driver
Espresso 驱动程序 Android 应用程序 通过 Espresso(Android 的测试框架)实现自动化 https://github.com/appium/appium-espresso-driver
UiAutomator2 驱动程序 Android 应用程序 利用 Google UiAutomator2 技术在 Android 设备或模拟器上实现自动化 https://github.com/appium/appium-uiautomator2-driver
Windows 驱动程序 Windows 桌面应用程序 使用 WinAppDriver,Windows 桌面应用程序的 WebDriver 服务器 https://github.com/appium/appium-windows-driver
Mac 驱动程序 macOS 桌面应用程序 使用 Apple 的 XCTest 框架自动化 macOS 应用程序 https://github.com/appium/appium-mac2-driver

一个基本的 Appium 测试

本节介绍了使用 Appium 服务器 2 和 Appium Java 客户端的基本测试用例。为了简单起见,我使用了 UiAutomator2 驱动程序和一个模拟的 Android 设备。SUT 将是一个 Web 应用程序,具体来说,是本书中始终使用的练习站点。对 Appium Java 客户端的调用嵌入在其他示例中使用的不同单元测试框架中(即 JUnit 4 和 5、TestNG 和 Selenium-Jupiter)。如往常一样,您可以在示例存储库中找到完整的源代码。运行此测试的要求如下:

  1. 安装 Appium 服务器 2。

  2. 安装 UiAutomator2 驱动程序。

  3. 安装 Android SDK(即 Android 的官方软件开发工具包)。您可以通过在计算机上安装Android Studio来轻松安装此 SDK。

  4. 使用 Android Studio 中的 AVD 管理器创建 Android 虚拟设备(AVD)。图 10-2 显示了打开此工具的菜单选项,图 10-3 显示了测试中使用的虚拟设备(使用 Android API 级别 30 的 Nexus 5 手机)。

  5. 启动虚拟设备和 Appium 服务器。

hosw 1002

图 10-2. Android Studio

hosw 1003

图 10-3. AVD 管理器

如前所述,Appium 服务器是一个 Node.js 应用程序。因此,您需要在系统中安装 Node.js 才能运行 Appium。以下命令概述了如何安装 Appium 服务器 2 和 UiAutomator2 驱动程序,以及如何启动 Appium 服务器:

npm install -g appium@next ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
appium driver install uiautomator2 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
appium --allow-insecure chromedriver_autodownload ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)

1

我们使用 npm(Node.js 的默认软件包管理器)安装 Appium 2。

2

我们使用 Appium 安装 UiAutomator2 驱动程序。

3

我们启动 Appium 服务器(默认情况下,它监听端口 4723)。我们包含一个标志,让 Appium 管理所需的浏览器驱动程序(例如,chromedriver)以自动化 Web 应用程序(就像在 Selenium WebDriver 中一样)。

示例 10-1 展示了使用 Appium Java 客户端进行完整测试的示例。正如你所见,这个测试与本书中解释的常规 Selenium WebDriver 测试非常相似。在这种情况下的主要区别是,我们使用了 AppiumDriver 的一个实例,这是 Appium Java 客户端提供的一个类。该类扩展了 Selenium WebDriver API 的 RemoteWebDriver 类。因此,我们可以利用 Selenium WebDriver API 来测试移动设备上的 Web 应用程序。图 10-4 展示了在此测试期间的模拟移动设备(Nexus 5)。

hosw 1004

图 10-4. Android 设备
示例 10-1. 使用 Appium Java 客户端进行测试
class AppiumJupiterTest {

    WebDriver driver;

    @BeforeEach
    void setup() throws MalformedURLException {
        URL appiumServerUrl = new URL("http://localhost:4723"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        assumeThat(isOnline(new URL(appiumServerUrl, "/status"))).isTrue(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

        ChromeOptions options = new ChromeOptions(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        options.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
        options.setCapability(MobileCapabilityType.DEVICE_NAME,
                "Nexus 5 API 30"); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        options.setCapability(MobileCapabilityType.AUTOMATION_NAME,
                "UiAutomator2"); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)

        driver = new AppiumDriver(appiumServerUrl, options); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)
    }

    @AfterEach
    void teardown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void testAppium() {
        driver.get("https://bonigarcia.dev/selenium-webdriver-java/"); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
        assertThat(driver.getTitle()).contains("Selenium WebDriver");
    }

}

1

我们指定了 Appium 服务器 URL。

2

我们假设使用 Appium 服务器 URL 的端点 /status。如果此 URL 不在线,则跳过测试。

3

我们使用 Chrome 选项来指定功能。

4

使用 Appium 时的第一个强制性功能是平台名称(在本例中为 Android)。

5

以下是设备名称。此名称必须与 AVD 管理器中定义的名称匹配(参见 图 10-3)。

6

最后一个强制性功能是驱动程序名称(在本例中为 UiAutomator2)。

7

我们使用 Appium 服务器 URL 和浏览器选项创建了一个 AppiumDriver 的实例。

8

我们像往常一样使用 driver 对象来执行 SUT。

REST 服务

REST(表现状态转移)是一种用于设计分布式服务的架构风格。Roy Fielding 在他的 2000 年博士论文中创造了这个术语。REST 是在 HTTP 协议之上创建 Web 服务的流行方式。

REST 遵循客户端-服务器架构。服务器处理一组资源,监听客户端发出的传入请求。这些资源是 REST 服务的构建块,并定义了传输的信息类型。每个资源都有唯一的标识。在 HTTP 中,我们使用 URL(也称为端点)来访问单个资源。每个资源都有一个表示形式,即资源当前状态的机器可读说明。我们使用数据交换格式来定义表示,如 JSON、YAML 或 XML。REST 服务公开一组对资源执行的操作,例如 CRUD(创建、检索、更新和删除)。我们可以使用 HTTP 方法(也称为动词)来映射 REST 操作。表 10-3 总结了用于创建 REST 服务的 HTTP 方法。最后,我们可以使用 HTTP 状态码来识别与 REST 操作相关联的响应。表 10-4 总结了在 REST 中使用的典型 HTTP 状态码。图 10-5 显示了一个示例 REST 服务的请求和响应序列,该服务使用了不同的 HTTP 方法和响应代码。

表 10-3. 用于创建 REST 服务的 HTTP 方法

HTTP 方法 描述
GET 读取资源
POST 将新资源发送到服务器
PUT 更新资源
DELETE 删除资源
PATCH 部分更新资源
HEAD 询问给定资源是否存在,而不返回其任何表示形式
OPTIONS 检索给定资源的可用动词

表 10-4. 用于创建 REST 服务的 HTTP 状态码

状态码 描述
200 OK 请求成功,返回请求的内容(例如在 GET 请求中)。
201 Created 已创建资源(例如在 POSTPUT 请求中)。
204 No content 操作成功,但未返回内容。此状态码在不需要响应主体的操作中很有用(例如在 DELETE 请求中)。
301 Moved permanently 资源已移动到另一个位置。
400 Bad request 请求存在问题(例如,缺少参数)。
401 Unauthorized 请求的资源对于发出请求的用户不可访问。
403 Forbidden 资源不可访问,但与 401 不同,身份验证不会影响响应。
404 Not found 提供的端点未标识任何资源。
405 Method not allowed 不允许使用的动词(例如在只读资源中使用 PUT)。
500 Internal server error 服务器端发生一般性意外情况。

hosw 1005

Figure 10-5. REST 服务示例

REST Assured

REST API 是无处不在的。通常情况下,强烈建议为验证这些服务实现自动化测试,例如使用 REST Assured。REST Assured 是一个流行的开源(Apache 2.0 许可)Java 库,用于测试 REST 服务。它提供了一个流畅的 API 用于测试和验证 REST 服务。使用 REST Assured 创建可读断言的便捷方法是生成 POJOs(Plain Old Java Objects),将 REST 响应(例如 JSON 格式)映射为 Java 类。然后,我们可以使用像 AssertJ 这样的库,通过这些 POJOs 的访问器(即 getter 方法)来验证预期条件。示例 Example 10-2 展示了使用这种方法的测试用例。示例 Example 10-3 包含了此测试中使用的 POJO。

Example 10-2. 使用 REST Assured 进行测试
class RestJupiterTest {

    @Test
    void testRest() {
        HttpBinGet get = RestAssured.get("https://httpbin.org/get").then()
                .assertThat().statusCode(200).extract().as(HttpBinGet.class); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

        assertThat(get.getHeaders()).containsKey("Accept-Encoding"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
        assertThat(get.getOrigin()).isNotBlank(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    }

}

1

我们使用 REST Assured 使用 GET HTTP 方法请求在线公共 REST 服务。此行还验证了预期的状态码(200),并将响应有效负载(以 JSON 形式)转换为 Java 类(如示例 Example 10-3 所示)。

2

我们断言头部列表(使用相应的访问器方法)包含给定的键值对。

3

我们断言原点不为空。

Example 10-3. 用于测试 REST 服务的 POJO 类
public class HttpBinGet {

    public Map<String, String> args; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    public Map<String, String> headers;
    public String origin;
    public String url;

    // Getters and setters ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

}

1

此 POJO 定义了一组属性,用于将 JSON 响应有效载荷映射到 Java 中。

2

我们为每个类属性定义访问器(getter)和修改器(setter)。现代 IDE 允许从类属性自动生成这些方法。

Selenium 的替代方案

Selenium 目前是实现端到端测试的领先技术。尽管如此,它并非唯一的可用选择。本节提供了其他框架和库的概述,这些框架和库同样允许为 Web 应用程序实现端到端测试。此外,以下小节还审视了每个这些替代方案的主要优缺点。在我看来,Selenium 仍然是端到端测试的参考解决方案,因为它是为促进 Web 标准(即 W3C WebDriver 和 WebDriver BiDi)而构建,以支持自动化过程,从而确保跨浏览器兼容性。

Cypress

Cypress 是一个 JavaScript 端到端自动化测试框架。如 Figure 10-6 所示,Cypress 架构包括一个 Node.js 进程以及在浏览器中执行的 Test Runner 工具。

hosw 1006

Figure 10-6. Cypress 架构

测试运行器是一个交互式的 Web 应用,它包含基于 Mocha(一个 JavaScript 单元测试框架)的测试和被测试的 Web 应用作为两个 iframe。测试代码和应用程序代码在同一个浏览器标签页中运行(即在同一个 JavaScript 循环中)。Node.js 进程使用 WebSocket 与测试运行器进行通信。最后,Node.js 进程是测试运行器和被测试的 Web 应用之间 HTTP 流量的代理。

Cypress 测试运行器是开源的,根据 MIT 许可证授权。Cypress 团队还提供商业支持以获取高级功能。其中之一是 Cypress Dashboard,一个云管理的 Web 应用,用于跟踪在测试运行器中执行的测试。表格 10-5 总结了 Cypress 的一些最相关的优缺点。

表格 10-5. Cypress 的优缺点

优点 缺点

|

  • 自动等待和快速执行,因为测试和应用程序在同一个浏览器中运行。

  • 实时重载(测试运行器自动跟踪测试中的变化)

|

  • 仅支持一些浏览器:Firefox 和基于 Chromium 的浏览器(包括 Chrome、Edge 和 Electron),但不支持其他浏览器如 Safari 或 Opera。

  • 由于应用程序在浏览器 iframe 中执行,某些操作是不允许的(例如驱动不同的浏览器或多个标签页)。

|

以下命令展示如何在本地安装 Cypress 并执行它。执行完这些命令后,你会看到 Cypress 的 GUI(类似于 图 10-7)。你可以使用这个 GUI 来执行 Cypress 的测试。

npm install cypress ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
npx cypress open ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

1

我们可以使用 npm(Node.js 中的默认包管理器)来安装 Cypress。

2

我们可以使用 npx(一个 npm 包运行器)来运行 Cypress 进程。

hosw 1007

图 10-7. Cypress GUI

在 Cypress GUI 中,默认情况下可以在 1-getting-started2-advanced-examples 文件夹中找到入门测试示例。此外,我们可以使用 New Spec File 按钮创建新的测试。例如,示例 10-4 展示了使用 Cypress 的全新基础测试(即 Cypress 中的 hello world)。这个测试被称为 hello-world-cypress.spec.js(默认在 Mocha 测试中使用 .spec.js 扩展名),并存储在 Cypress 安装路径的 cypress/integration 中。图 10-8 展示了在执行此测试时的 Cypress 测试运行器的截图。

示例 10-4. 使用 Cypress 进行的 Hello world 测试
describe('Hello World with Cypress', () => {
   it('Login in the practice site', () => {
      cy.visit('https://bonigarcia.dev/selenium-webdriver-java/login-form.html') ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)

      cy.get('#username').type('user') ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
      cy.get('#password').type('user')
      cy.contains('Submit').click() ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
      cy.contains('Login successful') ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

      cy.screenshot("hello-world-cypress") ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
  })
})

1

我们在实践站点中打开登录页面。

2

我们输入正确的凭据(用户名和密码)。

3

我们点击提交按钮。

4

我们验证结果页面包含成功登录的消息。

5

我们进行浏览器截图。

hosw 1008

图 10-8. Cypress 测试运行器

WebDriverIO

WebDriverIO是用于 Web 和移动应用程序的自动化测试框架。它完全开源(MIT 许可证),基于诸如 W3C WebDriver 协议之类的 Web 标准。图 10-9 展示了它的架构。WebDriverIO 是用 JavaScript 编写的,运行在 Node.js 上。它使用几个服务来支持自动化:chromedriver(用于本地 Chrome 浏览器)、Selenium Server(用于其他浏览器)、Appium Server(用于移动设备)、Chrome DevTools(用于使用 CDP 的基于 Chromium 的本地浏览器)以及云提供商(如 Sauce Labs、BrowserStack 或 TestingBot)。这些服务操作相应的浏览器和移动设备。表 10-6 总结了 WebDriverIO 的一些优缺点。

hosw 1009

图 10-9. WebDriverIO 架构

表格 10-6. WebDriverIO 的优缺点

优点 缺点

|

  • 支持多个浏览器和移动设备

  • 适用于不同的测试和报告框架

  • 基于 Web 标准

|

  • 仅支持 JavaScript

|

下面的npm命令在本地安装 WebDriverIO。此安装程序显示一个命令行向导,询问几个选项,例如服务(chomedriver、Selenium Server、Appium Server、CDP 或云提供商)、测试框架(Mocha、Jasmine 或 Cucumber)或报告工具(JUnit 或 Allure 等):

npm init wdio .

当前命令完成后,我们可以创建自定义测试。例如,示例 10-5 展示了一个基本的使用 Mocha 的 WebDriverIO 测试。我们将此测试放在项目脚手架的test文件夹下,并通过以下命令运行它:

npx wdio run ./wdio.conf.js
示例 10-5. 使用 WebDriverIO 的 Hello world 测试
describe('Hello World with WebDriverIO', () => {
   it('Login in the practice site', async () => {
      await browser.url(
            `https://bonigarcia.dev/selenium-webdriver-java/login-form.html`);

      await $('#username').setValue('user');
      await $('#password').setValue('user');
      await $('button[type="submit"]').click();
      await expect($('#success')).toHaveTextContaining('Login successful');
      await browser.saveScreenshot('hello-world-webdriverio.png');
    });
});

TestCafe

TestCafe是一个开源的跨浏览器自动化测试工具(MIT 许可证)。TestCafe 的核心思想是避免使用外部驱动程序来支持自动化过程,而是使用混合的客户端-服务器架构模拟用户操作(参见图 10-10)。服务器端采用 Node.js 实现,并包含一个代理,用于拦截与测试中的 Web 应用程序的 HTTP 流量。TestCafe 测试也是作为 Node.js 脚本编写并在服务器端执行的。在浏览器中测试页上运行模拟用户活动的自动化脚本在客户端上运行。表 10-7 总结了 TestCafe 的一些优势和局限性。

hosw 1010

图 10-10. TestCafe 架构

表格 10-7. TestCafe 优缺点

优点 缺点

|

  • 完全跨浏览器支持(由于 TestCafe 仅启动浏览器,因此可以自动化任何浏览器)

|

  • 仅支持 JavaScript 和 TypeScript

  • 由于 JavaScript 的限制,某些操作无法自动化

|

我们可以使用 npm 轻松安装 TestCafe。然后,我们可以使用 TestCafe CLI 工具从命令行运行 TestCafe 脚本。以下代码段说明了如何操作:

npm install -g testcafe ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
testcafe chrome helloworld-testcafe.js ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)

1

我们全局安装 TestCafe。

2

我们启动一个使用 Chrome 作为浏览器的 TestCafe 基本脚本(示例 10-6)。

示例 10-6. 使用 TestCafe 的 Hello world 测试
import { Selector } from 'testcafe';

fixture`Hello World with TestCafe`
   .page`https://bonigarcia.dev/selenium-webdriver-java/login-form.html`;
test('Login in the practice site', async t => {
   await t
      .typeText('#username', 'user')
      .typeText('#password', 'user')
      .click('button[type="submit"]')
      .expect(Selector('#success').innerText).eql('Login successful')
      .takeScreenshot();
});

Puppeteer

Puppeteer 是一个开源(MIT 许可)的 Node.js 库,提供了一个高级 API 来通过 DevTools 协议控制基于 Chromium 的浏览器。Puppeteer 由 Google 的 Chrome DevTools 团队维护。图 10-11 展示了 Puppeteer 的架构。表 10-8 展示了 Puppeteer 的主要优缺点。

hosw 1011

图 10-11. Puppeteer 架构

表格 10-8. Puppeteer 的优缺点

优点 缺点

|

  • 快速执行和全面的自动化能力(由于使用 CDP 与浏览器直接通信)

|

  • 仅支持有限的跨浏览器(仅基于 Chromium 的浏览器,尽管在撰写时有试验性的 Firefox 支持)

  • 仅支持 JavaScript 和 TypeScript

|

我们可以使用 npm 安装 Puppeteer。然后,我们需要使用 Node.js 运行 Puppeteer 测试(例如,示例 10-7)。以下代码段显示了这些命令:

npm install puppeteer
node helloword-puppeteer.js
示例 10-7. 使用 Puppeteer 的 Hello world 测试
const puppeteer = require('puppeteer');

(async () => {
   const browser = await puppeteer.launch(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
   const page = await browser.newPage();

   await page.goto('https://bonigarcia.dev/selenium-webdriver-java/login-form.html');
   await page.type('#username', 'user');
   await page.type('#password', 'user');
   await page.click('button[type="submit"]');
   await page.waitForXPath('//*[contains(text(), "Login successful")]');
   await page.screenshot({ path: 'helloword-puppeteer.png' });

   await browser.close();
})();

1

Puppeteer 默认以无头模式运行浏览器。可以通过将此语句更改为以下内容来配置为使用非无头浏览器:

const browser = await puppeteer.launch({ headless: false });

Playwright

Playwright 是由微软支持的开源(Apache 2.0 许可)的浏览器自动化库。Playwright 最初是一个 Node.js 库。除了 JavaScript,它现在还支持其他编程语言,包括 Python、Java 和 .NET C#。

Playwright 支持三种类型的 Web 引擎:Chromium、Firefox 和 WebKit(即 Safari 使用的 Web 浏览器引擎)。支持这些引擎的想法是它们涵盖了大部分的浏览器市场。因此,Playwright 团队维护了这些浏览器的补丁版本,以公开必要的功能来实现自动化。这些补丁版本提供了一个事件驱动的架构,以访问不同的内部浏览器进程(例如,渲染、网络、浏览器或服务工作进程)。图 10-12 说明了这种架构。表 10-9 包含了 Playwright 的一些最重要的优缺点。

hosw 1012

图 10-12. Playwright 架构

表格 10-9. Playwright 的优缺点

优点 缺点

|

  • 自动等待元素准备就绪

  • 多语言 API

  • 通过记录浏览器中的用户操作来提供测试生成器

  • 允许浏览器会话记录

  • 拦截网络流量以进行存根和模拟

|

  • 使用修补过的浏览器版本而不是实际发布版本

|

要使用 Playwright,我们首先需要安装修补过的浏览器二进制文件。我们可以使用 npm 来实现这一目标。以下命令将下载适用于运行此命令的操作系统(支持 Windows、Linux 和 macOS)的 Chromium、Firefox 和 WebKit 的正确浏览器二进制文件:

npm install -D playwright

然后,我们可以使用一个支持的 API 实现 Playwright 脚本。例如,当使用 JavaScript API 时,我们可以使用第三方测试运行器(例如 Jest、Jasmine、Mocha 等),或者使用 Playwright 测试(即由 Playwright 团队提供的测试运行器)。要使用后者,我们需要按照以下方式安装它:

npm install -D @playwright/test

示例 10-8 包含一个基本的 Playwright JavaScript 测试,可以使用 Playwright 运行器执行。这个命令假设这个测试(称为 helloworld-playwright.spec.mjs)位于 tests 目录下。我们可以像下面的片段所示调用 Playwright 运行器来运行这个测试。这个命令默认以无头模式运行 Playwright 测试。要在非无头模式下运行浏览器,您需要在命令的末尾包含 --headed 标志:

npx playwright test
示例 10-8. 使用 Playwright 进行的 Hello world 测试
const { test, expect } = require('@playwright/test');

test('Hello World with Playwright', async ({ page }) => {
   await page.goto('https://bonigarcia.dev/selenium-webdriver-java/login-form.html');

   await page.type('#username', 'user');
   await page.type('#password', 'user');
   await page.click('button[type="submit"]');
   await expect(page.locator('#success')).toHaveText('Login successful');

   await page.screenshot({ path: 'helloworld-playwright.png' });
});

摘要和最终说明

Web 开发是一个涉及许多不同技术的异构学科,例如客户端、服务器端或者与外部服务集成等。因此,本章介绍了两种对 Selenium 有帮助的补充技术,用于测试 Web 应用程序:Appium(用于移动应用程序的开源测试自动化框架)和 REST Assured(用于测试 REST 服务的开源 Java 库)。您还学习了用于实现 Web 应用程序端到端测试的替代工具的基础知识,包括 Cypress、WebDriverIO、TestCafe、Puppeteer 和 Playwright。虽然这些替代方案与 Selenium 相比具有显著的优势(例如自动等待),但在我看来,由于 Selenium 是基于 Web 标准构建的(例如 W3C WebDriver 和 WebDriver BiDi),因此提供了更全面的自动化模型。此外,Selenium 项目还积极参与这些规范的开发。

本章节总结了你通过使用 Selenium 开发端到端测试的旅程。下一步是将本书中提供的所有知识付诸实践到你的项目中。这样,你可以为你的团队、项目、公司等构建定制的自动化框架。你需要做出许多决策,比如项目设置(如 Maven、Gradle)、单元测试框架(如 JUnit、TestNG)、浏览器基础设施(如 Docker、云服务提供商)以及与第三方工具的集成。为了应对所有这些复杂性,作为最后一句话,我建议你在本书中提供的示例中进行实践。换句话说:克隆仓库,运行测试,并编辑代码以满足你的需求。书籍发布后,我会维护 GitHub 仓库。还要记住:这是一个开源软件项目,所以如果你想做出贡献,随时可以提交 pull request 来改进它。

附录 A. Selenium 4 新功能简介

本附录总结了 Selenium 4 中可用的新功能。内容的目的有两个:首先,它列举了 Selenium 套件核心组件(即 WebDriver、Driver 和 IDE)中的新功能,并提供了解释每个方面的书籍章节的链接。此外,本附录还描述了 Selenium 4 中随 Selenium 3 到 4 迁移时变化的其他方面,例如文档和治理。第二个目标是识别从 Selenium 3 到 4 迁移时废弃部分及相应的新功能。

Selenium WebDriver

Selenium WebDriver 4.0.0 的第一个稳定版本于 2021 年 10 月 13 日发布。表 A-1 总结了此版本与上一个稳定版本(即 Selenium WebDriver 3.141.59)相比最重要的新功能。

表 A-1. Selenium WebDriver 4 中的新功能

功能 描述 章节 部分
完全采纳 W3C WebDriver Selenium WebDriver API 和驱动程序之间的标准通信协议 第一章 “Selenium WebDriver”
相对定位器 基于其他网页元素的接近程度的定位策略 第三章 “相对定位器”
固定脚本 将一段 JavaScript 附加到 WebDriver 会话 第四章 “固定脚本”
元素截图 捕获网页元素的截图(而不是整个页面) 第四章 “WebElement 截图”
影子 DOM 无缝访问影子树 第四章 “影子 DOM”
打开新窗口和标签页 改进的导航到不同窗口和标签页的方式 第四章 “标签页和窗口”
装饰器 用于实现事件监听器的 WebDriver 对象包装器 第四章 “事件监听器”
Chrome DevTools 协议 在基于 Chromium 的浏览器中(如 Chrome 和 Edge)原生访问 DevTools 第五章 “Chrome DevTools 协议”
网络拦截 针对后端请求进行存根处理并拦截网络流量 第五章 “网络拦截器”
基本身份验证 简化的基本和摘要身份验证 API 第五章 “基本和摘要身份验证”
全页面截图 捕获网页内容的完整截图 第五章 “全页面截图”
位置上下文 模拟地理位置坐标 第五章 “位置上下文”
将网页打印为 PDF 将网页保存为 PDF 文档 第五章 “打印页面”
WebDriver BiDi 驱动程序和浏览器之间的双向通信 第五章 “WebDriver BiDi”

迁移指南

本节总结了将使用 Selenium WebDriver 3 的现有代码库迁移到版本 4 所需的更改。

定位器

用于查找元素的实用方法(FindsBy 接口)在 Selenium WebDriver 4 中已移除。表 A-2 比较了 Selenium WebDriver 中查找 Web 元素的旧 API 和新 API。有关此功能的更多详细信息,请参见 “定位 Web 元素”。

表 A-2. Selenium WebDriver 4 中网页元素位置的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

driver.findElementByTagName("tagName");
driver.findElementByLinkText("link");
driver
    .findElementByPartialLinkText("partLink");
driver.findElementByName("name");
driver.findElementById("id");
driver.findElementByClassName("class");
driver.findElementByCssSelector("css");
driver.findElementByXPath("xPath");

|

driver.findElement(By.tagName("tagName"));
driver.findElement(By.linkText("link"));
driver.findElement(By
    .partialLinkText("partLink"));
driver.findElement(By.name("name"));
driver.findElement(By.id("id"));
driver.findElement(By.className("class"));
driver.findElement(By.cssSelector("css"));
driver.findElement(By.xpath("xPath"));

|

用户手势

Actions 允许模拟复杂的用户手势(如拖放、悬停、鼠标移动等)。正如表 A-3 中展示的,这个类暴露的 API 在 Selenium WebDriver 4 中已经简化。关于这个类的更多信息和示例,请参见 “用户手势”。

表 A-3. Selenium WebDriver 4 中 Actions 类的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

actions.moveToElement(webElement).click();
actions.moveToElement(webElement).doubleClick();
actions.moveToElement(webElement).contextClick();
actions.moveToElement(webElement).clickAndHold();
actions.moveToElement(webElement).release();

|

actions.click(webElement);
actions.clickAndHold(webElement);
actions.contextClick(webElement);
actions.doubleClick(webElement);
actions.release(webElement);

|

等待和超时

指定超时的参数从 TimeUnit 切换到 Duration。表 A-4 中描述了这一变化。您可以在 “等待策略” 和 “超时” 中找到更多细节。

表 A-4. Selenium WebDriver 4 中等待和超时的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

new WebDriverWait(driver, 3).
    until(ExpectedConditions.
    elementToBeClickable(By.id("id")));

Wait<WebDriver> wait =
    new FluentWait<WebDriver>(driver)
    .withTimeout(30, TimeUnit.SECONDS)
    .pollingEvery(5, TimeUnit.SECONDS)
    .ignoring(NoSuchElementException.class);

driver.manage().timeouts()
    .implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().timeouts()
    .setScriptTimeout(3, TimeUnit.MINUTES);
driver.manage().timeouts()
    .pageLoadTimeout(30, TimeUnit.SECONDS);

|

new WebDriverWait(driver,
    Duration.ofSeconds(3)).until(
    ExpectedConditions.
    elementToBeClickable(By.id("id")));

Wait<WebDriver> wait =
     new FluentWait<WebDriver>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofSeconds(5))
    .ignoring(NoSuchElementException.class);

driver.manage().timeouts()
    .implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts()
    .scriptTimeout(Duration.ofMinutes(3));
driver.manage().timeouts()
    .pageLoadTimeout(Duration.ofSeconds(30));

|

事件监听器

在 Selenium WebDriver 3 中,我们使用 EventFiringWebDriver 类来创建事件监听器。在 Selenium WebDriver 4 中,这个类已被弃用,建议改用 EventFiringDecorator 类。表 A-5 概述了这一变化。关于此功能的完整示例,请参见 “事件监听器”。

表 A-5. Selenium WebDriver 4 中事件监听器的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

EventFiringWebDriver newDriver =
    new EventFiringWebDriver(originalDriver);
wrapper.register(myListener);

|

WebDriver newDriver =
    new EventFiringDecorator(myListener)
    .decorate(originalDriver);

|

功能

选择不同浏览器类型的静态方法在 Selenium WebDriver 4 中已移除。相反,我们应该使用特定于浏览器的选项。表 A-6 概述了这一变化。关于这个特性的更多细节,请参见 “浏览器能力”。

表 A-6. Selenium WebDriver 4 中期望能力的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

DesiredCapabilities caps =
    DesiredCapabilities.chrome();
DesiredCapabilities caps =
    DesiredCapabilities.edge();
DesiredCapabilities caps =
    DesiredCapabilities.firefox();
DesiredCapabilities caps =
    DesiredCapabilities.internetExplorer();
DesiredCapabilities caps =
    DesiredCapabilities.safari();
DesiredCapabilities caps =
    DesiredCapabilities.chrome();

|

ChromeOptions options =
    new ChromeOptions();
EdgeOptions options =
    new EdgeOptions();
FirefoxOptions options =
    new FirefoxOptions();
InternetExplorerOptions options =
    new InternetExplorerOptions();
SafariOptions options =
    new SafariOptions();
ChromeOptions options =
    new ChromeOptions();

|

然后,基于能力的 WebDriver 构造函数已弃用,而非特定于浏览器的选项。表 A-7 展示了此过程的示例。您可以在 “浏览器能力” 中找到使用此构造函数的示例。

表 A-7. Selenium WebDriver 4 中使用能力进行 WebDriver 实例化的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

WebDriver driver = new ChromeDriver(caps);

|

WebDriver driver = new ChromeDriver(options);

|

此外,Selenium WebDriver 4 中的能力合并方式发生了变化。在 Selenium WebDriver 3 中,可以通过改变调用对象来合并能力。而在 Selenium WebDriver 4 中,需要分配合并操作。表 A-8 提供了这一变化的示例。

表 A-8. Selenium WebDriver 4 中合并能力的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

MutableCapabilities caps =
    new MutableCapabilities();
caps.setCapability("platformName",
    "Linux");
ChromeOptions options =
    new ChromeOptions();
options.setHeadless(true);
options.merge(caps);

|

MutableCapabilities caps =
    new MutableCapabilities();
caps.setCapability("platformName",
    "Linux");
ChromeOptions options =
    new ChromeOptions();
options.setHeadless(true);
options = options.merge(caps);

|

最后,BrowserType 接口已弃用,转而采用新的 Browser 接口。表 A-9 显示了指定能力时这些接口之间的区别。有关此方面的更多细节,请参阅 “创建 RemoteWebDriver 对象”。

表 A-9. Selenium WebDriver 4 中能力的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

caps.setCapability("browserName",
    BrowserType.CHROME);

|

caps.setCapability("browserName",
    Browser.CHROME);

|

Selenium Grid

Selenium Grid 4 已完全重写。新代码库基于 Selenium Grid 3 的所有经验教训,提高了其源代码的可维护性。Selenium Grid 4 支持经典模式,即独立和中心节点架构,同时提供了完全分布式架构以改善其整体性能和可扩展性。最后,Selenium Grid 4 使用 Docker、Kubernetes 或使用 OpenTelemetry 进行分布式跟踪等现代基础设施技术。

“Selenium Grid” 介绍了 Selenium Grid。然后,“Selenium Grid” 解释了 Selenium Grid 不同模式(即独立、中心节点和完全分布式)的详细信息,以及如何在 Selenium WebDriver 测试中使用它。

迁移指南

当作为 Java 依赖项使用 Selenium Grid 并升级到版本 4 时,除了升级版本外,还需知道 Selenium Grid 4 中项目坐标的变化。以前的 artifactIdselenium-server,在 Selenium Grid 4 中变为 selenium-grid。表 A-10 包含了 Maven 和 Gradle 中 Selenium Grid 4 的新坐标。

表 A-10. 将 Selenium Grid 4 作为 Maven 和 Gradle 依赖项的迁移

Selenium WebDriver 3 Selenium WebDriver 4

|

<dependency>
 <groupId>org.seleniumhq.selenium</groupId>
 <artifactId>selenium-server</artifactId>
 <version>3.141.59</version>
</dependency>

|

<dependency>
 <groupId>org.seleniumhq.selenium</groupId>
 <artifactId>selenium-grid</artifactId>
 <version>4.0.0</version>
</dependency>

|

|

testImplementation("org.seleniumhq.selenium:
 selenium-server:3.141.59")

|

testImplementation("org.seleniumhq.selenium:
 selenium-grid:4.0.0")

|

Selenium IDE

Selenium IDE 在 “Selenium IDE” 中引入。截至 Selenium 4,一些新功能包括:

备份元素选择器

Selenium IDE 为每个元素记录多个定位器(例如按 id、XPath 或 CSS 选择器),因此,如果测试执行未能使用第一个定位器找到元素,它会依次尝试后续的定位器直到找到为止。

控制流

Selenium IDE 通过条件语句(例如ifelseelse ifend)和循环(例如dowhiletimesforEach)增强了脚本执行。

代码导出

Selenium IDE 允许将录制结果导出为多种 Selenium WebDriver 绑定语言(即 C#、Java、JavaScript、Python 和 Ruby)和单元测试框架(即 NUnit、xUnit、JUnit、Mocha、pytest 和 RSpec)。

插件

我们可以通过自定义插件扩展 Selenium IDE(例如引入新的命令或第三方集成)。

其他新功能

Selenium 4 大大改进了官方的 Selenium 文档。新网站位于https://www.selenium.dev,内容涵盖了 Selenium 的子项目(WebDriver、IDE 和 Grid)、用户指南、博客、支持和其他项目信息。

最后,自 2009 年以来一直担任 Selenium 项目联合创始人和项目负责人的 Simon Stewart,于 2021 年 10 月 27 日辞去了 Selenium 项目的领导职务。你可以在Selenium 网站上找到当前的项目结构(包括项目领导委员会、技术领导委员会和 Selenium 贡献者及触发器)和治理(即模型、哲学、项目角色、决策过程等)。

附录 B. 驱动程序管理

正如在第一章讨论的那样,驱动程序管理涉及三个步骤:下载、设置和维护。手动驱动程序管理在努力方面昂贵,并且在可维护性方面可能存在问题。因此,在本书的所有示例中,我使用 WebDriverManager 来以自动化和自动维护的方式执行这个过程。为了完整起见,本附录还描述了手动驱动程序管理中涉及的步骤(下载、设置和维护)。

WebDriverManager:自动化驱动程序管理

WebDriverManager 是一个开源的 Java 库,用于自动管理 Selenium WebDriver 所需的驱动程序(例如 chromedriver、geckodriver、msedgedriver 等)。WebDriverManager 为不同的浏览器提供一组管理器,包括 Chrome、Firefox、Edge、Opera、Chromium 和 Internet Explorer。

WebDriverManager 在内部执行一个解析算法来管理每个浏览器所需的驱动程序。该算法旨在自动发现、下载、设置和维护这些驱动程序。

图 B-1 在 WebDriverManager 实现的方法论背景下代表了这个算法。对于每个管理器(例如,chromedriver()firefoxdriver()等),解析算法的工作原理如下:

  1. WebDriverManager 尝试查找安装在本地计算机上的浏览器版本(例如 Chrome)。为此,它使用一个名为命令数据库的内部知识数据库。该数据库包含一组 Shell 命令(在不同操作系统上),允许发现浏览器版本(例如,在 Linux 上使用google-chrome --version)。

  2. 使用找到的主要浏览器版本(例如,Chrome 89),WebDriverManager 确定正确的驱动程序版本(例如,chromedriver 89.0.4389.23)。我称这个过程为版本解析。为了简化这个过程,几个驱动程序维护者(例如 chromedriver 和 msedgedriver)在其在线存储库中发布特定的驱动程序版本信息,使用简单的文本文件(例如,https://chromedriver.storage.googleapis.com/LATEST_RELEASE_89)。不幸的是,对于 geckodriver 或 operadriver 等其他驱动程序,这些信息是不可用的。因此,WebDriverManager 使用另一个内部知识数据库(称为版本数据库)来保持浏览器版本和驱动程序之间的关联。这两个版本和命令数据库通过存储在 GitHub 上的在线主参考进行值的同步。

  3. WebDriverManager 下载特定的驱动程序,适用于本地操作系统(Windows、Linux 或 macOS),并将其存储在本地文件系统中的驱动程序缓存中(默认情况下在路径~/.cache/selenium)。

  4. 最后,WebDriverManager 使用适当的 Java 系统属性(例如,webdriver.chrome.driver)导出已下载的驱动程序路径。

出于性能和可维护性考虑,WebDriverManager 在内部使用解析缓存。该缓存(默认情况下存储为属性文件)保持了解决驱动程序版本之间的关系。此关系遵循存活时间(TTL)方法有效。默认情况下,驱动程序的 TTL 为一天(例如 chromedriver 89.0.4389.23),浏览器的 TTL 为一小时(例如 Chrome 89)。解析算法在后续调用中使用缓存文件来解决驱动程序(通常在 Selenium WebDriver 测试套件中发生)。然后,当 TTL 过期时,解析算法尝试解析新的驱动程序发布。最后,当检测到不同的浏览器版本时,WebDriverManager 会下载新的驱动程序(如果需要)。通过此过程,即使对于常青树浏览器,也可以确保浏览器和驱动程序的版本兼容性。

hosw ab01

图 B-1. WebDriverManager 方法论

通用管理器

除了特定于浏览器的管理器(例如chromedriver()firefoxdriver()等),WebDriverManager 还提供了通用管理器,即可以参数化为充当特定管理器(例如 Chrome、Firefox 等)的管理器。通过 WebDriverManager API 的getInstance()方法可以使用此功能。有多种调用此方法的选项:

getInstance(Class<? extends WebDriver> webDriverClass)

其中webDriverClassWebDriver层次结构的类,如ChromeDriver.classFirefoxDriver.class等。

getInstance(DriverManagerType driverManagerType)

其中driverManagerType是由 WebDriverManager 提供的枚举,用于识别可用的管理器。该枚举的可能值为CHROMEFIREFOXEDGEOPERACHROMIUMIEXPLORERSAFARI

getInstance(String browserName)

其中browserName是不区分大小写的浏览器名称字符串。可能的值包括ChromeFirefoxEdgeOperaChromiumIExplorerSafari

getInstance()

当未指定参数时,将使用配置键wdm.defaultBrowser来选择管理器(默认为 Chrome)。

高级配置

WebDriverManager 提供了不同的配置方式。首先,您可以通过每个管理器使用其 Java API。此 API 允许连接多个方法以指定自定义选项或首选项。您可以在其文档中找到 WebDriverManager API 的完整描述。例如,以下命令显示如何为网络连接设置代理:

WebDriverManager.chromedriver().proxy("server:port").setup();

配置 WebDriverManager 的第二种方式是使用 Java 系统属性。每个 WebDriverManager API 方法都有一个相应的配置键。例如,API 方法 cachePath()(用于指定驱动程序缓存文件夹)的工作方式与配置键 wdm.cachePath 相同。例如,可以通过命令行传递这些类型的配置键:

mvn test -Dwdm.cachePath=/custom/path/to/driver/cache

最后,您还可以使用环境变量来配置 WebDriverManager。变量名称派生自每个配置键(例如,wdm.cachePath),将其转换为大写,并用下划线替换符号 .(例如,WDM_CACHEPATH)。此机制可以方便地在操作系统级别配置全局参数。

其他用途

除了作为 Java 依赖项外,WebDriverManager 还可以以其他方式使用,即:

作为命令行界面(CLI)工具

此模式允许您解析驱动程序(例如,chromedriver、geckodriver)。此外,此模式还允许您在 Docker 容器中执行浏览器,并通过远程桌面会话与其交互。

作为服务器

WebDriverManager 服务器基于 HTTP,并提供两种类型的服务。首先,它公开了一个简单的类似 REST 的 API 来解析驱动程序。其次,它作为常规 Selenium 服务器运行,因此您可以与不同于 Java 的语言绑定一起使用它。

作为 Java 代理

在这种情况下,并使用 JVM 插装 API,WebDriverManager 使用 Java 插装 API 检查在 JVM 中创建的对象。当实例化 WebDriver 对象(ChromeDriverFirefoxDriver 等)时,将使用所需的管理器来解析其驱动程序(chromedrivergeckodriver 等)。由于此方法,您可以从测试中移除 WebDriverManager 的调用。

手动驱动管理

本节描述了如何手动实现驱动程序管理过程(下载、设置和维护)。

下载

驱动管理的第一步是下载适当的驱动程序。表 B-1 显示了获取主要浏览器驱动程序的网站。您需要为您计划使用的浏览器找到正确的驱动程序版本和平台(Windows、Linux、macOS)。关于版本,Chrome 和 Edge(但不幸的是,不包括 Firefox)的维护者遵循相同的驱动程序和浏览器版本号方案,以简化此过程。例如,如果您使用 Chrome 或 Edge 91.x,您还需要使用 chromedriver 和 msedgedriver 91.x。您可以在网站提供的文档中找到具体的驱动程序版本。例如,要使用 Chrome 91,您需要下载 ChromeDriver 91.0.4472.19。

表 B-1. 设置驱动程序的 Java 系统属性

浏览器 驱动程序 下载网站
Chrome/Chromium chromedriver https://chromedriver.chromium.org/downloads
Edge msedgedriver https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver
Firefox geckodriver https://github.com/mozilla/geckodriver/releases

设置

当您为 WebDriver 脚本准备好所需的驱动程序后,需要正确设置它们。有两种方法可以进行此过程。第一种方法是将驱动程序位置(完整路径或包含驱动程序的父文件夹)添加到您的PATH环境变量中(env)。PATH环境变量在类 Unix 系统(如 Linux 和 macOS)和 Windows 操作系统中是标准的。该环境变量允许指定操作系统定位可执行程序的一组文件夹。我们配置PATH(以及其他环境变量)的方式取决于具体的操作系统。例如,在 Windows 系统中,可以通过其 GUI 进行配置(控制面板 → 系统 → 高级 → 环境变量)。在类 Unix 系统中,我们可以使用命令行来执行此过程,例如使用以下命令(或等效命令):

export PATH=$PATH:/path/to/drivers >> ~/.profile

设置驱动程序的第二种方法是使用Java 系统属性,这些属性是以名称/值形式传递给 JVM 的配置属性。表 B-2 总结了 Selenium WebDriver 中主要驱动程序的名称。这些属性的值是给定驱动程序的完整路径(例如/path/to/drivers/chromedriver)。

表 B-2. 设置驱动程序的 Java 系统属性

Browser Driver Java 系统属性名称
Chrome/Chromium chromedriver webdriver.chrome.driver
Edge msedgedriver webdriver.edge.driver
Firefox geckodriver webdriver.gecko.driver

配置这些属性有两种方法:命令行(使用-Dname=value语法传递系统属性)或 Java 代码。例如,示例 B-1 展示了使用 Maven 和 Gradle 命令来执行给定项目的所有测试,并传递属性以设置 Chrome、Edge 和 Firefox 的驱动程序的方法。然后,示例 B-2 展示了如何进行相同的配置,但这次使用 Java 语言。

示例 B-1. Maven 和 Gradle 命令以在命令行中配置系统属性
mvn test -Dwebdriver.chrome.driver=/path/to/drivers/chromedriver
mvn test -Dwebdriver.edge.driver=/path/to/drivers/msedgedriver
mvn test -Dwebdriver.gecko.driver=/path/to/drivers/geckodriver

gradle test -Dwebdriver.chrome.driver=/path/to/drivers/chromedriver
gradle test -Dwebdriver.edge.driver=/path/to/drivers/msedgedriver
gradle test -Dwebdriver.gecko.driver=/path/to/drivers/geckodriver
示例 B-2. Java 命令以配置系统属性
System.setProperty("webdriver.chrome.driver", "/path/to/drivers/chromedriver");
System.setProperty("webdriver.edge.driver", "/path/to/drivers/msedgedriver");
System.setProperty("webdriver.gecko.driver", "/path/to/drivers/geckodriver");

维护

最后但同样重要的是,驱动程序管理的最后一步是维护这些驱动程序。由于 Chrome、Edge 或 Firefox 等持续更新的浏览器会自动进行升级,因此这种维护是必要的。尽管从用户角度来看自动升级很吸引人,但对于手动管理驱动程序的 Selenium WebDriver 脚本来说,这种自动化升级会带来问题。在这种情况下,驱动程序和浏览器的兼容性长期无法保证。

特定的驱动程序(例如,chromedriver 版本 84.0.4147.30)通常与特定的浏览器版本兼容(例如,Chrome 84)。但是,由于自动升级,这种兼容性并不保证。因此,基于这种驱动程序的 Selenium WebDriver 脚本可能会停止工作(即测试被称为失败)。在实践中,当由于驱动程序与浏览器不兼容而导致测试失败时,Selenium WebDriver 开发人员经常会遇到这个问题。例如,在使用 Chrome 作为浏览器时,由于驱动程序不兼容而导致的测试失败会报告以下错误消息:“this version of chromedriver only supports Chrome version N”(其中 N 是特定版本 chromedriver 支持的最新 Chrome 版本)。为了说明这个问题,图 B-2 显示了这个错误消息在 2019 年和 2020 年期间在 Google 上的全球搜索兴趣,以及该时期不同 Chrome 版本的发布日期。正如您所见,关于这个错误消息的兴趣随时间变化与某些 Chrome 版本的发布相关联。

hosw ab02

图 B-2. “this version of chromedriver only supports chrome version” 在 Google 趋势中的全球相对搜索兴趣随时间变化的图表,以及 2019 年和 2020 年 Chrome 的发布日期

摘要

Selenium WebDriver 是一个允许你以编程方式控制 web 浏览器的库。自动化基于每个浏览器的本地能力。因此,我们需要在使用 Selenium WebDriver API 的脚本/测试和浏览器之间放置一个依赖于平台的二进制文件,称为驱动程序。一些驱动程序的例子包括 chromedriver(用于 Chrome)、geckodriver(用于 Firefox)和 msedgedriver(用于 Edge)。本附录介绍了驱动程序管理过程。此过程包括三个步骤(下载、设置和维护),可以手动或自动完成。默认情况下,我建议您使用自动化驱动程序管理方法。为此,Java 中的参考工具是 WebDriverManager。

附录 C. 示例仓库设置

示例仓库 是本书的重要组成部分,因为它包含了所有涵盖的示例以及 Maven 和 Gradle 的完整配置。此外,此仓库使用 GitHub 提供的多项服务,如:

GitHub Pages

一个允许从 GitHub 仓库直接配置公共网站的服务。我使用一个简单的网站链接到示例仓库,展示用作 Selenium WebDriver 测试示例中 SUT 的网页:https://bonigarcia.dev/selenium-webdriver-java。正如您所见,它包含了使用 Bootstrap 作为 CSS 框架的不同 HTML 页面。

GitHub Actions

用于 GitHub 仓库的 CI/CD 构建服务器。我使用此服务在每次新提交时构建和测试整个仓库。您可以在本节末尾看到有关工作流配置的详细信息。

Dependabot

一个自动更新项目依赖的机器人。当此机器人检测到任何 Maven 和 Gradle 依赖的新版本(有关更多详情,请参见下一小节)时,它将创建相应的拉取请求。

在本附录的其余部分,您将找到示例仓库的配置详细信息。此配置包括 Maven 和 Gradle 依赖声明以及其他方面,并且对于使用 Selenium WebDriver 的标准项目应该是足够的。此外,本附录的最后部分解释了如何配置日志库、Dependabot 和 GitHub Actions(以 CI 方法构建和测试项目)。

项目布局

图 C-1 显示了示例仓库布局的示意图。

hosw ac01

图 C-1. 示例仓库布局(托管在 GitHub 上)

由于我为每个示例提供了四种风格(JUnit 4、JUnit 5、JUnit 5 加 Selenium-Jupiter 和 TestNG),因此 Maven 和 Gradle 中的配置基于 多项目。这样,示例仓库就有四个模块,每个测试框架一个:selenium-webdriver-junit4selenium-webdriver-junit5selenium-webdriver-junit5-seljupselenium-webdriver-testng。在 Maven 中,多项目设置位于根文件夹中的 pom.xml 中,在 Gradle 中则位于 settings.gradle 文件中。

如你所见,在图 C-1 中,每个模块都有相同的结构。你可以在src/test/java文件夹中找到测试源代码。我使用 Java 包将示例按章节分开(例如,io.github.bonigarcia.webdriver.jupiter.ch02.helloworld)。接下来,每个项目都需要自己的 Logback 配置文件。我使用通用配置文件(即logback.xml),放置在src/main/resources文件夹下。我遵循这种约定,因为通常也会将日志记录用于应用程序,并且如果你计划重用这个项目结构,这是标准做法。最后,在每个子项目的根目录下,你可以找到用于 Maven(pom.xml)和 Gradle(build.gradle)的特定配置文件。你可以在这些文件中找到依赖项的声明,如下一节所述。

Maven

Maven 中的核心概念之一是构建生命周期,这是指构建和分发特定项目的过程。在 Maven 中有三个标准的构建生命周期:default(用于项目部署),clean(用于项目清理)和site(用于文档)。这些构建生命周期包含一系列构建阶段,每个阶段代表生命周期中的一个阶段。default生命周期的主要阶段包括:

validate

确保项目正确并且所有必要信息都可用。

compile

编译源代码。

test

使用单元测试框架执行测试。

package

将编译后的代码打包成可分发的格式,如 Java ARchive(JAR)文件。

verify

执行进一步的测试(通常是集成或其他高级测试)。

install

将包安装到本地仓库。

deploy

将包安装到远程仓库或服务器。

我们可以使用 Shell 来调用 Maven,使用命令mvn。例如,以下命令调用clean生命周期(即清理target文件夹及其所有内容),然后按顺序调用default生命周期的所有阶段直到package(即validatecompiletest,最后是package):

mvn clean package

Maven 中的另一个核心元素是插件的概念。插件是一个内置的工件,用于执行上述阶段。在本书中,我们特别关注测试。因此,我们着重介绍testverify阶段及其对应的插件:maven-surefire-pluginmaven-failsafe-plugin。表 C-1 总结了这两个插件之间的主要差异。

表 C-1. Surefire 和 Failsafe Maven 插件之间的差异

maven-surefire-plugin maven-failsafe-plugin
描述 在打包前执行测试的 Maven 插件 在打包后执行测试的 Maven 插件
经典用法 单元测试 集成(和其他高级)测试
基本命令 mvn test mvn verify
类型 默认插件(即,我们可以在 pom.xml 中不声明而使用) 非默认插件(即,我们需要在 pom.xml 中声明才能使用)
使用的版本 Maven 内部定义的版本 最新可用版本

| 测试名称模式 | **/Test*.java **/*Test.java

**/*Tests.java

**/*TestCase.java | **/IT*.java **/*IT.java

**/*ITCase.java |

为简单起见,我在示例库中仅使用 maven-surefire-plugin 执行测试。尽管这些测试不是单元测试(事实上,它们是端到端测试),但使用 maven-surefire-plugin 运行它们不成问题(即,在编译后和打包前)。表 C-2 总结了使用此插件从 shell 中运行测试的基本命令。

表 C-2. 使用 maven-surefire-plugin 运行测试的基本命令

命令 描述
mvn test 运行项目中的所有测试
mvn test -Dtest=MyClass 运行单个类中的所有测试
mvn test -Dtest=MyClass#myMethod 运行单个类中的单个测试

但是,如果您想要使用 maven-failsafe-plugin 执行测试,您需要在 pom.xml 文件中使用 示例 C-1 中展示的设置。最后,您可以使用命令 mvn verify 运行测试(即,在打包后运行测试)。

示例 C-1. 使用 maven-failsafe-plugin 执行测试所需的 Maven 配置
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

公共设置

示例 C-2 包含示例库中 Maven 配置的公共部分。

示例 C-2. 示例库中的常见 Maven 依赖项
<properties>
    <java.version>1.8</java.version> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <maven.compiler.source>${java.version}</maven.compiler.source>
</properties>

<dependencies> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>

    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>${selenium.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>${assertj.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>${wdm.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
        </plugin>
    </plugins>
</build>

1

在本项目中我们使用 Java 8。

2

我们指定了常见的依赖项。一方面,我们声明 Selenium WebDriver、AssertJ 和 WebDriverManager,并将其范围设置为 test。这样,我们只能从测试逻辑中(即位于 src/test/java 文件夹下的 Java 类)使用这些依赖项。另一方面,缺少 Simple Logging Facade for Java (SLF4J) 和 Logback 的范围,默认情况下 Maven 使用 compile。这意味着我们可以从应用程序和测试逻辑中使用这些依赖项。最后,请注意我们使用 Maven 属性来声明依赖项的版本(例如 ${selenium.version})。您可以在在线库中找到确切的版本。

3

我们需要声明特定版本的 maven-surefire-plugin。如 表 C-1 所述,此插件的版本由 Maven 内部定义。但为了充分利用此插件,我们需要指定一个更新的版本。

JUnit 4

在一个使用 JUnit 4 作为单元测试框架的 Maven 项目中,我们还需要声明以下依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit4.version}</version>
    <scope>test</scope>
</dependency>

JUnit 5

虽然 JUnit 5 是一个模块化框架,但我们可以在 Maven 项目中声明单个依赖项来使用 Jupiter 编程模型。如下片段所示,这个构件被称为junit-jupiter,并且它传递地拉取以下 JUnit 5 的构件:

junit-jupiter-api

用于开发测试

junit-jupiter-engine

用于在 JUnit 平台中执行测试

junit-jupiter-params

用于开发参数化测试(参见第八章)

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>${junit5.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Selenium-Jupiter

当与 Selenium-Jupiter 一起使用 Jupiter 时,除了以前的构件(junit-jupitermaven-surefire-plugin),我们还需要包含 Selenium-Jupiter 的坐标(请参见下一个代码示例)。在这种情况下,我们可以移除 WebDriverManager 的坐标,因为 Selenium-Jupiter 会传递地拉取它。

<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>selenium-jupiter</artifactId>
    <version>${selenium-jupiter.version}</version>
    <scope>test</scope>
</dependency>

TestNG

最后,在我们的pom.xml中包含 TestNG 所需的坐标是:

<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>${testng.version}</version>
    <scope>test</scope>
</dependency>

虽然在示例仓库中没有使用,但 TestNG 测试也可以在 JUnit 平台上执行。如果要启用此模式,需要在项目设置中添加 JUnit 平台的 TestNG 引擎。有关此信息,请参阅TestNG 引擎页面

其他依赖项

本书解释了与 Selenium WebDriver 结合使用的其他依赖项。表 C-3 总结了这些依赖项及其所在的章节。

表 C-3. 示例仓库中用于第三方集成的依赖项

Dependency Chapter groupId artifactId
HtmlUnitDriver 第一章 org.seleniumhq.selenium htmlunit-driver
Selenium Grid 第六章 org.seleniumhq.selenium selenium-grid
rerunner-jupiter 第八章 io.github.artsok rerunner-jupiter
JUnit 平台启动器 第八章 org.junit.platform junit-platform-launcher
Awaitility 第九章 org.awaitility awaitility
BrowserMob 第九章 net.lightbody.bmp browsermob-core
OWASP ZAP 客户端 API 第九章 org.zaproxy zap-clientapi
Axe Selenium 集成 第九章 com.deque.html.axe-core selenium
Selenide 第九章 com.codeborne selenide
JavaFaker 第九章 com.github.javafaker javafaker
Extent Reports 第九章 com.aventstack extentreports

| Allure | 第九章 | io.qameta.allure | io.qameta.allure allure-junit5

allure-testng |

Cucumber Java 第九章 io.cucumber cucumber-java

| Cucumber JUnit 4, 5 或 TestNG | 第九章 | io.cucumber | cucumber-junit cucumber-junit-platform-engine

cucumber-testng |

Spring-Boot Web 第九章 org.springframework.boot spring-boot-starter-web
Spring-Boot 测试 第九章 org.springframework.boot spring-boot-starter-test
Appium Java 客户端 第十章 io.appium java-client
REST Assured 第十章 io.rest-assured rest-assured

此外,使用一些这些第三方依赖项需要插件声明进行一些额外设置。以下代码片段显示了这个新的设置。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <!-- The following setup is required only when using Allure --> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
            <configuration>
                <properties>
                    <property>
                        <name>listener</name>
                        <value>io.qameta.allure.junit4.AllureJunit4</value> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
                    </property>
                </properties>
            </configuration>
            <!-- /Allure -->
        </plugin>
        <plugin> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-maven</artifactId>
            <version>${allure-maven.version}</version>
        </plugin>
        <plugin> ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring-boot.version}</version>
        </plugin>
    </plugins>
</build>

1

如果您计划使用 Allure 生成测试报告,则需要从这一行到 <!-- /Allure --> 进行设置。如果不使用它,可以安全地从项目中删除它。

2

不同单元测试框架的监听器类有所不同:

  • io.qameta.allure.junit4.AllureJunit4 适用于 JUnit 4

  • io.qameta.allure.junit5.AllureJunit5 适用于 JUnit 5(以及 JUnit 5 加 Selenium-Jupiter)

  • TestNG 不需要监听器

3

除了监听器外,使用此报告工具还需要 Allure 插件。

4

在使用 Spring-Boot 时建议使用 Spring-Boot 插件。

Gradle

每个 Gradle 项目由多个 任务 组成。每个任务代表构建中的一个原子工作单元。在 Java 项目中,任务的典型示例包括:

compileJava

编译应用程序逻辑(即位于文件夹 src/main/java 的 Java 类)。

processResources

复制应用程序资源(即位于文件夹 src/main/resources 的文件)到输出文件夹(build)。

compileTestJava

编译测试逻辑(即位于文件夹 src/test/java 的 Java 类)。

processTestResources

复制测试资源(即位于文件夹 src/test/resources 的文件)到输出文件夹中。

test

使用 JUnit 或 TestNG 运行测试。表 C-4 总结了在 shell 中运行 Gradle 测试的常见命令。

clean

删除项目输出文件夹及其内容。

表 C-4. 使用 Gradle 运行测试的基本命令

命令 描述
gradle test 运行项目中的所有测试
gradle test --rerun-tasks 运行项目中的所有测试(即使一切都是最新的)
gradle test --tests MyClass 运行单个类中的所有测试
gradle test --tests MyClass.MyMethod 运行单个类中的单个测试

示例 C-3 包含示例存储库所有子项目的通用配置。接下来我将解释这个片段的相关部分。

示例 C-3. Gradle 项目的通用设置
plugins {
    id "java" ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
}

compileTestJava { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    options.compilerArgs += "-parameters"
}

test {
    testLogging { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
        events "passed", "skipped", "failed"
        showStandardStreams = true
    }

    systemProperties System.properties ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)

    if (project.hasProperty("excludeTests")) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png)
        "$excludeTests".split(",").each { excludeTests ->
            exclude excludeTests
        }
    }

    if (project.hasProperty("parallel")) { ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png)
        maxParallelForks = Runtime.runtime.availableProcessors()
    }

    ext.failedTests = [] ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png)

    tasks.withType(Test) {
        afterTest { TestDescriptor descriptor, TestResult result ->
            if(result.resultType ==
                  org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE) {
                failedTests << ["${descriptor.className}::${descriptor.name}"]
            }
        }
    }

    gradle.buildFinished {
        if(!failedTests.empty){
            println "Failed test(s) for ${project.name}:"
            failedTests.each { failedTest ->
                println failedTest
            }
        }
    }
}

repositories {
    mavenCentral() ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png)
}

dependencies { ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/9.png)
    implementation("org.slf4j:slf4j-api:${slf4jVersion}")
    implementation("ch.qos.logback:logback-classic:${logbackVersion}")

    testImplementation("org.seleniumhq.selenium:selenium-java:${seleniumVersion}")
    testImplementation("org.assertj:assertj-core:${assertjVersion}")
    testImplementation("io.github.bonigarcia:webdrivermanager:${wdmVersion}")
}

1

由于我们正在实现一个 Java 项目,因此需要声明 java 插件

2

用于编译测试时,我们使用 Java 8。

3

虽然不是强制的,但我们强制将测试日志写入标准输出。

4

这允许在命令行中传递 Java 系统属性(如 示例 B-1 中所述)。

5

此子句允许在命令行中使用属性 excludeTests 来排除一些测试。例如,以下命令排除以 Docker 开头的测试:gradle test -PexcludeTests=**/Docker*

6

这些行允许使用命令 gradle test -Pparallel 来并行运行测试。

7

下列子句将失败的测试汇总到 failedTests 属性,并在测试套件执行结束时将此信息显示在标准输出中。

8

我们使用 Maven 中央仓库 来获取依赖项。

9

常用依赖项包括 Selenium WebDriver、AssertJ、WebDriverManager(用于测试)、SLF4J 和 Logback(整个项目)。

JUnit 4

JUnit 4 的具体设置如下:

test {
    useJUnit() { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
        if (project.hasProperty("groups")) {
            includeCategories "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeCategories "$excludedGroups"
        }
    }
}

dependencies { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
    testImplementation("junit:junit:${junit4Version}")
}

1

我们使用额外的配置来允许使用类名进行测试筛选(参见 “分类和筛选测试”)。

2

我们包含了 JUnit 4 依赖。

JUnit 5

在使用 JUnit 5 时,我们需要指定 junit-jupiter 构件(就像 Maven 中依赖于 junit-jupiter-api junit-jupiter-enginejunit-jupiter-params)。此外,我们需要使用 test 任务设置中的 useJUnitPlatform() 子句来选择使用 JUnit 平台执行。

test {
    useJUnitPlatform() {
        if (project.hasProperty("groups")) {
            includeTags "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeTags "$excludedGroups"
        }
    }
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:${junit5Version}")
}

Selenium-Jupiter

如果使用 Selenium-Jupiter,除了之前配置 JUnit 5 的设置外,还需要包含以下依赖项。在这种情况下,我们可以移除 WebDriverManager,因为它被 Selenium-Jupiter 传递性地引入。

dependencies {
    testImplementation("io.github.bonigarcia:selenium-jupiter:${selJupVersion}")
}

TestNG

最后,为了将 TestNG 作为单元测试框架使用,我们需要包含以下设置:

test {
    useTestNG() {
        if (project.hasProperty("groups")) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
            includeGroups "$groups"
        }
        if (project.hasProperty("excludedGroups")) {
            excludeGroups "$excludedGroups"
        }
    }

    scanForTestClasses = false ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
}

dependencies { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    testImplementation("org.testng:testng:${testNgVersion}")
}

1

我们包含这些语句以允许按类名进行筛选。

2

这个属性需要设置为 false,以匹配筛选过程中的包含和排除模式。

3

我们包含了 TestNG 依赖。

其他依赖项

我们需要在 Gradle 设置中添加额外的依赖项,以使用第三方库。表 C-3(在前一节中)总结了这些依赖项的坐标及其所述章节。此外,还需要在 Gradle 设置中使用一些额外的插件(用于使用 Allure 和 Spring-Boot,分别)。要使用 Allure,还需要定义一个额外的仓库,如下所示:

plugins {
    id "io.qameta.allure"
    id "org.springframework.boot"
}

repositories {
    maven {
       url "https://plugins.gradle.org/m2/"
    }
}

日志记录

我在示例库中使用了两个日志库:

Logback

这是实际的日志记录框架(也称为记录器)。Logback 被许多重要的 Java 项目使用,例如 Spring Framework 和 Groovy 等。

Simple Logging Facade for Java (SLF4J)

这是一个基于外观设计模式的流行实用工具,用于解耦底层记录器。它支持主要的日志框架(例如 Logback、Log4j 或 SimpleLogger 等)。正如在表 C-5 中总结的那样,SLF4J 根据消息的严重性定义了五个日志级别。

表 C-5. SLF4J 中的日志级别

日志级别 描述
ERROR 用于报告应用程序中的缺陷。
WARN 发生了一些意外情况,但不影响预期的应用行为。
INFO 提供信息的消息,例如应用程序进入了特定状态等。
DEBUG 用于诊断和故障排除的信息。
TRACE 最精细的信息。我们仅在需要完全了解应用程序发生情况的特殊情况下使用此级别。

通常情况下,要使用这些库,我们需要解决相应的依赖关系(详见 Maven 和 Gradle 的下一节)。然后,我们需要正确配置 Logback。为此,我们需要在项目类路径中包含一个 XML 配置文件。如果我们正在为整个项目配置日志记录(即应用程序加测试逻辑),则此文件的名称应为 logback.xml。在这种情况下,它应该在应用程序资源中可用,通常位于 src/main/resources 文件夹下(有关项目布局的更多信息,请参见下一节)。如果我们仅用于测试日志记录,配置文件的名称为 logback-test.xml,存储在测试资源中(例如位于 src/test/resources 文件夹下)。

在这两种情况下的语法(logback.xmllogback-test.xml)是相同的。示例 C-4 展示了配置文件的示例。这个 XML 文件设置了每条日志行的模式,由时间戳、线程名称、跟踪级别、源(包、类名和代码行)和消息组成。在这个示例中,INFO是默认的日志级别。这样,每个这个级别或更严重的追踪(即WARNERRORFATAL)都会显示,但不包括以下级别(即DEBUGTRACE)。此外,来自包io.github.bonigarcia的追踪(用于测试示例、WebDriverManager 和 Selenium-Jupiter)为DEBUG

C-4. Logback 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}.%M\(%line\)
                  - %msg%n</pattern>
      </encoder>
   </appender>

   <logger name="io.github.bonigarcia" level="DEBUG" />

   <root level="INFO">
      <appender-ref ref="STDOUT" />
   </root>

</configuration>

最后一步是在我们的 Java 类中使用变量记录日志。为此,我们可以使用示例 C-5 中的代码。这段代码通过lookup()方法提供了一种方便的方式来获取当前类的反射信息。然后,我们声明记录日志的变量(在这个示例中称为log),并使用 SLF4J 的getLogger()方法。最后,我们可以在这个类的任何方法中使用变量log记录不同级别的消息。

C-5. 记录消息的示例
static final Logger log = getLogger(lookup().lookupClass());

log.info("This is an informative message");
log.debug("This is a debugging message");

GitHub Actions

我使用 GitHub Actions 作为示例仓库的 CI 服务器。这样,每次我向仓库提交新更改时,GitHub Actions 都会构建项目并执行所有测试。示例 C-6 展示了执行此过程的配置。

C-6. GitHub Actions 工作流配置
name: build

on: ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

env: ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
  DISPLAY: :99
  WDM_GITHUBTOKEN: ${{ secrets.WDM_GITHUBTOKEN }}

jobs:
  tests: ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ ubuntu-latest, windows-latest, macos-latest ]
        java: [ 8 ]

    steps: ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png)
    - name: Checkout GitHub repo
      uses: actions/checkout@v2
    - name: Set up Java
      uses: actions/setup-java@v2
      with:
        distribution: 'temurin'
        java-version: ${{ matrix.java }}
    - name: Start Xvfb
      run: Xvfb :99 &
    - name: Test with Maven
      run: mvn -B test
    - name: Test with Gradle
      run: ./gradlew test

1

触发工作流的事件是push(仓库中的新提交)和pull_request(其他开发人员提出的提交)。

2

需要两个环境变量:

DISPLAY

默认情况下,由 Selenium WebDriver 控制的浏览器需要在带有图形用户界面的操作系统中执行。另一方面,GitHub Actions 中提供的 Linux 发行版是无头的(即没有图形用户界面)。因此,我们使用 Xvfb(X 虚拟帧缓冲)在这些 Linux 发行版上运行 WebDriver 测试。Xvfb 是 Unix-like 系统的内存中显示服务器,需要在 Linux(X11)中声明带有屏幕号的环境变量DISPLAY

WDM_GITHUBTOKEN

GitHub 托管 Selenium WebDriver 需要的一些驱动程序(如 geckodriver 或 operadriver)。当外部客户端(如 WebDriverManager)连续向 GitHub 发送多个请求时,GitHub 最终会返回 HTTP 错误响应(403,禁止),原因是其速率限制。WebDriverManager 可以使用个人访问令牌进行身份验证请求,以避免此问题。图 C-2 展示了授予此令牌的权限在示例仓库中。总之,此环境变量导出了此令牌的值。我将此令牌的实际值保存为 GitHub 仓库的机密。

3

为了完整起见,我在三种不同的操作系统中执行工作流程:Ubuntu(即 Linux)、Windows 和 macOS,所有操作系统均使用 Java 8。

4

工作流程包括五个步骤:

  1. 检出仓库。

  2. 使用Eclipse Adoptium设置 Java 8。

  3. 启动 X 虚拟帧缓冲区。

  4. 使用 Maven 运行所有测试。

  5. 使用 Gradle 运行所有测试。

hosw ac02

图 C-2. GitHub 仓库示例中使用的个人访问令牌的权限

Dependabot

要配置 Dependabot,我们需要在仓库的 .github 文件夹中包含名为 dependabot.yml 的文件。示例 C-7 展示了示例仓库中的内容。

示例 C-7. Dependabot 配置
version: 2
updates:
- package-ecosystem: maven ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png)
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

- package-ecosystem: gradle ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png)
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

- package-ecosystem: github-actions ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png)
  directory: "/"
  schedule:
    interval: daily
    time: '06:00'
  open-pull-requests-limit: 99

1

我们每天检查 Maven 依赖项的更新情况。

2

我们每天检查 Gradle 依赖项的更新情况。

3

我们每天检查 GitHub Actions 设置的更新情况。

摘要

本书中所有示例都可在公共GitHub 仓库中找到。本附录展示了构建工具(Maven 和 Gradle)、依赖项(Selenium WebDriver、JUnit、TestNG、Selenium-Jupiter、WebDriverManager 等)、日志记录(Logback 和 SLF4J)以及其他服务(GitHub Actions、GitHub Pages 和 Dependabot)的详细配置。

posted @ 2024-06-15 12:22  绝不原创的飞龙  阅读(24)  评论(0编辑  收藏  举报