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/ChromiumchromedriverWindows/macOS/LinuxGooglehttps://chromedriver.chromium.org
EdgemsedgedriverWindows/macOS/Linux微软https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver
FirefoxgeckodriverWindows/macOS/LinuxMozillahttps://github.com/mozilla/geckodriver
OperaoperadriverWindows/macOS/LinuxOpera Software AShttps://github.com/operasoftware/operachromiumdriver
Internet ExplorerIEDriverServerWindowsSelenium 项目https://www.selenium.dev/downloads
SafarisafaridrivermacOSApple内建

驱动程序(例如 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)进行仪器化和控制。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0101.png

图 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 之间通信的标准协议。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0102.png

图 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. 关闭浏览器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0103.png

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

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0104.png

图 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-webdriverHaskellBSD-3-ClauseAdam Curtishttps://github.com/kallisti-dev/hs-webdriver
php-webdriverPHPMITFacebook、社区https://github.com/php-webdriver/php-webdriver
RSeleniumRAGPLv3rOpenScihttps://github.com/ropensci/RSelenium
SeleniumGoMITMiki Tebekahttps://github.com/tebeka/selenium
Selenium-Remote-DriverPerlApache 2.0George S. Baughhttps://github.com/teodesian/Selenium-Remote-Driver
webdriver.dartDartApache 2.0Googlehttps://github.com/google/webdriver.dart
wdJavaScriptApache 2.0Adam Christianhttps://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 的驱动程序管理器

NameLanguageLicenseMaintainerWebsite
WebDriverManagerJavaApache 2.0Boni Garcíahttps://github.com/bonigarcia/webdrivermanager
webdriver-managerJavaScriptMITGooglehttps://www.npmjs.com/package/webdriver-manager
webdriver-managerPythonApache 2.0Serhii Pirohovhttps://pypi.org/project/webdriver-manager
WebDriverManager.NetC#MITAliaksandr Rasolkahttps://github.com/rosolko/WebDriverManager.Net
webdriversRubyMITTitus Fortnerhttps://github.com/titusfortner/webdrivers
提示

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

定位器工具

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

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

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

框架

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

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

名称语言描述许可证维护者网站
CodeceptJSJavaScript将浏览器交互建模为用户视角的简单步骤的多后端测试框架MITMichael Bodnarchukhttps://codecept.io
FluentSeleniumJavaSelenium WebDriver 的流畅 APIApache 2.0Paul Hammanthttps://github.com/SeleniumHQ/fluent-selenium
FluentLeniumJava网站和移动自动化框架,用于创建可读性强、可重用的 WebDriver 测试Apache 2.0FluentLenium 团队https://fluentlenium.com
HealeniumJava使用机器学习算法分析 Web 和移动 Web 元素,改善 Selenium 测试稳定性的库Apache 2.0Anna Chernyshova 和 Dmitriy Gumeniukhttps://healenium.io
HeliumPython基于 Selenium WebDriver 的高级 APIMITMichael Herrmannhttps://github.com/mherrmann/selenium-python-helium
QAF (QMetry Automation Framework)Java用于 Web 和移动应用程序的测试自动化平台MITChirag Jayswalhttps://qmetry.github.io/qaf
LightningJava轻量级的 Selenium WebDriver Java 客户端Apache 2.0FluentLeniumhttps://github.com/aerokube/lightning-java
NerodiaPythonWatir Ruby gem 的 Python 移植版MITLucas Tierneyhttps://nerodia.readthedocs.io
Robot FrameworkPython, Java, .NET 等基于可读测试用例的通用自动化框架Apache 2.0Robot Framework Foundationhttps://robotframework.org
Selenide 团队JavaSelenium WebDriver 的流畅、简洁 APIMITSelenide 团队https://selenide.org
SeleniumBasePython基于 WebDriver 和 pytest 的浏览器自动化框架MITMichael Mintzhttps://seleniumbase.io
Watir (Web Application Testing in Ruby)Ruby基于 WebDriver 的 Ruby gem 库,用于自动化 Web 浏览器MITTitus Fortnerhttp://watir.com
WebDriverIOJavaScript基于 WebDriver 和 Appium 的测试自动化框架MITChristian Bromannhttps://webdriver.io
Nightwatch.jsJavaScript基于 W3C WebDriver 的集成端到端测试框架MITAndrei Rusuhttps://nightwatchjs.org
ApplitoolsJava, JavaScript, C#, Ruby, PHP, Python用于视觉用户界面回归和 A/B 测试的测试自动化框架。它为 Selenium、Appium 等提供 SDK商业Applitools 团队https://applitools.com
Katalon StudioJava, Groovy利用 Selenium WebDriver、Appium 和云提供商的测试自动化平台商业Katalon 团队https://www.katalon.com
TestProjectJava, 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-seleniumSelenium Grid 的官方 Docker 镜像Apache 2.0Selenium 项目https://github.com/seleniumhq/docker-selenium
Selenoid用 Go 语言轻量级实现的 Selenium Hub,在 Docker 中运行浏览器(镜像可在 Docker Hub 上找到)Apache 2.0Aerokubehttps://aerokube.com/selenoid
Moon使用 Docker 和 Kubernetes 的企业级 Selenium 集群商业Aerokubehttps://aerokube.com/moon
Callisto开源的 Kubernetes 本地实现的 Selenium GridMITAerokubehttps://github.com/wrike/callisto

社区

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

表 1-7. Selenium 社区资源

类别描述网站
官方文档用户指南https://www.selenium.dev/documentation
博客https://www.selenium.dev/blog
Wikihttps://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
Slackhttps://seleniumhq.slack.com
IRChttps://webchat.freenode.net/#selenium
StackOverflowhttps://stackoverflow.com/questions/tagged/selenium
Reddithttps://www.reddit.com/r/selenium
活动会议https://www.selenium.dev/categories/conference
Meetupshttps://www.meetup.com/topics/selenium

软件测试基础

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

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

注意

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

测试级别

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0105.png

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

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

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

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0106.png

图 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 是一个跟踪源代码、谁做了每个更改以及何时做的工具(有时称为补丁)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0107.png

图 1-7. CI 通用流程

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

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

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

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

表 1-8. 构建服务器

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

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

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

持续交付(CD)

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

持续部署

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0108.png

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

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

测试自动化工具

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

表 1-9. 单元测试框架

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

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

设置

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

练习

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

验证

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

拆卸

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0109.png

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

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

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

表 1-10. Java 的断言库

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

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

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

表 1-11. Java 的模拟库

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

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

表 1-12. BDD 框架

名称语言描述许可证维护人员网站
CucumberRuby, Java, JavaScript, Python用于创建遵循 BDD 方法的自动验收测试的测试框架MITSmartBear Softwarehttps://cucumber.io
FitNesseJava独立的协作 wiki 和验收测试框架CPLFitNesse 团队http://fitnesse.org
JBehaveJava, Groovy, Kotlin, Ruby, Scala适用于所有 JVM 语言的 BDD 框架BSD-3-ClauseJBehave 团队https://jbehave.org
JasmineJavaScript用于 JavaScript 的 BDD 框架MITJasmine 团队https://jasmine.github.io
CapybaraRuby模拟用户故事场景的基于 Web 的验收测试框架MITThomas Walpolehttps://teamcapybara.github.io/capybara
Serenity BDDJava, Javascript自动验收测试库Apache 2.0Serenity 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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0201.png

图 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
远程浏览器(参见第六章)

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0202.png

图 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();
Chromechromedriver

|

WebDriverManager.edgedriver().setup();
Edgemsedgedriver

|

WebDriverManager.firefoxdriver().setup();
Firefoxgeckodriver

|

WebDriverManager.operadriver().setup();
Operaoperadriver

|

WebDriverManager.chromiumdriver().setup();
Chromiumchromedriver

|

WebDriverManager.iedriver().setup();
Internet ExplorerIEDriverServer

单元测试框架

如 第 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 的基本测试生命周期的图形表示。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0203.png

图 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 通常被提供测试模型的框架所使用。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0204.png

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 相同的工作流程。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0205.png

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 PioneerJupiter 的扩展包EPL 2.0JUnit Pioneer 团队https://junit-pioneer.org
rerunner-jupiter用于重新运行失败的 Jupiter 测试的扩展Apache 2.0Artem Sokovetshttps://github.com/artsok/rerunner-jupiter
MockitoExtensionJupiter 扩展,用于初始化模拟对象和处理存根MITMockito 团队https://github.com/mockito/mockito
QuickPerf用于评估一些性能相关属性的库Apache 2.0QuickPerf 团队https://github.com/quick-perf/quickperf
Selenium-JupiterSelenium WebDriver 的 Jupiter 扩展Apache 2.0Boni Garcíahttps://bonigarcia.dev/selenium-jupiter
SpringExtensionSpring 框架的 Jupiter 扩展Apache 2.0Pivotal Softwarehttps://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 提供了额外的注解来在套件和测试之前和之后执行自定义逻辑。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0206.png

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

流畅断言

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0207.png

图 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; <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

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

    @BeforeEach
    void setup() {
        driver = new ChromeDriver(); <https://github.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); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
        String title = driver.getTitle(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
        log.debug("The title of {} is {}", sutUrl, title); <https://github.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"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
    }

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

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-6

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-7

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO1-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 启动,最后,我们可以看到测试跟踪(具体来说,是网页标题)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0208.png

图 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(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assumeThat(browserPath).isPresent(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    WebDriverManager.operadriver().setup();
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO2-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_preparing_for_testing_CO2-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)。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0209.png

图 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(); <https://github.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(); <https://github.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
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO1-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO1-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(); <https://github.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
    }

}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO2-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); <https://github.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"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(driver.getCurrentUrl()).isEqualTo(sutUrl); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    assertThat(driver.getPageSource()).containsIgnoringCase("</html>"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO3-1

我们打开实践网站。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO3-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO3-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO3-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(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(sessionId).isNotNull(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    log.debug("The sessionId is {}", sessionId.toString()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO4-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO4-2

我们验证 sessionId 具有某些值。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO4-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>

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0301.png

图 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 的文档(如网页)中选择节点。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0302.png

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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(textarea.getDomAttribute("rows")).isEqualTo("3"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO5-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO5-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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(textByName.isEnabled()).isTrue(); <https://github.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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    assertThat(textById.getAttribute("type")).isEqualTo("text"); <https://github.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"); <https://github.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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
    assertThat(byClassName.size()).isPositive(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/7.png>
    assertThat(byClassName.get(0).getAttribute("name")).isEqualTo("my-text"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/8.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-6

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-7

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO6-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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(linkByText.getTagName()).isEqualTo("a"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(linkByText.getCssValue("cursor")).isEqualTo("pointer"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO7-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO7-2

我们检查其标签名称为a

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO7-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO7-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO7-5

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

通过 CSS 选择器定位

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

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

表 3-5. 基本 CSS 选择器

CategorySyntaxDescriptionExampleExample explanation
Universal*选择所有元素*匹配所有元素
TypeelementName选择所有具有给定标签名的元素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]")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
        assertThat(hidden.isDisplayed()).isFalse(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    }

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO8-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO8-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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(checkbox1.getAttribute("id")).isEqualTo("my-checkbox-1"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(checkbox1.isSelected()).isTrue(); <https://github.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)")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    assertThat(checkbox2.getAttribute("id")).isEqualTo("my-checkbox-2"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
    assertThat(checkbox2.isSelected()).isFalse(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO9-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']")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(hidden.isDisplayed()).isFalse(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO10-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO10-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]")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(radio1.getAttribute("id")).isEqualTo("my-radio-1"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(radio1.isSelected()).isTrue(); <https://github.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)]")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    assertThat(radio2.getAttribute("id")).isEqualTo("my-radio-2"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/5.png>
    assertThat(radio2.isSelected()).isFalse(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/6.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO11-6

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

提示

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

在网页上查找定位器

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

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0303.png

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

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

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0304.png

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0305.png

图 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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(fileElement.getAttribute("id")).isBlank(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(fileElement.getAttribute("name")).isNotBlank(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO12-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO12-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO12-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"))); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    assertThat(rowsInForm.size()).isEqualTo(1); <https://github.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"))); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    assertThat(rowsInForm.size()).isEqualTo(5); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO13-1

我们使用ByChained定位器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO13-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO13-3

我们使用ByAll定位器。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO13-4

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

相对定位器

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0306.png

图 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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    RelativeBy relativeBy = RelativeLocator.with(By.tagName("input")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    WebElement readOnly = driver.findElement(relativeBy.above(link)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>
    assertThat(readOnly.getAttribute("name")).isEqualTo("my-readonly"); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO14-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO14-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO14-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO14-4

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

警告

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

一个具有挑战性的例子

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0307.png

图 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 <https://github.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 <https://github.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 <https://github.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 <https://github.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 <https://github.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 <https://github.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 <https://github.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 <https://github.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);
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-6

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-7

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO15-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 选择器之间的一些差异

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

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

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

定位器XPathCSS 选择器
所有元素//**
所有 <div> 元素//divdiv
通过 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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    String textValue = "Hello World!";
    inputText.sendKeys(textValue); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(inputText.getAttribute("value")).isEqualTo(textValue); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO16-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO16-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO16-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO16-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO16-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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

    Path tempFile = Files.createTempFile("tempfiles", ".tmp"); <https://github.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); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO17-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO17-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO17-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO17-4

我们提交表单。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO17-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); <https://github.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); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO18-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO18-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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0308.png

图 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(); <https://github.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(); <https://github.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(); <https://github.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(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO19-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO19-2

确认中间菜单显示正确。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO19-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO19-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) { <https://github.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)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
        actions.moveToElement(image).build().perform(); <https://github.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)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO20-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO20-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO20-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO20-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO20-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0309.png

图 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")); <https://github.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(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
    assertThat(initLocation).isEqualTo(draggable.getLocation()); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-1

我们定位可拖动元素。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-5

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO21-6

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0310.png

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

点击并保持

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0311.png

图 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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>
    actions.moveToElement(canvas).clickAndHold(); <https://github.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++) { <https://github.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); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/4.png>
    }

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO22-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO22-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO22-3

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO22-4

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO22-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")); <https://github.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; <https://github.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(); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/3.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO23-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO23-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO23-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),并将剪贴板内容粘贴到文本区域中。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO23-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 中。由于这些图片在页面加载之前不可用,我们需要等待这些图片可用。

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0312.png

图 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)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO24-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO24-2

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

提示

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

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

显式等待

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

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0313.png

图 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)); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/1.png>

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO25-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO25-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(); <https://github.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")); <https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/2.png>
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO26-1

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO26-2

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

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/hosw_0314.png

图 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); <https://github.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");
}

https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/hsn-slnm-wdvr-java/img/#co_webdriver_fundamentals_CO27-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 异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值