Selenium-Webdriver3-实践指南-全-
Selenium Webdriver3 实践指南(全)
原文:
zh.annas-archive.org/md5/1e009aabda4e5d0fd935a95cf0785b01译者:飞龙
前言
本书是关于 Selenium WebDriver 的,即一个浏览器自动化工具,软件开发人员和 QA 工程师使用它来测试他们在不同浏览器上的 Web 应用程序。本书可以用作 WebDriver 日常使用的参考。
Selenium 是一套用于自动化浏览器的工具。它主要用于测试应用程序,但其用途不仅限于测试。它还可以用于屏幕抓取和在浏览器窗口中自动化重复性任务。Selenium 支持在所有主要浏览器上自动化,包括 Firefox、Internet Explorer、Google Chrome、Safari 和 Opera。Selenium WebDriver 现在是 W3C 标准的一部分,并且得到了主要浏览器厂商的支持。
本书面向对象
如果你是一名质量保证/测试专业人士、测试工程师、软件开发人员或 Web 应用程序开发人员,希望为您的 Web 应用程序创建自动化测试套件,那么这本书是您的完美指南!作为先决条件,本书假设您对 Java 编程有基本的了解,尽管不需要 WebDriver 或 Selenium 的先验知识。本书结束时,您将获得 WebDriver 的全面知识,这将有助于您编写自动化测试。
本书涵盖内容
第一章,介绍 WebDriver 和 WebElements,将从 Selenium 和其特性的概述开始。然后,我们快速跳入 WebDriver,描述它是如何感知网页的。我们还将探讨 WebDriver 的 WebElement 是什么。然后,我们将讨论在网页上定位 WebElements 以及对他们执行一些基本操作。
第二章,与浏览器驱动程序一起工作,将讨论 WebDriver 的各种实现,如 FirefoxDriver、IEDriver 和 ChromeDriver。我们将配置浏览器选项以在无头模式下运行测试、移动仿真以及使用自定义配置文件。随着 WebDriver 成为 W3C 规范的一部分,现在所有主要浏览器厂商都在浏览器中原生支持 WebDriver。
第三章,使用 Java 8 特性与 Selenium 结合,将讨论 Java 8 的突出特性,如 Streams API 和 Lambda 表达式,用于处理 WebElements 列表。Stream API 和 Lambda 表达式有助于应用函数式编程风格,创建可读性和流畅性强的测试。
第四章,探索 WebDriver 的特性,将讨论 WebDriver 的一些高级特性,如网页截图、执行 JavaScript、处理 Cookies 以及处理窗口和框架。
第五章,探索高级交互 API,将深入探讨 WebDriver 可以在网页的 WebElements 上执行更高级的操作,例如将元素从一个页面的一个框架拖放到另一个框架,以及在 WebElements 上右键单击/上下文单击。我们相信你将发现这一章很有趣。
第六章,理解 WebDriver 事件,将处理 WebDriver 的事件处理方面。例如,事件可以是 WebElement 上的值变化、浏览器后退导航调用、脚本执行完成等。我们将使用这些事件来运行可访问性和性能检查。
第七章,探索 RemoteWebDriver,将讨论如何使用 RemoteWebDriver 和 Selenium Standalone Server 从你的机器上执行远程机器上的测试。你可以使用 RemoteWebDriver 类与远程机器上的 Selenium Standalone Server 通信,以在远程机器上运行的所需浏览器上执行命令。其流行的用例之一是浏览器兼容性测试。
第八章,设置 Selenium Grid,将讨论 Selenium 的一个重要且有趣的功能——Selenium Grid。使用它,你可以通过 Selenium Grid 在分布式计算机网络上执行自动化测试。我们将配置一个 Hub 和多个 Nodes 进行跨浏览器测试。这也使得并行运行测试和在分布式架构中运行测试成为可能。
第九章,页面对象模式,将讨论一个名为页面对象模式(PageObject Pattern)的知名设计模式。这是一个经过验证的模式,将帮助你更好地掌握自动化框架和场景,以实现更好的可维护性。
第十章,使用 Appium 在 iOS 和 Android 上进行移动测试,将介绍如何使用 WebDriver 通过 Appium 自动化 iOS 和 Android 平台的测试脚本。
第十一章,使用 TestNG 进行数据驱动测试,将讨论使用 TestNG 进行数据驱动测试技术。使用数据驱动测试方法,我们可以使用多组测试数据重用测试,以获得额外的覆盖率。
为了充分利用这本书
预期读者对编程有一个基本了解,最好是使用 Java,因为我们将通过代码示例带读者了解 WebDriver 的几个功能。以下软件是本书所需的:
-
Oracle JDK8
-
Eclipse IDE
-
Maven 3
-
Google Chrome
-
Mozilla Firefox
-
Internet Explorer 或 Edge(在 Windows 上)
-
Apple Safari
-
Appium
安装 Java
在本书中,我们展示的所有代码示例,涵盖 WebDriver 的各种功能,都将使用 Java 编写。为了遵循这些示例并编写您自己的代码,您需要在您的计算机上安装 Java 开发工具包。最新版本的 JDK 可以从以下链接下载:
www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
安装 Eclipse
本书是一本实用指南,期望用户编写和执行 WebDriver 示例。为此,安装一个 Java 集成开发环境会很有帮助。Eclipse IDE 是 Java 用户社区中流行的选择。Eclipse IDE 可以从 www.eclipse.org/downloads/ 下载。
下载示例代码文件
您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在“搜索”框中输入书籍名称,并遵循屏幕上的说明。
一旦文件下载完成,请确保您使用最新版本解压缩或提取文件夹,具体如下:
-
适用于 Windows 的 WinRAR/7-Zip
-
适用于 Mac 的 Zipeg/iZip/UnRarX
-
适用于 Linux 的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Selenium-WebDriver-3-Practical-Guide-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,请访问 github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从 www.packtpub.com/sites/default/files/downloads/SeleniumWebDriver3PracticalGuideSecondEdition_ColorImages.pdf 下载。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“beforeMethod()”,它带有 @BeforeMethod 测试 NG 注解。
代码块设置如下:
<input id="search" type="search" name="q" value="" class="input-text required-entry" maxlength="128" placeholder="Search entire store here..." autocomplete="off">
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
WebElement searchBox = driver.findElement(By.id("q"));
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“要运行测试,在代码编辑器中右键单击并选择“运行 As | TestNG 测试”,如图所示。”
警告或重要提示看起来像这样。
技巧和窍门看起来像这样。
联系我们
我们欢迎读者的反馈。
总体反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件联系我们的questions@packtpub.com。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一错误。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能向我们提供位置地址或网站名称。请通过电子邮件联系我们的copyright@packtpub.com,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买书籍的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packtpub.com。
第一章:介绍 WebDriver 和 WebElements
在本章中,我们将简要介绍 Selenium,其各种组件,如 Appium,然后转向网页的基本组件,包括各种类型的 WebElements。我们将学习在网页上定位 WebElements 的不同方法,并在它们上执行各种用户操作。本章将涵盖以下主题:
-
Selenium 测试工具的各种组件
-
使用 Maven 和 TestNG 在 Eclipse 中设置项目
-
在网页上定位 WebElements
-
可以在 WebElements 上执行的操作
Selenium 是一套广泛使用的工具,用于自动化浏览器。它主要用于测试应用程序,但其用途不仅限于测试。它还可以用于执行屏幕抓取和在浏览器窗口中自动化重复性任务。Selenium 支持在所有主要浏览器上自动化,包括 Google Chrome、Mozilla Firefox、Microsoft Internet Explorer 和 Edge、Apple Safari 和 Opera。Selenium 3.0 现已成为 W3C 标准,并得到主要浏览器供应商的支持。
Selenium 测试工具
Selenium 3.0 提供了三个重要的工具:Selenium WebDriver、Selenium 服务器和 Selenium IDE。每个工具都提供创建、调试和运行在支持的浏览器和操作系统上的测试的功能。让我们详细探讨每个工具。
Selenium WebDriver
Selenium WebDriver 是 Selenium RC(远程控制)的继任者,Selenium RC 已被官方弃用。Selenium WebDriver 使用 JSON-Wire 协议(也称为客户端 API)接受命令,并将它们发送到由特定驱动程序类(如 ChromeDriver、FirefoxDriver 或 IEDriver)启动的浏览器。这是通过浏览器特定的浏览器驱动程序实现的。它按照以下顺序工作:
-
驱动程序监听来自 Selenium 的命令
-
它将这些命令转换为浏览器的原生 API
-
驱动程序接收原生命令的结果,并将结果发送回 Selenium:

我们可以使用 Selenium WebDriver 执行以下操作:
-
创建健壮的基于浏览器的回归自动化
-
在许多浏览器和平台上扩展和分发脚本
-
使用您喜欢的编程语言创建脚本
Selenium WebDriver 提供了一组特定于语言的绑定(客户端库),用于驱动浏览器。WebDriver 提供了一套更好的 API,通过其实现类似于面向对象编程,以满足大多数开发者的期望。WebDriver 正在一段时间内积极开发,您可以看到许多与 Web 以及移动应用程序的先进交互。
Selenium 客户端 API 是一种特定于语言的 Selenium 库,它为 Java、C#、Python、Ruby 和 JavaScript 等编程语言提供了一致的 Selenium API。这些语言绑定允许测试启动 WebDriver 会话并与浏览器或 Selenium 服务器通信。
Selenium 服务器
Selenium Server 允许我们在远程机器上运行的浏览器实例以及并行运行测试,从而将测试负载分散到多台机器上。我们可以创建一个 Selenium Grid,其中一个服务器作为 Hub 运行,管理节点池。我们可以配置我们的测试以连接到 Hub,然后 Hub 获取一个空闲节点,该节点与我们需要运行测试的浏览器相匹配。Hub 有一个节点列表,提供对浏览器实例的访问,并允许测试像负载均衡器一样使用这些实例。Selenium Grid 通过集中管理不同类型的浏览器、它们的版本和操作系统配置,使我们能够在多台机器上并行执行测试。
Selenium IDE
Selenium IDE 是一个 Firefox 插件,允许用户记录、编辑、调试和回放以 Selenese 格式捕获的测试,该格式是在 Selenium 核心版本中引入的。它还为我们提供了将这些测试转换为 Selenium RC 或 Selenium WebDriver 格式的功能。我们可以使用 Selenium IDE 执行以下操作:
-
使用记录和回放创建快速简单的脚本,或者将它们用于探索性测试
-
创建脚本以辅助自动化辅助的探索性测试
-
创建宏以在 Web 页面上执行重复性任务
当 Firefox 55 从 XPI 格式迁移到 WebExtension 格式后,Selenium IDE for Firefox 停止了工作,并且目前不再维护。
Selenium 2 和 Selenium 3 之间的差异
在我们进一步深入了解 Selenium 3 之前,让我们了解 Selenium 2 和 Selenium 之间的差异。
处理浏览器
由于 Selenium WebDriver 已被接受为 W3C 标准,Selenium 3 对浏览器实现带来了一系列变化。现在,所有主要的浏览器厂商都支持 WebDriver 规范,并提供了必要的功能,包括浏览器。例如,微软推出了 EdgeDriver,苹果支持 SafariDriver 实现。我们将在本书的后面看到一些这些变化。
拥有更好的 API
由于 W3C 标准的 WebDriver 携带了一套更好的 API,这些 API 通过类似于面向对象编程的实现来满足大多数开发者的期望。
拥有开发者支持和高级功能
WebDriver 正在积极开发中,并且现在根据 W3C 规范由浏览器厂商支持;您可以看到许多与 Web 以及移动应用程序的先进交互,例如文件处理和触摸 API。
使用 Appium 测试移动应用程序
在 Selenium 3 中引入的主要差异之一是引入了 Appium 项目。Selenium 2 中作为其一部分的移动测试功能现在已移动到名为 Appium 的独立项目中。
Appium 是一个开源的移动自动化框架,用于使用 JSON-Wire 协议在 iOS 和 Android 平台上测试原生、混合和 Web 移动应用程序。Appium 替换了 Selenium 2 中的 iPhoneDriver 和 AndroidDriver API,这些 API 用于测试移动 Web 应用程序。
Appium 允许使用和扩展现有的 Selenium WebDriver 框架来构建移动测试。因为它使用 Selenium WebDriver 来驱动测试,所以我们可以使用任何编程语言为 Selenium 客户端库创建测试。
使用 Java 在 Eclipse 中设置 Maven 和 TestNG 项目
Selenium WebDriver 是一个帮助您自动化浏览器的库。然而,当用于测试和构建测试框架或用于非测试目的的浏览器自动化时,需要更多。您需要一个集成开发环境(IDE)或代码编辑器来创建一个新的 Java 项目,并添加 Selenium WebDriver 和其他依赖项以构建测试框架。
在 Java 开发社区中,Eclipse 是一个广泛使用的 IDE,以及 IntelliJ IDEA 和 NetBeans。Eclipse 为 Selenium WebDriver 测试开发提供了一个功能丰富的环境。
与 Eclipse 一起,Apache Maven 提供了对测试项目生命周期的管理支持。Maven 用于定义项目结构、依赖项、构建和测试管理。
我们可以使用 Eclipse 和 Maven 从单个窗口构建我们的 Selenium WebDriver 测试框架。使用 Maven 的另一个重要好处是,我们可以通过配置 pom.xml 文件来获取所有 Selenium 库文件及其依赖项。在构建项目时,Maven 会自动从存储库下载必要的文件。
在本节中,我们将学习如何配置 Eclipse 和 Maven 以进行 Selenium WebDriver 测试开发。本书中的大部分代码都是在 Eclipse 和 Maven 中开发的。
您需要 Eclipse 和 Maven 来设置测试开发环境。从 maven.apache.org/download.html 下载并设置 Maven。遵循 Maven 下载页面上的说明(请参阅页面上的安装说明部分)。
从 eclipse.org/downloads/ 下载并设置 Eclipse Java 开发者 IDE。
与 Eclipse 和 Maven 一起,我们还将使用 TestNG 作为项目的测试框架。TestNG 库将帮助我们定义测试用例、测试设置和断言。我们需要通过 Eclipse Marketplace 安装 TestNG 插件。
让我们按照以下步骤配置 Eclipse 和 Maven 以使用 Selenium WebDriver 开发测试:
-
启动 Eclipse IDE。
-
通过选择 Eclipse 主菜单中的“文件 | 新建 | 其他”来创建一个新项目。
-
在“新建”对话框中,选择 Maven | Maven 项目,如图所示,然后点击“下一步”:

- 新建 Maven 项目对话框将显示。选择创建简单项目(跳过存档选择)复选框,然后点击下一步按钮,如图所示:

- 在新建 Maven 项目对话框中,在组 ID:文本框中输入 com.example,在工件 ID:文本框中输入 chapter1。您还可以添加一个名称和描述。点击完成按钮,如图所示:

- Eclipse 将创建一个与以下截图所示结构(在包资源管理器中)相似的 chapter1 项目:

- 从包资源管理器中选择 pom.xml。这将打开编辑器区域中的 pom.xml 文件,并打开 概览 选项卡。选择紧邻概览选项卡的 pom.xml 选项卡,如图所示:

- 在以下代码片段中添加 Selenium WebDriver 和 TestNG 依赖项,并将其添加到 pom.xml 中的
project节之间:
<properties>
<java.version>1.8</java.version>
<selenium.version>3.13.0</selenium.version>
<testng.version>6.13.1</testng.version>
<maven.compiler.version>3.7.0</maven.compiler.version> </properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
- 在包资源管理器中选择 src/test/java,然后右键单击以显示菜单。选择新建 | 其他,如图所示:

- 从选择向导对话框中选择 TestNG | TestNG 类,如图所示:

- 在新建 TestNG 类对话框中,在源文件夹:字段中输入 /chapter1/src/test/java。在包名:字段中输入 com.example。在类名:字段中输入 NavigationTest。选择 @BeforeMethod 和 @AfterMethod 复选框,并在 XML 测试套件文件:字段中添加
src/test/resources/suites/testng.xml。点击完成按钮:

- 这将在 com.example 包中创建一个名为 NavigationTest.java 的类,其中包含
@Test、@BeforeMethod和@AfterMethod等 TestNG 注解,以及beforeMethod和afterMethod方法:

- 使用以下代码修改
NavigationTest类:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.*;
public class NavigationTest {
WebDriver driver;
@BeforeMethod
public void beforeMethod() {
// set path of Chromedriver executable
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
// initialize new WebDriver session
driver = new ChromeDriver();
}
@Test
public void navigateToAUrl() {
// navigate to the web site
driver.get("http://demo-store.seleniumacademy.com/");
// Validate page title
Assert.assertEquals(driver.getTitle(), "Madison Island");
}
@AfterMethod
public void afterMethod() {
// close and quit the browser
driver.quit();
}
}
在前面的代码中,我们向 NavigationTest 类中添加了三个方法。我们还声明了一个 WebDriver driver; 实例变量,我们将在测试中稍后使用它来启动浏览器并导航到网站。
beforeMethod() 方法,它被 @BeforeMethod TestNG 注解标记,将在测试方法之前执行。它将设置 Google Chrome 所需的 chromedriver 可执行文件的路径。然后,它将使用 ChromeDriver() 类实例化 driver 变量。这将启动屏幕上的一个新的 Google Chrome 窗口。
下一个方法navigateToAUrl()使用@Test注解,是测试方法。我们将通过传递应用程序的 URL 调用 WebDriver 接口的get()方法。这将导航到浏览器中的网站。我们将通过调用 TestNG 的Assert.assertEquals方法和 WebDriver 接口的getTitle()方法来检查页面标题。
最后,afterMethod()方法使用@AfterMethod TestNG 注解,将在浏览器窗口关闭。
我们需要从sites.google.com/a/chromium.org/chromedriver/downloads下载并复制 chromedriver 可执行文件。根据您计算机上安装的 Google Chrome 浏览器版本以及操作系统下载适当的版本。将可执行文件复制到/src/test/resources/drivers文件夹中。
要运行测试,在代码编辑器中右键单击,并选择“运行”|“TestNG 测试”,如下面的截图所示:

这将启动一个新的 Google Chrome 浏览器窗口并导航到网站。测试将验证页面标题,并在测试结束时关闭浏览器窗口。TestNG 插件将在 Eclipse 中显示结果:

您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 http://www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。示例代码也托管在github.com/PacktPublishing/Selenium-WebDriver-3-Practical-Guide-Second-Edition
WebElements
一个网页由许多不同类型的 HTML 元素组成,如链接、文本框、下拉按钮、主体、标签和表单。在 WebDriver 的上下文中,这些元素被称为 WebElements。这些网页上的元素共同实现了用户功能。例如,让我们看看一个网站登录页面的 HTML 代码:
<html>
<body>
<form id="loginForm">
<label>Enter Username: </label>
<input type="text" name="Username"/>
<label>Enter Password: </label>
<input type="password" name="Password"/>
<input type="submit"/>
</form>
<a href="forgotPassword.html">Forgot Password ?</a>
</body>
</html>
在前面的 HTML 代码中,有不同类型的 WebElements,如<html>、<body>、<form>、<label>、<input>和<a>,它们共同构成了一个网页,为用户提供登录功能。让我们分析以下 WebElement:
<label>Enter Username: </label>
在这里,<label>是 WebElement 标签的开始标签。Enter Username:是label元素上的文本。最后,</label>是结束标签,表示 WebElement 的结束。
同样,取另一个 WebElement:
<input type="text" name="Username"/>
在前面的代码中,type和name是具有text和Username值的input WebElement 的属性。
使用 Selenium 进行 UI 自动化主要涉及在网页上定位这些 Web 元素并对其执行用户操作。在本章的剩余部分,我们将使用各种方法来定位 Web 元素并在其上执行相关的用户操作。
使用 WebDriver 定位 Web 元素
让我们从自动化演示应用程序首页的搜索功能开始这一节,demo-store.seleniumacademy.com/,这涉及到导航到首页,在文本框中输入搜索文本,并执行搜索。代码如下:
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
如您所见,有三个新的内容被突出显示,如下所示:
WebElement searchBox = driver.findElement(By.name("q"));
它们是findElement()方法、By.name()方法和WebElement接口。findElement()和By()方法指示 WebDriver 在网页上定位一个WebElement,一旦找到,findElement()方法就返回该元素的WebElement实例。使用WebElement接口中声明的方 法在返回的WebElement上执行操作,如点击和输入,这些将在下一节详细讨论。
findElement方法
在 UI 自动化中,在执行任何用户操作之前,定位元素是第一步。WebDriver 的findElement()方法是在网页上定位元素的一种便捷方式。根据 WebDriver 的 Javadoc(selenium.googlecode.com/git/docs/api/java/index.html),方法的声明如下:
WebElement findElement(By by)
因此,findElement()方法的输入参数是By实例。By实例是一个定位WebElement的机制。有八种不同的方式可以在网页上定位一个WebElement。我们将在本章后面看到这八种方法。
findElement()方法的返回类型是表示网页实际 HTML 元素或组件的WebElement实例。该方法返回第一个满足定位机制条件的WebElement。从那时起,此WebElement实例将作为该组件的句柄。测试脚本开发者可以通过使用返回的WebElement实例来对该组件采取适当的操作。
如果 WebDriver 找不到元素,它将抛出一个名为NoSuchElementException的运行时异常,调用类或方法应该处理它。
findElements方法
对于在网页上查找符合相同定位条件的多元素,可以使用findElements()方法。它返回一个包含给定定位机制找到的 Web 元素的列表。findElements()方法的声明如下:
java.util.List findElements(By by)
输入参数与 findElement() 方法相同,它是一个 By 类的实例。区别在于返回类型。在这里,如果没有找到元素,则返回一个空列表;如果有多个 WebElements 满足定位机制,则将它们全部以列表形式返回给调用者。
使用开发者工具检查元素
在我们开始探索如何在页面上查找元素以及使用什么定位机制之前,我们需要查看页面的 HTML 代码,以了解文档对象模型(DOM)树,页面显示的元素定义了哪些属性或属性,以及应用程序如何从 JavaScript 或 AJAX 调用。浏览器使用为页面编写的 HTML 代码在浏览器窗口中渲染视觉元素。它使用其他资源,包括 JavaScript、CSS 和图像,来决定这些元素的外观、感觉和行为。
这里是演示应用程序的登录页面示例以及用于在浏览器中渲染此页面的 HTML 代码,如图所示:

我们需要能够以结构化和易于理解的方式显示页面 HTML 代码的工具。现在几乎所有的浏览器都提供了开发者工具来检查页面结构和相关资源。
使用 Mozilla Firefox 检查页面和元素
新版本的 Mozilla Firefox 提供了内置的方式来检查页面和元素。要检查页面上的元素,将鼠标移至所需的元素上,右键单击以打开弹出菜单。选择检查元素选项,如图所示:

这将显示带有 HTML 代码的检查器选项卡,以树形格式显示,并突出显示所选元素,如图所示:

使用检查器,我们还可以使用检查器部分显示的搜索框验证 XPath 或 CSS 选择器。只需输入 XPath 或 CSS 选择器,检查器就会突出显示匹配表达式的元素,如图所示:

开发者工具还提供了各种其他调试功能。它还会为元素生成 XPath 和 CSS 选择器。为此,在树中选择所需的元素,右键单击,然后从弹出菜单中选择复制 > XPath 或复制 > CSS 路径选项,如图所示:

这将把建议的 XPath 或 CSS 选择器值粘贴到剪贴板,以便稍后与 findElement() 方法一起使用。
使用开发者工具在 Google Chrome 中检查页面和元素
与 Mozilla Firefox 类似,Google Chrome 也提供了一个内置功能来检查页面和元素。我们可以将鼠标移至页面上的所需元素上,右键单击以打开弹出菜单,然后选择“检查元素”选项。这将打开浏览器中的开发者工具,显示类似于 Firefox 的信息,如下面的截图所示:

与 Firefox 类似,我们也可以在 Google Chrome 开发者工具中测试 XPath 和 CSS 选择器。在“元素”选项卡中按Ctrl + F(在 Mac 上使用Command + F)。这将显示一个搜索框。只需输入XPath或CSS Selector,匹配的元素将在树中突出显示,如下面的截图所示:

Chrome 开发者工具还提供了一个功能,您可以通过在树中右键单击所需的元素并从弹出菜单中选择“复制 XPath”选项来获取元素的 XPath。
与 Mozilla Firefox 和 Google Chrome 类似,您将在任何主要浏览器中找到类似的开发者工具,包括 Microsoft Internet Explorer 和 Edge。
浏览器开发者工具在测试脚本开发过程中非常有用。这些工具将帮助您找到需要交互的元素的定位细节。这些工具解析页面代码,并以分层树的形式显示信息。
网页上的 WebElements 可能不具有所有声明的属性。选择用于在网页上唯一标识 WebElements 的属性是测试脚本开发者的责任,以便进行自动化。
使用 By 定位机制
By 是传递给findElement()方法或findElements()方法以获取网页上的相应 WebElement(s)的定位机制。有八种不同的定位机制;也就是说,有八种不同的方式来识别
网页上的一个 HTML 元素。它们可以通过 ID、Name、ClassName、TagName、LinkText、PartialLinkText、XPath 和 CSS Selector 来定位。
By.id()方法
在网页上,每个元素都通过一个 ID 属性来唯一标识,这个属性是可选的。ID 可以由网络应用的开发者手动分配,或者由应用动态生成。动态生成的 ID 可以在每次页面刷新或一段时间内更改。现在,考虑搜索框的 HTML 代码:
<input id="search" type="search" name="q" value="" class="input-text required-entry" maxlength="128" placeholder="Search entire store here..." autocomplete="off">
在前面的代码中,搜索框的id属性值是search。
让我们看看如何使用 ID 属性作为定位机制来找到搜索框:
@Test
public void byIdLocatorExample() {
WebElement searchBox = driver.findElement(By.id("search"));
searchBox.sendKeys("Bags");
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Bags'");
}
在前面的代码中,我们使用了By.id()方法和搜索框的id属性值来找到元素。
在这里,尝试使用By.id标识符,并使用名称值(即q)而不是id值(即search)。将第三行修改如下:
WebElement searchBox = driver.findElement(By.id("q"));
测试脚本将无法抛出异常,如下所示:
Exception in thread "main" org.openqa.selenium.NoSuchElementException: Unable to locate element: {"method":"id","selector":"q"}
WebDriver 无法通过 id 值为 q 的元素找到元素。因此,它抛出一个异常,表示 NoSuchElementException。
By.name() 方法
如前所述,网页上的每个元素都有许多属性。名称是其中之一。例如,搜索框的 HTML 代码如下:
<input id="search" type="search" name="q" value="" class="input-text required-entry" maxlength="128" placeholder="Search entire store here..." autocomplete="off">
在这里,name 是搜索框的许多属性之一,其值是 q。如果我们想在测试脚本中识别这个搜索框并为其设置值,代码将如下所示:
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
如果你观察第四行,这里使用的定位机制是 By.name,名称是 q。那么,我们从哪里得到这个名称的呢?如前所述,是浏览器开发者工具帮助我们获取按钮的名称。启动开发者工具并使用检查元素小部件来获取元素的属性。
By.className() 方法
在我们讨论 className() 方法之前,我们必须稍微谈谈样式和 CSS。网页上的每个 HTML 元素通常由网页开发者或设计师进行样式设计。每个元素都进行样式设计并不是强制性的,但它们通常是为了使页面对最终用户更具吸引力。
因此,为了将样式应用于一个元素,它们可以直接在元素标签中声明,或者放置在一个名为 CSS 文件的单独文件中,并使用 class 属性在元素中引用。例如,按钮的样式属性可以在 CSS 文件中声明如下:
.buttonStyle{
width: 50px;
height: 50px;
border-radius: 50%;
margin: 0% 2%;
}
现在,这个样式可以应用于网页中的按钮元素,如下所示:
<button name="sampleBtnName" id="sampleBtnId" class="buttonStyle">I'm Button</button>
因此,buttonStyle 被用作按钮元素的 class 属性的值,并继承 CSS 文件中声明的所有样式。现在,让我们在我们的主页上尝试这个。我们将尝试使用其类名来识别搜索按钮并点击它。
首先,为了获取搜索按钮的类名,正如我们所知,我们将使用开发者工具来获取它。获取后,将定位机制更改为 By.className 并在其中指定类属性值。相应的代码如下:
@Test
public void byClassNameLocatorExample() {
WebElement searchBox = driver.findElement(By.id("search"));
searchBox.sendKeys("Electronics");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Electronics'");
}
在前面的代码中,我们通过传递类属性值给 By.className 定位机制来使用了 By.className 定位机制。
有时,一个元素可能为 class 属性指定了多个值。例如,以下 HTML 片段中搜索按钮的 class 属性指定了 *button* 和 *search-button* 值:
<button type="submit" title="Search" class="button search-button"><span><span>Search</span></span></button>
我们必须使用 By.className 方法中的一个 class 属性的值。在这种情况下,我们可以使用 *button* 或 *search-button*, whichever 唯一标识了该元素。
By.linkText() 方法
如其名所示,By.linkText定位机制只能用来识别 HTML 链接。在我们开始讨论如何使用 WebDriver 通过链接文本来识别链接元素之前,让我们看看 HTML 链接元素的样子。HTML 链接元素在网页上使用<a>标签表示,这是锚标签的缩写。一个典型的锚标签看起来像这样:
<a href="http://demo-store.seleniumacademy.com/customer/account/" title="My Account">My Account</a>
在这里,href是链接到另一个页面,当你点击链接时,你的网络浏览器会带你到那里。所以,浏览器渲染的前面的 HTML 代码看起来像这样:

这个MY ACCOUNT是链接文本。因此,By.linkText定位机制使用这个文本在锚标签上识别 WebElement。代码看起来是这样的:
@Test
public void byLinkTextLocatorExample() {
WebElement myAccountLink =
driver.findElement(By.linkText("MY ACCOUNT"));
myAccountLink.click();
assertThat(driver.getTitle())
.isEqualTo("Customer Login");
}
在这里,使用By.linkText定位机制来识别MY ACCOUNT链接。
linkText和partialLinkText方法是区分大小写的。
By.partialLinkText()方法
By.partialLinkText定位机制是By.linkText定位器的扩展。如果你不确定整个链接文本或者只想使用链接文本的一部分,你可以使用这个定位器来识别链接元素。所以,让我们修改之前的例子,只使用链接的部分文本;在这种情况下,我们将使用网站页脚中的隐私政策链接中的“隐私”:

代码看起来是这样的:
@Test
public void byPartialLinkTextLocatorExample() {
WebElement orderAndReturns =
driver.findElement(By.partialLinkText("PRIVACY"));
orderAndReturns.click();
assertThat(driver.getTitle())
.isEqualTo("Privacy Policy");
}
如果存在多个链接,其文本包含隐私,会发生什么?这是一个关于findElement()方法的问题,而不是定位器的问题。记得我们之前讨论findElement()方法时,它只会返回它遇到的第一个 WebElement。如果你想找到所有包含隐私的链接文本的 WebElement,请使用findElements()方法,它将返回所有这些元素的列表。
如果你认为你需要所有满足定位机制条件的 WebElement,请使用 WebDriver 的findElements()方法。
By.tagName()方法
通过标签名定位元素与之前看到的定位机制略有不同。例如,在主页上,如果你搜索带有button标签名的元素,它将导致多个 WebElement,因为主页上有九个按钮。所以,在尝试使用标签名定位元素时,始终建议使用findElements()方法而不是findElement()方法。
让我们看看当搜索主页上存在的链接数量时,代码看起来是什么样的:
@Test
public void byTagNameLocatorExample() {
// get all links from the Home page
List<WebElement> links = driver.findElements(By.tagName("a"));
System.out.println("Found links:" + links.size());
// print links which have text using Java 8 Streams API
links.stream()
.filter(elem -> elem.getText().length() > 0)
.forEach(elem -> System.out.println(elem.getText()));
}
在前面的代码中,我们使用了 By.tagName 定位机制和 findElements() 方法,它返回所有链接的列表,即页面上定义的 a 锚标签。在第五行,我们打印了列表的大小,然后只打印了提供文本的链接文本。我们使用 Java 8 Stream API 通过调用 getText() 方法来过滤元素列表并输出文本值。这将生成以下输出:
Found links:88
ACCOUNT
CART
WOMEN
...
By.xpath() 方法
WebDriver 使用 XPath 来识别网页上的 WebElement。在我们看到它是如何做到这一点之前,让我们快速看一下 XPath 的语法。XPath 是 XML 路径的简称,是用于搜索 XML 文档的查询语言。我们网页的 HTML 也是 XML 文档的一种形式。因此,为了在 HTML 页面上识别元素,我们需要使用特定的 XPath 语法:
-
根元素被标识为
//。 -
要识别所有 div 元素,语法将是
//div。 -
要识别位于 div 元素内的链接标签,语法将是
//div/a。 -
要识别所有具有标签的元素,我们使用
*。语法将是//div/*。 -
要识别从根向下三级的所有 div 元素,我们可以使用
//*/*/div。 -
要识别特定元素,我们使用这些元素的属性值,例如
//*/div/a[@id='attrValue'],这将返回锚点元素。该元素位于根元素内部的div元素中,并且具有id值为attrValue。
因此,我们需要将 XPath 表达式传递给 By.xpath 定位机制,使其能够识别我们的目标元素。
现在,让我们看看代码示例和 WebDriver 如何使用这个 XPath 来识别元素:
@Test
public void byXPathLocatorExample() {
WebElement searchBox =
driver.findElement(By.xpath("//*[@id='search']"));
searchBox.sendKeys("Bags");
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Bags'");
}
在前面的代码中,我们正在使用 By.xpath 定位机制并将 WebElement 的 XPath 传递给它。
使用 XPath 的一个缺点是它在时间上代价很高。对于要识别的每个元素,WebDriver 实际上都会扫描整个页面,这非常耗时,并且过多地在测试脚本中使用 XPath 会使执行速度变得非常慢。
By.cssSelector() 方法
By.cssSelector() 方法在用法上与 By.xpath() 方法类似,但不同之处在于它比 By.xpath 定位机制略快。以下是一些常用的语法来识别元素:
-
要识别具有
#flrsID 的 div 元素,我们使用#flrs语法 -
要识别子锚点元素,我们使用
#flrs > a语法,这将返回链接元素 -
要识别具有其属性的锚点元素,我们使用
#flrs > a[a[href="/intl/en/about.html"]]语法
让我们尝试修改之前的代码,该代码使用 XPath 定位机制,改为使用 cssSelector 机制:
@Test
public void byCssSelectorLocatorExample() {
WebElement searchBox =
driver.findElement(By.cssSelector("#search"));
searchBox.sendKeys("Bags");
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Bags'");
}
前面的代码使用了 By.cssSelector 定位机制,它使用搜索框的 css 选择器 ID。
让我们来看一个稍微复杂一点的例子。我们将尝试在主页上识别“关于我们”:
@Test
public void byCssSelectorLocatorComplexExample() {
WebElement aboutUs =
driver.findElement(By
.cssSelector("a[href*='/about-magento-demo-store/']"));
aboutUs.click();
assertThat(driver.getTitle())
.isEqualTo("About Us");
}
之前的代码使用了cssSelector()方法来查找通过其href属性标识的锚元素。
与 WebElement 交互
在上一节中,我们看到了如何通过不同的定位方法在网页上定位 WebElement。在这里,我们将看到可以对 WebElement 执行的所有不同用户操作。不同的 WebElement 将具有不同的操作。例如,在一个文本框元素中,我们可以输入一些文本或者清除已经输入的文本。同样,对于按钮,我们可以点击它,获取它的尺寸等,但我们不能在按钮中输入文本,对于链接,我们也不能在它中输入文本。所以,尽管所有操作都列在了一个 WebElement 接口中,但使用目标元素支持的操作是测试脚本开发者的责任。如果我们尝试在 WebElement 上执行错误操作,我们不会看到任何异常或错误抛出,也不会看到任何操作被执行;WebDriver 会静默忽略这些操作。
现在,让我们通过查看它们的 Javadocs 和代码示例来逐个了解这些操作。
获取元素属性和属性值
在本节中,我们将学习从 WebElement 接口检索值和属性的各种方法。
getAttribute()方法
getAttribute方法可以在所有 WebElement 上执行。记住,我们在 WebElements 部分看到了 WebElement 的属性。HTML 属性是 HTML 元素的修饰符。它们通常是出现在元素起始标签中的键值对。例如:
<label name="Username" id="uname">Enter Username: </label>
在之前的代码中,name和id是属性或属性键,而Username和uname是属性值。
getAttribute()方法的 API 语法如下:
java.lang.String getAttribute(java.lang.String name)
在之前的代码中,输入参数是String,它是属性的名字。返回类型同样是String,它是属性值。
现在让我们看看如何使用 WebDriver 获取一个 WebElement 的所有属性。在这里,我们将使用示例应用程序中的搜索框。这个元素看起来是这样的:
<input id="search" type="search" name="q" value="" class="input-text required-entry" maxlength="128" placeholder="Search entire store here..." autocomplete="off">
我们将使用 WebDriver 列出这个 WebElement 的所有属性。相应的代码如下:
@Test
public void elementGetAttributesExample() {
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Name of the box is: "
+ searchBox.getAttribute("name"));
System.out.println("Id of the box is: " + searchBox.getAttribute("id"));
System.out.println("Class of the box is: "
+ searchBox.getAttribute("class"));
System.out.println("Placeholder of the box is: "
+ searchBox.getAttribute("placeholder"));
}
在之前的代码中,最后四行代码使用了getAttribute()方法来获取 WebElement 搜索框的name、id、class和placeholder属性的属性值。之前代码的输出将是以下内容:
Name of the box is: q
Id of the box is: search
Class of the box is: input-text required-entry
Placeholder of the box is: Search entire store here...
回到上一节的By.tagName()方法,如果通过定位机制By.tagName搜索的结果超过一个,你可以使用getAttribute()方法进一步过滤结果并到达你确切想要的目标元素。
getText()方法
getText 方法可以从所有 WebElement 中调用。如果元素上包含任何文本,它将返回可见文本,否则将返回空值。getText() 方法的 API 语法如下:
java.lang.String getText()
上述方法没有输入参数,但如果 WebElement 中有任何可见的 innerText 字符串,它将返回该字符串,否则将返回空字符串。
以下代码示例用于获取示例应用程序首页上存在的站点通知元素上的文本:
@Test
public void elementGetTextExample() {
WebElement siteNotice = driver.findElement(By
.className("global-site-notice"));
System.out.println("Complete text is: "
+ siteNotice.getText());
}
上述代码使用 getText() 方法获取站点通知元素上的文本,它返回以下内容:
Complete text is: This is a demo store. Any orders placed through this store will not be honored or fulfilled.
getCssValue() 方法
getCssValue 方法可以在所有 WebElement 上调用。此方法用于从 WebElement 中获取 CSS 属性值。CSS 属性可以是 font-family、background-color、color 等。当您想通过测试脚本验证应用于 WebElement 的 CSS 样式时,这非常有用。getCssValue() 方法的 API 语法如下:
java.lang.String getCssValue(java.lang.String propertyName)
在上述代码中,输入参数是 CSS 属性名称的字符串值,返回类型是分配给该属性名称的值。
以下是从搜索框检索文本 font-family 的代码示例:
@Test
public void elementGetCssValueExample() {
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Font of the box is: "
+ searchBox.getCssValue("font-family"));
}
上述代码使用 getCssValue() 方法查找搜索框中可见文本的 font-family,方法输出如下:
Font of the box is: Raleway, "Helvetica Neue", Verdana, Arial, sans-serif
getLocation() 方法
getLocation 方法可以在所有 WebElement 上执行。这用于获取元素在网页上渲染的相对位置。这个位置是相对于网页的左上角计算的,其中 (x, y) 坐标假定为 (0, 0)。如果您的测试脚本尝试验证网页布局,此方法将非常有用。
getLocation() 方法的 API 语法如下:
Point getLocation()
上述方法显然不需要任何输入参数,但返回类型是一个包含元素 (x, y) 坐标的 Point 类。
以下代码用于检索搜索框的位置:
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Location of the box is: "
+ searchBox.getLocation());
上述代码的输出是搜索框的 (x, y) 位置,如下截图所示:
Location of the box is: (873, 136)
getSize() 方法
getSize 方法也可以在所有可见的 HTML 组件上调用。它将返回渲染的 WebElement 的宽度和高度。getSize() 方法的 API 语法如下:
Dimension getSize()
上述方法不接收任何输入参数,返回类型是一个名为 Dimension 的类实例。该类包含目标 WebElement 的宽度和高度。以下代码用于获取搜索框的宽度和高度:
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Size of the box is: "
+ searchBox.getSize());
上述代码的输出是搜索框的宽度和高度,如下截图所示:
Size of the box is: (281, 40)
getTagName() 方法
getTagName 方法可以从所有 WebElements 中调用。这将返回 WebElements 的 HTML 标签名。例如,在以下 HTML 代码中,按钮是 HTML 元素的标签名:
<button id="gbqfba" class="gbqfba" name="btnK" aria-label="Google Search">
在前面的代码中,按钮是 HTML 元素的标签名。
getTagName() 方法的 API 语法如下:
java.lang.String getTagName()
前面方法的返回类型是 String,它返回目标元素的标签名。
以下代码返回搜索按钮的标签名:
@Test
public void elementGetTagNameExample() {
WebElement searchButton = driver.findElement(By.className("search-button"));
System.out.println("Html tag of the button is: "
+ searchButton.getTagName());
}
前面的代码使用了 getTagName() 方法来获取搜索按钮元素的标签名。代码的输出符合预期:
Html tag of the button is: button
对 WebElements 执行操作
在上一节中,我们看到了如何检索 WebElements 的值或属性。在本节中,我们将看到如何对 WebElements 执行操作,这是自动化中最关键的部分。让我们探索 WebElement 接口中的各种方法。
sendKeys() 方法
sendKeys 操作适用于 textbox 或 textarea HTML 元素。这是用来在文本框中输入文本的。这会模拟用户的键盘输入,将文本输入到 WebElements 中,就像用户自己输入一样。sendKeys() 方法的 API 语法如下:
void sendKeys(java.lang.CharSequence...keysToSend)
前面方法的输入参数是 CharSequence 类型的文本,该文本需要输入到元素中。此方法不返回任何内容。现在,让我们通过一个代码示例来看看如何使用 sendKeys() 方法在搜索框中输入搜索文本:
@Test
public void elementSendKeysExample() {
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
在前面的代码中,sendKeys() 方法用于在网页的文本框元素中输入所需的文本。这就是我们处理普通键的方式,但如果你想要输入一些特殊键,例如 退格键、回车键、制表键 或 Shift 键,我们需要使用 WebDriver 的一个特殊枚举类,名为 Keys。使用 Keys 枚举,你可以在输入到 WebElement 时模拟许多特殊键。
现在让我们看看一些代码示例,它使用 Shift 键在搜索框中输入大写文本:
@Test
public void elementSendKeysCompositeExample() {
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys(Keys.chord(Keys.SHIFT,"phones"));
searchBox.submit();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'PHONES'");
}
在前面的代码中,使用了 Keys 枚举中的 chord() 方法来输入键,同时指定的文本作为输入被赋予文本框。在你的环境中尝试这个操作,以查看所有输入的文本都变为大写。
clear() 方法
clear 操作类似于 sendKeys() 方法,适用于 textbox 和 textarea 元素。这是用来清除使用 sendKeys() 方法输入到 WebElement 中的文本。这可以通过使用 Keys.BACK_SPACE 枚举实现,但 WebDriver 已经为我们提供了一个清除文本的显式方法。clear() 方法的 API 语法如下:
void clear()
此方法不接收任何输入也不返回任何输出。它只是简单地在对目标文本输入元素上执行。
现在,让我们看看如何清除搜索框中输入的文本。相应的代码示例如下:
@Test
public void elementClearExample() {
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys(Keys.chord(Keys.SHIFT,"phones"));
searchBox.clear();
}
我们使用了 WebElement 的 clear() 方法在将 phones 输入到搜索框后清除文本。
submit() 方法
submit() 动作可以在 Form 或 Form 元素内的元素上执行。这用于将网页表单提交到托管 web 应用的服务器。submit() 方法的 API 语法如下:
void submit()
前面的方法不接收任何输入参数,也不返回任何内容。但是,当这个方法在一个不在表单内的 WebElement 上执行时,会抛出 NoSuchElementException。
现在,让我们通过一个代码示例来提交搜索页面上的表单:
@Test
public void elementSubmitExample() {
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys(Keys.chord(Keys.SHIFT,"phones"));
searchBox.submit();
}
在前面的代码中,接近结尾的部分是使用 submit() 方法将搜索表单提交到应用服务器的位置。现在,尝试在一个元素上执行 submit() 方法,比如关于链接,它不是任何表单的一部分。我们应该看到抛出了 NoSuchElementException。所以,当你使用 submit() 方法在 WebElement 上时,请确保它是 Form 元素的一部分。
检查 WebElement 状态
在前面的章节中,我们看到了如何检索值和执行 WebElements 的操作。现在,我们将看到如何检查 WebElement 的状态。我们将探索检查 WebElement 是否在浏览器窗口中显示、是否可编辑的方法,以及如果 WebElement 是单选按钮或复选框,我们可以确定它是选中还是未选中。让我们看看如何使用 WebElement 接口中的可用方法。
isDisplayed() 方法
isDisplayed 动作用于验证网页上的元素是否显示,并且可以在所有 WebElements 上执行。isDisplayed() 方法的 API 语法如下:
boolean isDisplayed()
前面的方法返回一个 Boolean 值,指定目标元素是否在网页上显示。以下是对 Search 框是否显示的验证代码,在这种情况下显然应该返回 true:
@Test
public void elementStateExample() {
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Search box is displayed: "
+ searchBox.isDisplayed());
}
前面的代码使用 isDisplayed() 方法来确定元素是否在网页上显示。前面的代码对 Search 框返回 true:
Search box is displayed: true
isEnabled() 方法
isEnabled 动作用于验证网页上的元素是否启用,并且可以在所有 WebElements 上执行。isEnabled() 方法的 API 语法如下:
boolean isEnabled()
前面的方法返回一个 Boolean 值,指定目标元素是否在网页上启用。以下是对 Search 框是否启用的验证代码,在这种情况下显然应该返回 true:
@Test
public void elementStateExample() {
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Search box is enabled: "
+ searchBox.isEnabled());
}
前面的代码使用 isEnabled() 方法来确定元素是否在网页上启用。前面的代码对 Search 框返回 true:
Search box is enabled: true
isSelected() 方法
如果网页上的元素被选中,isSelected 方法返回一个 boolean 值,并且只能在单选按钮、选择框中的选项和复选框 WebElement 上执行。在其他元素上执行时,它将返回 false。isSelected() 方法的 API 语法如下:
boolean isSelected()
上述方法返回一个 Boolean 值,指定目标元素是否在网页上被选中。以下代码用于验证搜索框是否在搜索页面上被选中:
@Test
public void elementStateExample() {
WebElement searchBox = driver.findElement(By.name("q"));
System.out.println("Search box is selected: "
+ searchBox.isSelected());
}
上述代码使用了 isSelected() 方法。对于搜索框,它返回 false,因为这不是单选按钮、选择框中的选项或复选框。上述代码对搜索框返回 false:
Search box is selected: false
要选择复选框或单选按钮,我们需要调用 WebElement.click() 方法,它切换元素的状态。我们可以使用 isSelected() 方法来查看它是否被选中。
摘要
在本章中,我们简要概述了 Selenium 测试工具,以及 WebDriver 和 WebElement 的架构。我们学习了如何使用 Eclipse、Maven 和 TestNG 设置测试开发环境。这将为我们使用 Selenium 构建测试框架奠定基础。然后,我们看到了如何定位元素,以及可以对它们执行的操作。这是自动化 Web 应用程序时最重要的方面。在本章中,我们使用了 ChromeDriver 来运行我们的测试。在下一章中,我们将学习如何配置和运行 Mozilla Firefox、Microsoft IE 和 Edge 以及 Apple Safari 上的测试。
问题
-
对或错:Selenium 是一个浏览器自动化库。
-
Selenium 提供了哪些不同类型的定位机制?
-
对或错:使用
getAttribute()方法,我们也可以读取 CSS 属性吗? -
在一个 WebElement 上可以执行哪些操作?
-
我们如何确定复选框是被勾选还是未勾选?
更多信息
您可以查看以下链接,获取本章涵盖主题的更多信息:
-
在
www.w3.org/TR/webdriver/阅读 WebDriver 规范。 -
在 Mark Collin 的《精通 Selenium WebDriver》一书中,了解更多关于在 第一章,创建更快的反馈循环 中使用 TestNG 和 Maven 的信息。
-
在 Unmesh Gundecha 的《Selenium 测试工具食谱》第 2 版中,了解更多关于元素交互的信息,包括 第二章,查找元素 和 第三章,与元素协作。
第二章:可用的不同 WebDriver
上一章介绍了 Selenium WebDriver 架构和 WebDriver 接口。我们使用 ChromeDriver 和 Google Chrome 创建了一个简单的测试。在本章中,我们将探讨 Mozilla Firefox、Microsoft Internet Explorer、Microsoft Edge 和 Safari 的 WebDriver 实现。随着 WebDriver 成为 W3C 规范,所有主要浏览器厂商现在都原生支持 WebDriver。在本章中,我们将探讨以下内容:
-
使用 Mozilla Firefox、Google Chrome、Microsoft Internet Explorer 和 Edge 以及 Apple Safari 的特定驱动程序实现
-
使用浏览器选项类以无头模式执行测试并使用自定义配置文件
-
使用 Google Chrome 进行移动仿真
Firefox 驱动程序
Firefox 驱动的实现已在 Selenium 3.0 中更改。从 Firefox 版本 47.0 开始,我们需要使用一个单独的驱动程序,该驱动程序将与 Firefox 浏览器交互,类似于 ChromeDriver。新的 Firefox 驱动程序称为 Geckodriver。
Geckodriver 提供了由 W3C WebDriver 协议描述的 HTTP API,用于与 Gecko 浏览器(如 Firefox)通信。它通过充当本地和远程端之间的代理,将调用转换为 Firefox 远程协议(Marionette)。
使用 GeckoDriver
在本节中,我们将了解如何配置和使用Geckodriver在 Firefox 测试中。首先,我们需要从 github.com/mozilla/geckodriver/releases 下载Geckodriver可执行文件。
根据您计算机上安装的 Firefox 版本以及操作系统,下载适当的Geckodriver版本。将可执行文件复制到/src/test/resources/drivers文件夹中。
我们将使用我们在第一章中创建的搜索测试,并修改测试以使用Geckodriver。为此,我们需要修改setup()方法,在webdriver.gecko.driver属性中提供Geckodriver二进制文件的路径,并实例化FirefoxDriver类:
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver.exe");
driver = new FirefoxDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
现在执行测试,你将在控制台看到Geckodriver正在运行:
1532165868138 geckodriver INFO geckodriver 0.21.0
1532165868147 geckodriver INFO Listening on 127.0.0.1:36466
它将启动一个新的 Firefox 窗口并执行测试。执行结束后,Firefox 窗口将被关闭。
使用无头模式
无头模式是使用 Selenium WebDriver 进行自动化测试时运行 Firefox 的一种非常有用的方式。在无头模式下,Firefox 运行得像正常一样,只是你看不到 UI 组件。这使得 Firefox 运行更快,测试运行更高效,尤其是在 CI(持续集成)环境中。
我们可以通过配置FirefoxOptions类以无头模式运行 Selenium 测试,如下代码片段所示:
@BeforeMethod
public void setup() {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setHeadless(true);
driver = new FirefoxDriver(firefoxOptions);
driver.get("http://demo-store.seleniumacademy.com/");
}
在前面的代码中,我们首先创建了一个FirefoxOptions类的实例,调用setHeadless()方法,将值设置为true以在无头模式下启动 Firefox 浏览器。您将看到一个长消息,指示浏览器实例已在无头模式下启动,如下面的控制台输出所示:
1532194389309 geckodriver INFO geckodriver 0.21.0
1532194389317 geckodriver INFO Listening on 127.0.0.1:21734
1532194390907 mozrunner::runner INFO Running command: "/Applications/Firefox.app/Contents/MacOS/firefox-bin" "-marionette" "-headless" "-foreground" "-no-remote" "-profile" "/var/folders/zr/rdwhsjk54k5bj7yr34rfftrh0000gn/T/rust_mozprofile.DmJCQRKVVRs6"
*** You are running in headless mode.
在执行过程中,您将不会在屏幕上看到 Firefox 窗口,但测试将在无头模式下执行。
理解 Firefox 配置文件
Firefox 配置文件是 Firefox 浏览器用来存储所有密码、书签、设置和其他用户数据的文件夹。Firefox 用户可以创建任意数量的配置文件,并使用不同的自定义设置。根据 Mozilla 的说法,以下是可以存储在配置文件中的不同属性:
-
书签和浏览历史记录
-
密码
-
站点特定首选项
-
搜索引擎
-
个人词典
-
自动完成历史记录
-
下载历史记录
-
饼干
-
DOM 存储
-
安全证书设置
-
安全设备设置
-
下载操作
-
插件 MIME 类型
-
存储的会话
-
工具栏自定义
-
用户样式
要创建、重命名或删除配置文件,您必须执行以下步骤:
- 打开 Firefox 配置文件管理器。为此,在命令提示符终端中,导航到 Firefox 的安装目录;通常,如果您在 Windows 上,它位于程序文件中。导航到可以找到
firefox二进制文件的位置,并执行以下命令:
/path/to/firefox -p
它将打开配置文件管理器,外观如下面的截图所示:

注意,在执行上述命令之前,您需要确保关闭所有当前运行的 Firefox 实例。
- 使用“创建配置文件...”按钮创建另一个配置文件,使用“重命名配置文件...”按钮重命名现有配置文件,使用“删除配置文件...”按钮删除一个。
因此,回到我们的 WebDriver,每次我们创建一个FirefoxDriver实例时,都会创建一个临时配置文件,并由 WebDriver 使用。要查看当前正在使用的 Firefox 实例的配置文件,请转到帮助 | 故障排除信息。
这将启动特定 Firefox 实例的所有详细信息,其中配置文件是其一部分。它看起来类似于以下截图:

前一个截图中的突出显示的椭圆形显示了配置文件文件夹。单击“显示文件夹”按钮;它应该打开与当前 Firefox 实例对应的配置文件位置。现在,让我们使用我们的 FirefoxDriver 启动一个 Firefox 浏览器实例,并验证其配置文件位置。
让我们使用以下代码启动 Firefox 浏览器:
public class FirefoxProfile {
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxDriver driver = new FirefoxDriver();
driver.get("http://www.google.com");
}
}
这将启动一个浏览器实例。现在,导航到帮助 | 故障排除信息,一旦信息被启动,点击显示文件夹按钮。这将打开当前 WebDriver 的配置文件目录。每次您使用 Firefox Driver 启动 Firefox 实例时,它都会为您创建一个新的配置文件。如果您向上移动一个目录级别,您将看到 FirefoxDriver 创建的配置文件,如下截图所示:

所有的前述文件夹都对应于 FirefoxDriver 启动的每个 Firefox 实例。
到目前为止,我们已经看到了 Firefox 配置文件是什么以及 WebDriver 每次启动浏览器时如何创建一个。现在,让我们看看如何使用 WebDriver API 创建我们自己的自定义配置文件。以下是一个使用 WebDriver 库创建自己的 Firefox 配置文件并设置您希望浏览器拥有的选项的代码示例,覆盖 FirefoxDriver 提供的选项:
public class FirefoxCustomProfile {
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxProfile profile = new FirefoxProfile();
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(profile);
FirefoxDriver driver = new FirefoxDriver(firefoxOptions);
try {
driver.get("http://www.google.com");
} finally {
driver.quit();
}
}
}
在前面的代码中,FirefoxProfile 是一个类,它已经被实例化以创建从测试中启动的 Firefox 浏览器的自定义配置文件。现在,拥有该类的实例后,我们可以在其中设置各种选项和首选项,我们将在稍后讨论。首先,FirefoxProfile 有两个构造函数的重载版本。一个创建一个空配置文件并根据需求对其进行塑形。这在前面的代码中可以看到。第二个版本从一个现有的配置文件目录创建配置文件实例,如下所示:
public FirefoxProfile(java.io.File profileDir)
这里,profileDir 输入参数是现有配置文件的目录位置。配置文件目录是我们在前面的截图中所看到的。让我们讨论一些我们可以对 Firefox 浏览器进行的有趣的自定义设置。
向 Firefox 添加扩展
在本节中,我们将看到如何使用配置文件扩展我们的 Firefox 浏览器以获得一些额外的功能。每当 WebDriver 启动一个新的 Firefox 浏览器时,它都会在磁盘上创建一个新的配置文件,而这个配置文件不包含安装的任何 Firefox 扩展。我们将使用配置文件在 WebDriver 每次创建 Firefox 浏览器的实例时添加一个扩展。
现在,让我们使用FirefoxProfile提供的addExtension()方法更改配置文件。此方法用于向 Firefox 浏览器添加扩展。
以下是为该方法提供的 API 语法:
public void addExtension(java.io.File extensionToInstall) throws java.io.IOException
输入参数是要安装到 Firefox 浏览器上的 XPI 文件。如果 WebDriver 在指定位置找不到文件,它将引发IOException。以下是将默认配置文件覆盖并扩展 Firefox 浏览器以包含名为 Xpath Finder 的扩展的代码:
public class FirefoxCustomProfile {
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxProfile profile = new FirefoxProfile();
profile.addExtension(
new File("./src/test/resources/extensions/xpath_finder.xpi"));
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(profile);
FirefoxDriver driver = new FirefoxDriver(firefoxOptions);
try {
driver.get("http://www.google.com");
} finally {
//driver.quit();
}
}
}
现在,如果您看到由 FirefoxDriver 启动的 Firefox 浏览器,您将发现 Xpath Finder 扩展已安装在其上。在控制台日志中,您将看到一个消息指示扩展已被添加到浏览器中:
1532196699704 addons.xpi-utils DEBUG 新增插件 xPathFinder@0.9.3 安装在 app-profile
存储和检索配置文件
我们还可以将浏览器的配置文件信息写入 JSON 文件,然后使用相同的配置文件实例化新的浏览器。FirefoxProfile类提供了一个方法来将配置文件信息导出为 JSON。以下是其 API 语法:
public String toJson()
输出或返回类型是一个包含 JSON 信息的字符串。
现在,为了创建具有相同配置文件的浏览器,FirefoxProfile类提供了一个静态方法,它接受 JSON 字符串作为输入。以下是其 API 语法:
public static FirefoxProfile fromJson(java.lang.String json) throws java.io.IOException
这是一个在FirefoxProfile类中的静态方法,它接受一个 JSON 字符串来创建一个配置文件。以下是该方法的代码示例:
FirefoxProfile profile = new FirefoxProfile();
profile.addExtension(
new File("./src/test/resources/extensions/xpath_finder.xpi"));
String json = profile.toJson();
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(FirefoxProfile.fromJson(json));
在前面的代码中,我们已经将配置文件导出为 JSON 字符串。在您的测试用例中,您可以将该 JSON 信息写入文件并存储它。稍后,您可以使用FirefoxOptions读取该 JSON 文件,并从中创建FirefoxDriver。
处理 Firefox 偏好设置
到目前为止,我们已经了解了 Firefox 配置文件,以及我们如何为 FirefoxDriver 创建自己的自定义配置文件。现在,让我们看看我们如何在创建的配置文件中设置我们的偏好设置,以及 FirefoxDriver 将它们存储在哪里。
根据 Mozilla 的说法,Firefox 偏好设置是指用户可以设置的任何值或定义的行为。这些值被保存到偏好设置文件中。如果您通过导航到帮助 | 故障排除信息并点击显示文件夹按钮来打开配置目录,您将看到两个偏好设置文件:prefs.js和user.js。所有用户偏好设置都是在 Firefox 应用程序启动期间写入到prefs.js文件的。用户可以覆盖这些值以选择自己的值,并且它们被存储在user.js文件中。对于特定偏好设置的user.js中的值优先于为该特定偏好设置设置的其它所有值。因此,您的 FirefoxDriver 会为您覆盖 Firefox 在user.js文件中的所有默认偏好设置。当您添加一个新的偏好设置时,FirefoxDriver 会将它写入到user.js偏好设置文件中,Firefox 浏览器会相应地表现。
打开配置目录下的user.js文件。以下是由 FirefoxDriver 默认为您设置的偏好设置列表:
user_pref("app.normandy.api_url", "");
user_pref("app.update.auto", false);
user_pref("app.update.enabled", false);
user_pref("browser.EULA.3.accepted", true);
user_pref("browser.EULA.override", true);
user_pref("browser.displayedE10SNotice", 4);
user_pref("browser.dom.window.dump.enabled", true);
user_pref("browser.download.manager.showWhenStarting", false);
user_pref("browser.laterrun.enabled", false);
user_pref("browser.link.open_external", 2);
user_pref("browser.link.open_newwindow", 2);
user_pref("browser.newtab.url", "about:blank");
user_pref("browser.newtabpage.enabled", false);
user_pref("browser.offline", false);
user_pref("browser.reader.detectedFirstArticle", true);
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
user_pref("browser.safebrowsing.downloads.enabled", false);
user_pref("browser.safebrowsing.enabled", false);
user_pref("browser.safebrowsing.malware.enabled", false);
user_pref("browser.safebrowsing.passwords.enabled", false);
user_pref("browser.safebrowsing.phishing.enabled", false);
user_pref("browser.search.update", false);
user_pref("browser.selfsupport.url", "");
user_pref("browser.sessionstore.resume_from_crash", false);
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("browser.showQuitWarning", false);
user_pref("browser.snippets.enabled", false);
user_pref("browser.snippets.firstrunHomepage.enabled", false);
user_pref("browser.snippets.syncPromo.enabled", false);
user_pref("browser.startup.homepage", "about:blank");
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("browser.startup.page", 0);
user_pref("browser.tabs.closeWindowWithLastTab", false);
user_pref("browser.tabs.warnOnClose", false);
user_pref("browser.tabs.warnOnOpen", false);
user_pref("browser.uitour.enabled", false);
user_pref("browser.usedOnWindows10.introURL", "about:blank");
user_pref("browser.warnOnQuit", false);
user_pref("datareporting.healthreport.about.reportUrl", "http://%(server)s/dummy/abouthealthreport/");
user_pref("datareporting.healthreport.documentServerURI", "http://%(server)s/dummy/healthreport/");
user_pref("datareporting.healthreport.logging.consoleEnabled", false);
user_pref("datareporting.healthreport.service.enabled", false);
user_pref("datareporting.healthreport.service.firstRun", false);
user_pref("datareporting.healthreport.uploadEnabled", false);
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
user_pref("devtools.errorconsole.enabled", true);
user_pref("dom.disable_open_during_load", false);
user_pref("dom.ipc.reportProcessHangs", false);
user_pref("dom.max_chrome_script_run_time", 30);
user_pref("dom.max_script_run_time", 30);
user_pref("dom.report_all_js_exceptions", true);
user_pref("extensions.autoDisableScopes", 10);
user_pref("extensions.blocklist.enabled", false);
user_pref("extensions.checkCompatibility.nightly", false);
user_pref("extensions.enabledScopes", 5);
user_pref("extensions.installDistroAddons", false);
user_pref("extensions.logging.enabled", true);
user_pref("extensions.shield-recipe-client.api_url", "");
user_pref("extensions.showMismatchUI", false);
user_pref("extensions.update.enabled", false);
user_pref("extensions.update.notifyUser", false);
user_pref("focusmanager.testmode", true);
user_pref("general.useragent.updates.enabled", false);
user_pref("geo.provider.testing", true);
user_pref("geo.wifi.scan", false);
user_pref("hangmonitor.timeout", 0);
user_pref("javascript.enabled", true);
user_pref("javascript.options.showInConsole", true);
user_pref("marionette.log.level", "INFO");
user_pref("marionette.port", 51549);
user_pref("network.captive-portal-service.enabled", false);
user_pref("network.http.phishy-userpass-length", 255);
user_pref("network.manage-offline-status", false);
user_pref("network.sntp.pools", "%(server)s");
user_pref("offline-apps.allow_by_default", true);
user_pref("plugin.state.flash", 0);
user_pref("prompts.tab_modal.enabled", false);
user_pref("security.csp.enable", false);
user_pref("security.fileuri.origin_policy", 3);
user_pref("security.fileuri.strict_origin_policy", false);
user_pref("services.settings.server", "http://%(server)s/dummy/blocklist/");
user_pref("signon.rememberSignons", false);
user_pref("startup.homepage_welcome_url", "");
user_pref("startup.homepage_welcome_url.additional", "about:blank");
user_pref("toolkit.networkmanager.disable", true);
user_pref("toolkit.startup.max_resumed_crashes", -1);
user_pref("toolkit.telemetry.enabled", false);
user_pref("toolkit.telemetry.prompted", 2);
user_pref("toolkit.telemetry.rejected", true);
user_pref("toolkit.telemetry.server", "https://%(server)s/dummy/telemetry/");
user_pref("webdriver_accept_untrusted_certs", true);
user_pref("webdriver_assume_untrusted_issuer", true);
user_pref("xpinstall.signatures.required", false);
user_pref("xpinstall.whitelist.required", false);
这个 FirefoxDriver 将它们视为冻结的偏好设置,并且不允许测试脚本开发者更改它们。然而,在先前的列表中,FirefoxDriver 允许您更改一些偏好设置,我们将在稍后看到。
设置偏好设置
现在,我们将学习如何设置我们自己的偏好。作为一个例子,我们将看到如何更改浏览器的用户代理。这些天,许多 Web 应用程序都有一个主站以及一个移动站/m.站。应用程序将验证传入请求的用户代理,并决定是否作为普通站或移动站的服务器。因此,为了从您的笔记本电脑或桌面浏览器测试您的移动站,您只需更改您的用户代理。让我们看一个代码示例,我们可以使用 FirefoxDriver 更改我们的 Firefox 浏览器的用户代理偏好,并发送请求到 Facebook 主页。但在那之前,让我们看看 FirefoxProfile 类提供的setPreference()方法:
public void setPreference(java.lang.String key, String value)
输入参数是key,它是一个字符串,代表您的偏好,以及value,它必须设置为偏好值。
上述方法有两个其他重载版本,其中一个如下所示:
public void setPreference(java.lang.String key, int value)
这里是另一个重载版本:
public void setPreference(java.lang.String key,boolean value)
现在,使用前面的setPreference()方法,我们将尝试使用以下代码更改我们浏览器的用户代理:
public class SettingPreferences {
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxProfile profile = new FirefoxProfile();
profile.setPreference("general.useragent.override",
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) " +
"AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 " +
"Mobile/15A356 Safari/604.1");
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(profile);
FirefoxDriver driver = new FirefoxDriver(firefoxOptions);
driver.get("http://facebook.com");
}
}
在setPreference()方法的先前代码中,general.useragent.override被设置为偏好的名称,第二个参数是那个偏好的值,它代表 iPhone 用户代理。现在打开这个特定 Firefox 实例的user.js文件,您将看到这个偏好的条目。您应该在您的user.js文件中使用以下偏好:
user_pref("general.useragent.override", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1");
除了这个之外,您还会观察到 Facebook 主页的移动版本已经被提供给您。
理解冻结偏好
现在,让我们回到之前看到的user.js中包含的冻结偏好列表。Firefox Driver 认为测试脚本开发者不需要处理这些偏好,并且不允许更改这些值。让我们选择一个冻结偏好,并尝试在我们的代码中更改其值。让我们考虑browser.shell.checkDefaultBrowser偏好,FirefoxDriver 实现者认为应该将其设置为false,这样 Firefox 浏览器就不会在您忙于执行测试用例时询问您是否要将 Firefox 设置为默认浏览器(如果尚未设置)。最终,您不需要在测试脚本中处理这个弹出窗口本身。除了将偏好值设置为false之外,FirefoxDriver 的实现者还认为应该冻结这个值,这样用户就不会更改这些值。这就是为什么这些偏好被称为冻结偏好。现在,如果您尝试在测试脚本中修改这些值会发生什么?让我们看一个代码示例:
public class FirefoxFrozenPreferences {
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxProfile profile = new FirefoxProfile();
profile.setPreference("browser.shell.checkDefaultBrowser", true);
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(profile);
FirefoxDriver driver = new FirefoxDriver(firefoxOptions);
driver.get("http://facebook.com");
}
}
现在当您执行代码时,您将立即看到一个异常,表示不允许覆盖这些值。以下是在您将看到的异常堆栈跟踪:
Exception in thread "main" java.lang.IllegalArgumentException: Preference browser.shell.checkDefaultBrowser may not be overridden: frozen value=false, requested value=true
这就是 FirefoxDriver 强制设置了一些不希望被修改的偏好设置。然而,有一些冻结列表中的偏好设置,FirefoxDriver 允许通过代码进行修改。为此,它明确地在 FirefoxProfile 类中公开了方法。这些豁免的偏好设置是用于处理 SSL 证书和原生事件。在这里,我们将看到如何覆盖 SSL 证书的偏好设置。
让我们使用一个代码示例来尝试覆盖默认的 Firefox 行为以处理 SSL 证书。FirefoxProfile 类有两个处理 SSL 证书的方法;第一个方法如下:
public void setAcceptUntrustedCertificates(boolean acceptUntrustedSsl)
这让 Firefox 知道是否接受不受信任的 SSL 证书。默认情况下,它被设置为 true,即 Firefox 接受不受信任的 SSL 证书。第二个方法如下:
public void setAssumeUntrustedCertificateIssuer(boolean untrustedIssuer)
这让 Firefox 假设不受信任的证书是由不受信任或自签名的证书代理机构签发的。默认情况下,Firefox 假设签发者是受信任的。这种假设在测试测试环境中的应用程序时,使用生产环境的证书尤其有用。
偏好设置 webdriver_accept_untrusted_certs 和 webdriver_assume_untrusted_issuer 与 SSL 证书相关。现在,让我们创建一个 Java 代码来修改这两个值。默认情况下,这些值被设置为 true,如 user.js 文件中所示。让我们用以下代码将它们标记为 false:
public static void main(String... args) {
System.setProperty("webdriver.gecko.driver",
"./src/test/resources/drivers/geckodriver 2");
FirefoxProfile profile = new FirefoxProfile();
profile.setAssumeUntrustedCertificateIssuer(false);
profile.setAcceptUntrustedCertificates(false);
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setProfile(profile);
FirefoxDriver driver = new FirefoxDriver(firefoxOptions);
driver.get("http://facebook.com");
}
在这里,我们将值设置为 false,现在如果你打开该实例的配置文件目录中的 user.js 文件,你会看到设置为 false 的值,如下所示:
user_pref("webdriver_accept_untrusted_certs", false);
user_pref("webdriver_assume_untrusted_issuer", false);
Chrome 驱动程序
ChromeDriver 与 Geckodriver 的工作方式相似,并实现了 W3C WebDriver 协议。我们在第一章中介绍了如何设置和使用 ChromeDriver。在本节中,我们将重点关注 ChromeDriver 选项,以在无头模式下运行测试、使用移动仿真以及使用自定义配置文件。
使用无头模式
与 Firefox 类似,我们可以使用 ChromeDriver 在无头模式下运行测试。这使得 Chrome 测试运行得更快,测试运行得更有效率,尤其是在 CI(持续集成)环境中。
我们可以通过配置 ChromeOptions 类来在无头模式下运行 Selenium 测试,如下面的代码片段所示:
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setHeadless(true);
driver = new ChromeDriver(chromeOptions);
driver.get("http://demo-store.seleniumacademy.com/");
}
在前面的代码中,我们首先创建了一个 ChromeOptions 类的实例,调用 setHeadless() 方法,将值设置为 true 以在无头模式下启动 Chrome 浏览器。在执行过程中,你将不会在屏幕上看到 Chrome 窗口,但测试将在无头模式下执行。
使用移动仿真进行移动网页应用测试
Chrome 浏览器允许用户通过 DevTools 从桌面版本的 Chrome 模拟移动设备上的 Chrome,例如 Pixel 2、Nexus 7、iPhone 或 iPad。以下截图显示了我们的示例应用程序在 Chrome iPhone 中的显示效果。我们可以通过以下步骤在 Chrome 浏览器中启动移动设备模拟:
- 在 Chrome 浏览器中导航到示例 Web 应用程序:

- 打开开发者工具。选择蓝色移动设备图标,然后选择设备。在这个例子中,我们选择了 iPhone X。Chrome 浏览器将根据所选设备重新加载:

移动设备模拟功能允许开发者和测试人员快速测试网站在移动设备上的显示效果,无需实际设备,从而加快开发过程。
我们也可以通过配置 ChromeOptions 使用 Selenium WebDriver 测试进行移动设备模拟。让我们修改搜索测试以在 Google Pixel 2 上测试:
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
Map<String, Object> deviceMetrics = new HashMap<>();
deviceMetrics.put("width", 411);
deviceMetrics.put("height", 823);
deviceMetrics.put("pixelRatio", 3.0);
Map<String, Object> mobileEmulation = new HashMap<>();
mobileEmulation.put("deviceMetrics", deviceMetrics);
mobileEmulation.put("userAgent", "Mozilla/5.0 (Linux; Android 8.0.0;" +
"Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/67.0.3396.99 Mobile Safari/537.36");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setExperimentalOption("mobileEmulation", mobileEmulation);
driver = new ChromeDriver(chromeOptions);
driver.get("http://demo-store.seleniumacademy.com/");
}
上述代码将在执行期间启用 Chrome 中的移动设备模拟,并加载网站的移动版本。这是通过首先使用 Java HashMap 配置设备度量,例如宽度和高度来完成的。在这个例子中,我们配置了deviceMetrics HashMap,如下面的代码所示:
Map<String, Object> deviceMetrics = new HashMap<>();
deviceMetrics.put("width", 411);
deviceMetrics.put("height", 823);
deviceMetrics.put("pixelRatio", 3.0);
接下来,我们需要创建另一个名为mobileEmulation的 HashMap,它将包含deviceMetrics和userAgent字符串。userAgent字符串指定了应使用哪个移动设备,例如 Pixel 2 XL,以及渲染引擎版本:
Map<String, Object> mobileEmulation = new HashMap<>();
mobileEmulation.put("deviceMetrics", deviceMetrics);
mobileEmulation.put("userAgent", "Mozilla/5.0 (Linux; Android 8.0.0;" +
"Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/67.0.3396.99 Mobile Safari/537.36");
最后,我们需要将mobileEmulation hashmap 传递给ChromeOptions类,并调用传递mobileEmulation hashmap 的setExperimentalOption()方法:
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setExperimentalOption("mobileEmulation", mobileEmulation);
driver = new ChromeDriver(chromeOptions);
这将加载应用程序的移动版本,Selenium 将像往常一样运行测试。
配置移动设备后,我们可以获取userAgent字符串。在 Chrome 开发者工具中转到“网络”标签页,重新加载页面,从列表中选择第一个请求,并从“头部”标签页复制User-Agent键的值,如下面的截图所示:

我们可以使用setExperimentalOptions()方法和ChromeOptions类设置多个 Chrome 首选项。
添加 Chrome 扩展
与 Firefox 类似,我们可以通过指定扩展的位置向 Chrome 浏览器添加扩展。我们可以使用 ChromeOptions 类添加打包(.crx 文件)或未打包(文件夹)扩展。
要添加打包扩展,我们需要调用addExtension()方法:
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addExtensions(new File("/path/to/extension.crx"));
ChromeDriver driver = new ChromeDriver(chromeOptions);
要添加未打包扩展,我们需要使用addArguments()方法,该方法将在启动 Chrome 二进制文件时读取指定的文件夹来加载扩展。操作如下:
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("load-extension=/path/to/extension");
ChromeDriver driver = new ChromeDriver(chromeOptions);
类似地,您可以使用 Chrome 选项向您的 Chrome 浏览器添加更多扩展、参数和二进制文件。
InternetExplorerDriver
为了在 Internet Explorer 浏览器上执行测试脚本,你需要 WebDriver 的 InternetExplorerDriver。与 Google Chrome 和 Firefox 类似,我们需要从www.seleniumhq.org/download/下载IEDriver Server可执行文件,用于 Internet Explorer。
IEDriver 服务器随后使用其 IEThreadExplorer 类,该类是用 C++编写的,通过组件对象模型框架来驱动 IE 浏览器。
为 IE 浏览器编写第一个测试脚本
现在您已经准备好编写在 Internet Explorer 浏览器上运行的测试脚本了。以下是如何实例化 InternetExplorerDriver 的代码:
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() {
System.setProperty("webdriver.ie.driver",
"./src/test/resources/drivers/IEDriverServer.exe");
driver = new InternetExplorerDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
理解 IEDriver 能力
在本节中,我们将讨论一些 InternetExplorerDriver 的重要功能。这是我们设置 IEDriver 能力以忽略安全域的地方。代码如下:
DesiredCapabilities ieCapabilities = DesiredCapabilities .internetExplorer(); ieCapabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAK INESS_BY_IGNORING_SECURITY_DOMAINS,true);
与INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS类似,IEDriver 有许多其他功能。以下是一个列表,解释了为什么使用它:
| 能力 | 要设置的值 | 目的 |
|---|---|---|
| INITIAL_BROWSER_URL | URL,例如,www.google.com |
此能力使用 URL 值设置驱动程序在打开浏览器时应导航到的浏览器。 |
| INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS | 布尔值 | 这定义了 IEDriverServer 是否应该忽略浏览器安全域设置。 |
| NATIVE_EVENTS | 布尔值 | 这告诉 IEDriver 服务器是否在执行鼠标或键盘动作时使用原生事件或 JavaScript 事件。 |
| REQUIRE_WINDOW_FOCUS | 布尔值 | 如果设置为 True,IE 浏览器窗口将获得焦点。这在执行原生事件时特别有用。 |
| ENABLE_PERSISTENT_HOVERING | 布尔值 | 如果设置为 True,IEDriver 将持续触发鼠标悬停事件。这在克服 IE 处理鼠标悬停事件的问题时特别重要。 |
| IE_ENSURE_CLEAN_SESSION | 布尔值 | 如果为 True,则清除 IE 的所有实例的所有 cookies、缓存、历史记录和保存的表单数据。 |
| IE_SET_PROXY_BY_SERVER | 布尔值 | 如果为 True,则使用 IEDriver 服务器的代理服务器设置。如果为 False,则使用 WindowsProxyManager 来确定代理服务器。 |
Edge 驱动程序
微软 Edge 是随着微软 Windows 10 推出的最新网络浏览器。微软 Edge 是首批实施 W3C WebDriver 标准的浏览器之一,并为 Selenium WebDriver 提供内置支持。
与 Internet Explorer 类似,为了在 Microsoft Edge 浏览器上执行测试脚本,我们需要使用 EdgeDriver 类和一个独立的 Microsoft WebDriver 服务器可执行文件。Microsoft WebDriver 服务器由 Microsoft Edge 开发团队维护。更多信息请参阅 docs.microsoft.com/en-gb/microsoft-edge/webdriver。
编写 Edge 浏览器的第一个测试脚本
让我们设置 Microsoft WebDriver 服务器并创建一个测试,用于测试 Microsoft Edge 浏览器的搜索功能。我们需要在 Windows 10 上下载和安装 Microsoft WebDriver 服务器([developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)):
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() {
System.setProperty("webdriver.edge.driver",
"./src/test/resources/drivers/MicrosoftWebDriver.exe");
EdgeOptions options = new EdgeOptions();
options.setPageLoadStrategy("eager");
driver = new EdgeDriver(options);
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
Microsoft WebDriver 服务器是一个独立的可执行服务器,实现了 WebDriver 的 JSON-wire 协议,它作为测试脚本和 Microsoft Edge 浏览器之间的粘合剂。在前面的代码中,我们需要使用 webdriver.edge.driver 属性指定可执行文件的路径,类似于我们在本章前面看到的其他浏览器配置。
我们还设置页面加载策略为 eager,使用 EdgeOptions 类:
EdgeOptions options = new EdgeOptions();
options.setPageLoadStrategy("eager");
当导航到新的页面 URL 时,Selenium WebDriver 默认会在将控制权传递给下一个命令之前等待页面完全加载。这在大多数情况下都很好,但在需要加载大量第三方资源的页面上可能会导致长时间等待。使用 eager 页面加载策略可以使测试执行更快。eager 页面加载策略将等待直到 DOMContentLoaded 事件完成,即仅下载和解析 HTML 内容,但其他资源,如图片,可能仍在加载。然而,这可能会引入元素动态加载时的不稳定性。
Safari Driver
随着 Selenium 3.0 和 WebDriver 成为 W3C 标准,苹果现在在浏览器中内置了 SafariDriver。我们无需单独下载它。然而,为了与 Selenium WebDriver 一起使用,我们必须从 Safari 的主菜单设置 Develop | Allow Remote Automation 选项,如下面的截图所示:

允许远程自动化
编写 Safari 浏览器的第一个测试脚本
这很简单。以下是用 Safari Driver 编写的测试脚本:
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() {
driver = new SafariDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
在前面的代码中,我们创建了一个 SafariDriver 类的实例,用于在 Safari 浏览器上启动和执行测试。
摘要
在本章中,你看到了 WebDriver 在行业中广泛使用的几个主要实现。我们查看了一些浏览器的重要配置选项,以及如何使用它们来创建自定义配置文件和移动仿真。每个驱动程序的基础技术是 JSONWire 协议,这是所有实现的基础。在下一章中,我们将学习如何使用 Java 8 Stream API 和 Lambda 函数与 Selenium WebDriver API 一起使用。
问题
-
WebDriver 成为 W3C 规范的意义是什么?
-
正确或错误:WebDriver 是一个接口?
-
哪些浏览器支持无头测试?
-
我们如何使用 Chrome 测试移动网站?
更多信息
你可以查看以下链接,获取更多关于本章涵盖主题的信息:
-
在
www.w3.org/TR/webdriver/阅读 WebDriver 规范 -
你可以在
github.com/mozilla/geckodriver找到更多关于 GeckoDriver 及其功能的信息 -
在
chromedriver.chromium.org/capabilities了解更多关于 ChromeDriver 的功能,以及在chromedriver.chromium.org/capabilities了解更多关于移动仿真的信息 -
在
docs.microsoft.com/en-gb/microsoft-edge/webdriver了解更多关于 EdgeDriver 的功能
第三章:使用 Java 8 功能与 Selenium
随着 Selenium 3.0 转向 Java 8,我们可以使用 Java 8 的一些新功能,如 Stream API 和 Lambda 或匿名函数,以函数式编程风格创建脚本。我们这样做是通过减少代码行数以及利用语言的新功能的好处。在本章中,我们将介绍这些主题:
-
介绍 Java 8 Stream API
-
使用 Stream API 收集和过滤数据
-
使用 Selenium WebDriver 与 Stream API
介绍 Java 8 Stream API
Stream API 是 Java 8 中 Collections API 的新增功能。Stream API 带来了处理对象集合的新方法。一个流代表一系列元素,并支持从集合中进行不同类型的操作(过滤、排序、映射和收集)。我们可以将这些操作链接在一起,形成一个查询数据的管道,如图所示:

我们可以使用.stream()方法从集合中获取 Stream。例如,我们有一个下拉菜单,显示在标题部分中支持的示例 Web 应用程序的语言。让我们将其捕获到一个Array list中,如下所示:
List<String> languages = new ArrayList<String>();
languages.add("English");
languages.add("German");
languages.add("French");
如果我们必须打印列表成员,我们将使用以下方式的for循环:
for(String language : languages) {
System.out.println(language);
}
使用流API,我们可以通过在languages数组列表上调用.stream()方法来获取流,并按以下方式打印成员:
languages.stream().forEach(System.out::println);
获得流后,我们调用了forEach()方法,传递了我们对每个元素想要执行的操作,即使用System.out.println方法在控制台上输出成员值。
一旦我们从集合中获取了 Stream,我们就可以使用该 Stream 来处理集合的元素或成员。
Stream.filter()
我们可以使用filter()方法过滤流。让我们过滤从languages列表中获得的流,以过滤以E开头的项,如下面的代码所示:
stream.filter( item -> item.startsWith("E") );
filter()方法接受一个 Predicate 作为参数。predicate接口包含一个名为boolean test(T t)的函数,它接受一个参数并返回一个布尔值。在上面的例子中,我们将 lambda 表达式item -> item.startsWith("E")传递给了test()函数。
当在 Stream 上调用filter()方法时,传递给filter()函数的过滤器被内部存储。项目不会立即过滤。
传递给filter()函数的参数决定了流中哪些项目应该被处理以及哪些应该被排除。如果Predicate.test()函数对一个项目返回true,这意味着它应该被处理。如果返回false,则该项目不会被处理。在上面的例子中,test()函数将返回以字符E开头的所有项目的true。
Stream.sort()
我们可以通过调用sort()函数对 Stream 进行排序。让我们在languages列表上使用sort()函数,如下面的代码所示:
languages.stream().sorted();
这将按字母顺序对元素进行排序。我们可以提供一个 lambda 表达式来使用自定义比较逻辑对元素进行排序。
Stream.map()
Streams 提供了一个 map() 方法,用于将流中的元素映射到另一种形式。我们可以将元素映射到新的对象中。让我们以之前的例子为例,将语言列表的元素转换为大写,如下所示:
languages.stream().map(item -> item.toUpperCase());
这将映射语言集合中所有字符串元素到它们的大写等效形式。再次强调,这实际上并没有执行映射;它只是配置了流以进行映射。一旦调用了其中一个流处理方法,映射(和过滤)将被执行。
Stream.collect()
Streams 在 Stream 接口中提供了 collect() 方法,以及其他方法,用于流处理。当调用 collect() 方法时,过滤和映射将发生,并且那些操作的结果对象将被收集。让我们以之前的例子为例,获取一个新的包含大写语言列表,如下面的代码所示:
List<String> upperCaseLanguages = languages.stream()
.map(item -> item.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperCaseLanguages);
此示例创建了一个流,添加了一个映射来将字符串转换为大写,并将所有对象收集到一个新的列表中。我们还可以使用 filter 或 sort 方法,并根据 filter 方法中应用的条件收集结果列表。
Stream.min() 和 Stream.max()
Streams API 提供了 min() 和 max() 方法——分别用于在流中查找最小或最大值。
让我们在测试的示例应用程序的上下文中举一个例子。我们将创建一个简单的 Java 类 Product,该类存储由搜索返回的产品名称和价格。我们想要找到价格最低的产品和价格最高的产品。我们的产品类将有两个成员,如下面的代码所示:
class Product {
String name;
Double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public Double getPrice() {
return price;
}
}
让我们创建一个包含搜索结果返回的产品列表,如下所示:
List<Product> searchResult = new ArrayList<>();
searchResult.add(new Product("MADISON OVEREAR HEADPHONES", 125.00));
searchResult.add(new Product("MADISON EARBUDS", 35.00));
searchResult.add(new Product("MP3 PLAYER WITH AUDIO", 185.00));
我们可以通过传递比较属性来调用 .min() 函数,在这种情况下,使用 .getPrice() 方法。.min() 函数将使用价格属性并返回价格最低的元素,如下面的代码所示:
Product product = searchResult.stream()
.min(Comparator.comparing(item -> item.getPrice()))
.get();
System.out.println("The product with lowest price is " + product.getName());
get() 方法将返回 min() 函数返回的对象。我们将将其存储在 Product 实例中。min() 函数找到 MADISON EARBUDS 作为价格最低的产品,如下面的控制台输出所示:
The product with lowest price is MADISON EARBUDS
与 min() 函数相反,max() 函数将返回价格最高的产品,如下面的代码所示:
product = searchResult.stream()
.max(Comparator.comparing(item -> item.getPrice()))
.get();
System.out.println("The product with highest price is " + product.getName());
max() 函数找到 MP3 PLAYER WITH AUDIO 作为价格最高的产品:
The product with highest price is MP3 PLAYER WITH AUDIO
min() 和 max() 函数返回一个可选实例,该实例有一个 get() 方法来获取对象。如果流中没有元素,get() 方法将返回 null。
这两个函数都接受一个比较器作为参数。Comparator.comparing() 方法根据传递给它的 lambda 表达式创建一个比较器。
Stream.count()
Streams API 提供了一个返回过滤后流中元素数量的count方法。让我们用之前的例子来获取 MADISON 品牌产品的数量:
long count = searchResult.stream()
.filter(item -> item.getName().startsWith("MADISON"))
.count();
System.out.println("The number of products from MADISON are: " + count);
count()方法返回一个long,这是匹配过滤标准的元素数量。在这个例子中,以下输出将在控制台上显示:
The number of products from MADISON are: 2
使用 Selenium WebDriver 的 Stream API
现在我们已经介绍了 Streams API 及其各种函数,让我们看看我们如何在测试中使用它们。
过滤和计数 Web 元素
让我们从一项简单的测试开始,以确定示例应用程序主页上显示的链接。我们从主页获取所有链接并打印它们的数量,然后是页面上可见链接的数量,如下面的代码所示:
@Test
public void linksTest() {
List<WebElement> links = driver.findElements(By.tagName("a"));
System.out.println("Total Links : " + links.size());
long count = links.stream().filter(item -> item.isDisplayed()).count();
System.out.println("Total Link visible " + count);
}
在前面的代码中,我们使用了findElements()方法和By.tagName来从主页获取所有链接。然而,为了找出其中的可见链接,我们使用了带有谓词的filter()函数来测试链接是否显示。这是通过调用WebElement接口的isDisplayed()方法来完成的。如果链接显示,isDisplayed方法将返回true;否则,它将返回false。最后,我们调用了count()方法来获取由filter()函数返回的链接数量。这将在控制台上显示以下输出:
Total Links : 88
Total Link visible 37
过滤元素属性
在示例代码中,我们将过滤一个具有空alt属性定义的图像列表。如果你想要检查页面上显示的图像的可用性,这很有用。根据可用性指南,所有图像都应该有定义的alt属性。这是通过过滤图像,通过测试getAttribute("alt")方法来完成的;它返回一个空字符串,如下面的代码所示:
@Test
public void imgAltTest() {
List<WebElement> images = driver.findElements(By.tagName("img"));
System.out.println("Total Images : " + images.size());
List<WebElement> imagesWithOutAlt = images.stream()
.filter(item -> item.getAttribute("alt") == "")
.collect(Collectors.toList());
System.out.println("Total images without alt attribute " + imagesWithOutAlt);
}
filter()函数将返回所有具有空alt属性定义的图像元素的列表。
使用 Map 函数从元素中获取文本值
在这个例子中,我们将修改我们在早期章节中创建的搜索测试,以测试包含预期产品列表的结果,如下面的代码所示:
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
List<WebElement> searchItems = driver
.findElements(By.cssSelector("h2.product-name a"));
List<String> expectedProductNames =
Arrays.asList("MADISON EARBUDS",
"MADISON OVEREAR HEADPHONES",
"MP3 PLAYER WITH AUDIO");
List<String> productNames = searchItems.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
assertThat(productNames).
isEqualTo(expectedProductNames);
}
在前面的代码中,我们创建了一个由findElements()方法返回的所有匹配产品的列表。然后我们通过调用map()函数检索每个元素的文本,并将返回值映射到一个字符串列表中。这与expectedProductNames列表进行比较。
对 Web 元素进行过滤和执行操作
让我们进一步修改搜索测试,找到一个与给定名称匹配的产品。然后我们将点击该产品以打开产品详情页面,如下面的代码所示:
@Test
public void searchAndViewProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
List<WebElement> searchItems = driver
.findElements(By.cssSelector("h2.product-name a"));
WebElement product = searchItems.stream()
.filter(item -> item.getText().equalsIgnoreCase("MADISON EARBUDS"))
.findFirst()
.get();
product.click();
assertThat(driver.getTitle())
.isEqualTo("Madison Earbuds");
}
在前面的代码中,我们使用了 filter() 函数从 WebElements 列表中找到特定的产品。我们使用 findFirst() 函数检索第一个匹配的产品,这将返回一个代表链接元素的 WebElement。然后我们点击该元素以在浏览器中打开产品详情页面。
因此,我们可以以多种方式使用 Streams API,用几行代码就创建出功能性强、易于阅读的代码。
摘要
在本章简短的介绍中,我们学习了如何使用 Selenium 8 Stream API 和 Lambda 函数来简化 Selenium WebDriver 代码。这有助于你以函数式编程风格编写代码,使其更加流畅和易读。Streams 在处理 WebElements 列表时非常有用。我们可以通过 stream 容易地收集和过滤数据。
在下一章中,我们将探讨 WebDriver 的截图功能、窗口和框架处理、同步以及管理 Cookie 的特性。
问题
-
哪个版本的 Java Streams API 被引入了?
-
解释 Streams API 的 filter 函数。
-
Streams API 的哪个方法会从 filter() 函数返回匹配元素的数量?
-
我们可以使用
map()函数通过属性值过滤 WebElements 列表:对或错?
更多信息
你可以查看以下链接以获取本章涵盖主题的更多信息:
第四章:探索 WebDriver 的功能
到目前为止,我们已经探讨了用户可以使用 WebDriver 在网页上执行的各种基本和高级交互。在本章中,我们将讨论 WebDriver 的不同功能和特性,这些功能和特性使测试脚本开发者能够更好地控制 WebDriver,从而更好地控制正在测试的 Web 应用程序。本章将要涵盖的功能如下:
-
截取屏幕
-
定位目标窗口和 iframe
-
探索导航
-
等待 Web 元素加载
-
处理 cookies
让我们立即开始,不要有任何延迟。
截取屏幕
截取网页截图是 WebDriver 的一个非常有用的功能。当测试用例失败,你想看到测试用例失败时应用程序的状态时,这非常有用。WebDriver 库中的TakesScreenShot接口由所有不同的 WebDriver 变体实现,例如 Firefox Driver、Internet Explorer Driver、Chrome Driver 等。
默认情况下,所有浏览器都启用了TakesScreenShot功能。因为这是一个只读功能,用户无法切换它。在我们看到使用此功能的代码示例之前,我们应该看看TakesScreenShot接口的一个重要方法——getScreenshotAs().
getScreenshotAs()方法的 API 语法如下:
public X getScreenshotAs(OutputType target)
在这里,OutputType是 WebDriver 库的另一个接口。我们可以要求 WebDriver 以三种不同的格式输出截图:BASE64、BYTES(原始数据)和FILE。如果你选择FILE格式,它将数据写入一个.png文件,一旦 JVM 被终止,该文件将被删除。因此,你应该始终将该文件复制到安全位置,以便以后参考。
返回类型是特定输出,它取决于所选的OutputType。例如,选择OutputType.BYTES将返回一个byte数组,而选择OutputType.FILE将返回一个文件对象。
根据使用的浏览器,输出截图将是以下之一,按优先级排序:
-
整个页面
-
当前窗口
-
当前框架的可视部分
-
包含浏览器的整个显示屏幕的截图
例如,如果你正在使用 Firefox Driver,getScreenshotAs()方法会截取整个页面的屏幕截图,但 Chrome Driver 只返回当前框架的可视部分。
是时候看看以下代码示例了:
@BeforeMethod
public void setup() throws IOException {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com/");
File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(scrFile, new File("./target/screenshot.png"));
}
在前面的代码中,我们使用了getScreenshotAs()方法来截取网页截图并将其保存为文件格式。我们可以从目标文件夹中打开保存的图像并检查它。
定位目标窗口和框架
WebDriver 允许开发者之间切换多个子窗口、浏览器标签页和应用程序中使用的框架。例如,当你点击银行网络应用程序上的网络银行链接时,它将在一个单独的窗口或标签页中打开网络银行应用程序。此时,你可能想切换回原始窗口来处理一些事件。同样,你可能需要处理一个在网页上分为两个框架的网络应用程序。左侧的框架可能包含导航项,而右侧的框架根据左侧框架中选择的项显示相应的网页。使用 WebDriver,你可以开发出能够轻松处理这种复杂情况的测试用例。
WebDriver.TargetLocator 接口用于定位给定的框架或窗口。在本节中,我们将看到 WebDriver 如何处理浏览器窗口之间的切换以及在同一个窗口中的两个框架之间的切换。
在窗口之间切换
首先,我们将看到一个处理多个窗口的代码示例。对于本章,本书提供了一个名为 Window.html 的 HTML 文件。这是一个非常基础的网页,它链接到谷歌的搜索页面。当你点击链接时,谷歌的搜索页面会在一个不同的窗口中打开。每次你使用 WebDriver 在浏览器窗口中打开一个网页时,WebDriver 都会为它分配一个窗口句柄。WebDriver 使用窗口句柄来识别窗口。此时,在 WebDriver 中已注册了两个窗口句柄。现在,在屏幕上,你可以看到谷歌的搜索页面位于前面并且拥有焦点。此时,如果你想切换到第一个浏览器窗口,你可以使用 WebDriver 的 switchTo() 方法来实现。
TargetLocator 的 API 语法如下:
WebDriver.TargetLocator switchTo()
此方法返回 WebDriver.TargetLocator 实例,其中你可以告诉 WebDriver 是否要在浏览器窗口或框架之间切换。让我们看看 WebDriver 如何处理这个问题:
public class WindowHandlingTest {
WebDriver driver;
@BeforeMethod
public void setup() throws IOException {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://guidebook.seleniumacademy.com/Window.html");
}
@Test
public void handleWindow() {
String firstWindow = driver.getWindowHandle();
System.out.println("First Window Handle is: " + firstWindow);
WebElement link = driver.findElement(By.linkText("Google Search"));
link.click();
String secondWindow = driver.getWindowHandle();
System.out.println("Second Window Handle is: " + secondWindow);
System.out.println("Number of Window Handles so for: "
+ driver.getWindowHandles().size());
driver.switchTo().window(firstWindow);
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
观察以下代码行:
String firstWindow = driver.getWindowHandle();
在这里,驱动程序返回窗口的分配标识符。现在,在我们移动到不同的窗口之前,最好存储这个值,以便如果我们想切换回这个窗口,我们可以使用这个句柄或标识符。要检索到目前为止与你的驱动程序注册的所有窗口句柄,你可以使用以下方法:
driver.getWindowHandles()
这将返回在驱动程序会话期间打开的所有浏览器窗口句柄的标识符集合。现在,在我们的示例中,在我们打开谷歌的搜索页面后,对应的窗口会显示在前面并且拥有焦点。如果你想回到第一个窗口,你必须使用以下代码:
driver.switchTo().window(firstWindow);
这将使第一个窗口获得焦点。
在框架之间切换
现在我们来看看如何处理在网页框架之间的切换。在这本书提供的 HTML 文件中,您将看到一个名为Frames.html的文件。如果您打开它,您将看到两个 HTML 文件在不同的框架中加载。让我们看看我们如何在这两个框架之间切换,并在每个框架中可用的文本框中输入:
public class FrameHandlingTest {
WebDriver driver;
@BeforeMethod
public void setup() throws IOException {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://guidebook.seleniumacademy.com/Frames.html");
}
@Test
public void switchBetweenFrames() {
// First Frame
driver.switchTo().frame(0);
WebElement firstField = driver.findElement(By.name("1"));
firstField.sendKeys("I'm Frame One");
driver.switchTo().defaultContent();
// Second Frame
driver.switchTo().frame(1);
WebElement secondField = driver.findElement(By.name("2"));
secondField.sendKeys("I'm Frame Two");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
在前面的代码中,我们使用了switchTo().frame而不是switchTo().window,因为我们正在跨框架移动。
frame的 API 语法如下:
WebDriver frame(int index)
此方法接受您想要切换到的框架的索引。如果您的网页有三个框架,WebDriver 将它们索引为 0、1 和 2,其中零索引分配给在 DOM 中遇到的第一个框架。同样,您可以使用之前重载的方法通过名称在框架之间切换。API 语法如下:
WebDriver frame(String frameNameOrframeID)
您可以传递框架的名称或其 ID。使用此方法,如果您不确定目标框架的索引,可以切换到该框架。另一个重载方法如下:
WebDriver frame(WebElement frameElement)
输入参数是框架的WebElement。让我们考虑我们的代码示例:首先,我们已经切换到了第一个框架并在文本字段中输入。然后,我们不是直接切换到第二个框架,而是来到了主内容或默认内容,然后切换到第二个框架。该代码如下:
driver.switchTo().defaultContent();
这非常重要。如果您不这样做,而试图在仍然处于第一个框架时切换到第二个框架,您的 WebDriver 将抱怨说它找不到索引为 1 的框架。这是因为 WebDriver 在第一个框架的上下文中搜索第二个框架,这显然是不可用的。因此,您必须首先来到顶级容器,然后切换到您感兴趣的框架。
切换到默认内容后,现在可以使用以下代码切换到第二个框架:
driver.switchTo().frame(1);
因此,您可以在框架之间切换并执行相应的 WebDriver 操作。
处理警报
除了在窗口和框架之间切换之外,您可能还需要处理 Web 应用程序中的各种模态对话框。为此,WebDriver 提供了一个处理警报对话框的 API。该 API 如下:
Alert alert()
前面的方法将切换到网页上当前活动的模态对话框。这返回一个Alert实例,可以在该对话框上执行适当的操作。如果没有当前对话框,并且您调用此 API,它将抛出NoAlertPresentException。
Alert接口包含许多 API 来执行不同的操作。以下列表将逐一讨论它们:
-
void accept(): 这相当于对话框上的确定按钮操作。当在对话框上执行accept()操作时,将调用相应的确定按钮操作。 -
void dismiss(): 这相当于点击取消操作按钮。 -
java.lang.String getText(): 这将返回对话框上显示的文本。如果你想要评估模态对话框上的文本,可以使用此方法。 -
void sendKeys(java.lang.String keysToSend): 如果警报有相应的设置,这将允许开发者输入一些文本到警报中。
探索 Navigate
如我们所知,WebDriver 以原生方式与单个浏览器进行通信。这种方式不仅使它能够更好地控制网页,还能控制浏览器本身。Navigate 是 WebDriver 的一个功能,允许测试脚本开发者与浏览器的后退、前进和刷新控件进行交互。作为网络应用的用户,我们经常使用浏览器的前进和后退控件在单个应用或有时是多个应用之间导航。作为测试脚本开发者,你可能希望开发出在点击浏览器导航按钮时观察应用行为的测试,特别是后退按钮。例如,如果你在一个银行应用中使用导航按钮,会话应该过期,用户应该被登出。因此,使用 WebDriver 的导航功能,你可以模拟这些操作。
用于此目的的方法是 navigate()。以下是其 API 语法:
WebDriver.Navigation navigate()
显然,此方法没有输入参数,但返回类型是 WebDriver.Navigation 接口,它包含所有帮助你在浏览器历史记录中导航的浏览器导航选项。
现在让我们看一个代码示例,然后分析代码:
@Test
public void searchProduct() {
driver.navigate().to("http://demo-store.seleniumacademy.com/");
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
driver.navigate().back();
driver.navigate().forward();
driver.navigate().refresh();
}
前面的代码打开了演示应用的主页,首先搜索了 Phone;然后,在搜索结果加载后。现在,我们在浏览器中创建了一个导航历史,使用 WebDriver 导航返回浏览器历史记录,然后前进并刷新页面。
让我们分析前面代码中使用的导航方法。最初加载演示应用主页的代码行使用了 Navigation 类的 to() 方法,如下所示:
driver.navigate().to("http://demo-store.seleniumacademy.com/");
在这里,driver.navigate() 方法返回 WebDriver.Navigation 接口,在该接口上使用 to() 方法导航到网页 URL。
API 语法如下:
void to(java.lang.String url)
此方法的输入参数是要在浏览器中加载的 url 字符串。此方法将通过使用 HTTP GET 操作在浏览器中加载页面,并且直到页面完全加载,它会阻塞其他所有操作。此方法与 driver.get(String url) 方法相同。
WebDriver.Navigation 接口还提供了一个重载的 to() 方法,以便更容易地传递 URL。它的 API 语法如下:
void to(java.net.URL url)
接下来,在代码示例中,我们搜索了 Phone。然后,我们尝试使用 Navigation 的 back() 方法来模拟浏览器的后退按钮,使用以下代码行:
driver.navigate().back();
这将带浏览器回到主页。此方法的 API 语法非常简单;如下所示:
void back()
此方法不接收任何输入,也不返回任何内容,但它将浏览器的历史记录向后推进一级。
然后,导航中的下一个方法是forward()方法,它与back()方法非常相似,但它将浏览器的历史记录向前推进一级。在先前的代码示例中,调用了以下内容:
driver.navigate().forward();
该方法的 API 语法如下:
void forward()
此方法不接收任何输入,也不返回任何内容,但它将浏览器的历史记录向前推进一级。
代码示例中的最后一行使用了 WebDriver 导航的refresh()方法:
driver.navigate().refresh();
此方法将重新加载当前 URL 以模拟浏览器的刷新(F5键)操作。API 语法如下:
void refresh()
如您所见,语法与back()和forward()方法非常相似,并且此方法将重新加载当前 URL。因此,这些都是 WebDriver 为开发者提供的用于模拟一些浏览器操作的各种方法。
等待网页元素加载
如果您有之前的 UI 自动化经验,我敢肯定您会遇到一种情况,即您的测试脚本无法在网页上找到元素,因为网页仍在加载。这可能是由于各种原因。一个经典的例子是当应用服务器或 Web 服务器由于资源限制而响应太慢时;另一个可能是当您在一个非常慢的网络中访问页面时。原因可能是当您的测试脚本尝试找到它时,网页上的元素尚未加载。这就是您必须计算和配置测试脚本等待网页元素加载的平均等待时间的时候。
WebDriver 为测试脚本开发者提供了一个非常实用的功能来管理等待时间。"等待时间"是指您的驱动程序在放弃并抛出NoSuchElementException之前,将等待 WebElement 加载的时间。记住,在第一章介绍 WebDriver 和 WebElements中,我们讨论了findElement(By by)方法,当它无法找到目标 WebElement 时会抛出NoSuchElementException。
您可以通过两种方式让 WebDriver 等待 WebElement。它们是隐式等待时间和显式等待时间。隐式超时对所有 WebElement 都是通用的,并且与它们关联一个全局超时周期,但显式超时可以配置为针对单个 WebElement。让我们在这里讨论每个选项。
隐式等待时间
隐式等待时间用于您想要配置测试应用程序中 WebDriver 的整体等待时间时。想象一下,您已经在本地服务器和远程服务器上托管了一个 Web 应用程序。显然,托管在本地服务器上的网页加载时间会小于托管在远程服务器上的相同网页的加载时间,这是由于网络延迟造成的。现在,如果您想针对每个服务器执行测试用例,您可能需要相应地配置等待时间,以确保您的测试用例不会花费太多时间等待页面,或者花费的时间远远不够,从而导致超时。为了处理这类等待时间问题,WebDriver 提供了一个选项,可以通过 manage() 方法为驱动程序执行的所有操作设置隐式等待时间。
让我们看看一个隐式等待时间的代码示例:
driver = new ChromeDriver();
driver.navigate().to("http://demo-store.seleniumacademy.com/");
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
让我们分析以下突出显示的代码行:
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
在这里,driver.manage().timeouts() 返回 WebDriver.Timeouts 接口,该接口声明了一个名为 implicitlyWait 的方法,您可以在其中指定当在网页上搜索 WebElement 时,如果它不是立即出现,则驱动程序应该等待多长时间。WebDriver 将定期在网页上轮询 WebElement,直到之前方法指定的最大等待时间结束。在前面的代码中,10 秒是您的驱动程序将等待任何 WebElement 在浏览器中加载的最大等待时间。如果在这个时间范围内加载,WebDriver 将继续执行其余代码;否则,它将抛出 NoSuchElementException。
当您想要指定一个最大等待时间时,这通常是您 Web 应用程序上大多数 WebElement 的通用做法。影响您页面性能的各种因素包括网络带宽、服务器配置等。根据这些条件,作为 WebDriver 测试用例的开发者,您必须确定一个最大隐式等待时间的值,以确保您的测试用例不会执行时间过长,同时也不会频繁超时。
显式等待时间
隐式超时适用于网页上的所有 WebElement。但是,如果您在应用程序中有一个特定的 WebElement,您希望等待非常长的时间,这种方法可能不起作用。将隐式等待时间设置为这个非常长的时间段将延迟整个测试套件的执行。因此,您必须为特定情况(如这个 WebElement)做出例外。为了处理这类场景,WebDriver 为 WebElement 提供了显式等待时间。
那么,让我们看看如何使用 WebDriver 等待特定的 WebElement,以下是一段代码示例:
WebElement searchBox = (new WebDriverWait(driver, 20))
.until((ExpectedCondition<WebElement>) d -> d.findElement(By.name("q")));
突出的代码是我们为特定的 WebElement 创建了一个条件等待。ExpectedCondition接口可以用于将条件等待应用于 WebElement。在这里,WebDriver 将等待最多 20 秒以等待这个特定的 WebElement。对于这个 WebElement,不会应用隐式超时。如果 WebElement 在 20 秒的最大等待时间内没有加载,正如我们所知,驱动器会抛出NoSuchElementException。因此,您可以通过使用这个方便的显式等待时间来专门覆盖您认为将花费更多时间的 WebElements 的隐式等待时间。
处理 cookie
假设您正在自动化演示应用程序。您可能想要自动化的场景有很多,例如搜索产品、将产品添加到购物车、结账、退货等。对于所有这些操作,一个共同点是在每个测试用例中都需要登录到演示应用程序。因此,在您的每个测试用例中登录应用程序将显著增加整体测试执行时间。为了减少测试用例的执行时间,您实际上可以跳过每个测试用例的登录。这可以通过一次性登录并将该域的所有 cookie 写入文件来实现。从下一次登录开始,您实际上可以从文件中加载 cookie 并将其添加到驱动器中。
要获取加载到网页上的所有 cookie,WebDriver 提供了以下方法:
driver.manage().getCookies()
这将返回网页在当前会话中存储的所有 cookie。每个 cookie 都与一个名称、值、域、路径、过期时间和是否安全的状态相关联。服务器解析客户端 cookie 时会解析所有这些值。现在,我们将为每个 cookie 存储所有这些信息到一个文件中,以便我们的单个测试用例从这个文件中读取并加载这些信息到驱动器中。因此,您可以跳过登录,因为一旦您的驱动器会话中有了这些信息,应用程序服务器就会将您的浏览器会话视为已认证,并直接带您到您请求的 URL。以下是一个快速存储 cookie 信息的代码示例:
public class StoreCookieInfo {
WebDriver driver;
@BeforeMethod
public void setup() throws IOException {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com/customer/account/login/");
}
@Test
public void storeCookies() {
driver.findElement(By.id("email")).sendKeys("user@seleniumacademy.com");
driver.findElement(By.id("pass")).sendKeys("tester");
driver.findElement(By.id("send2")).submit();
File dataFile = new File("./target/browser.data");
try {
dataFile.delete();
dataFile.createNewFile();
FileWriter fos = new FileWriter(dataFile);
BufferedWriter bos = new BufferedWriter(fos);
for (Cookie ck : driver.manage().getCookies()) {
bos.write((ck.getName() + ";" + ck.getValue() + ";" + ck.
getDomain()
+ ";" + ck.getPath() + ";" + ck.getExpiry() + ";" + ck.
isSecure()));
bos.newLine();
}
bos.flush();
bos.close();
fos.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
从现在开始,对于每一个测试用例或一组测试用例,从browser.data文件中加载 cookie 信息,并使用以下方法将其添加到驱动器中:
driver.manage().addCookie(ck);
在您将此信息添加到浏览器会话并转到仪表板页面后,它将自动重定向到主页,而无需登录,从而避免每次测试用例都进行登录。将所有之前的 cookie 添加到驱动器的代码如下:
public class LoadCookieInfo {
WebDriver driver;
@BeforeMethod
public void setup() throws IOException {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com");
}
@Test
public void loadCookies() {
try {
File dataFile = new File("./target/browser.data");
FileReader fr = new FileReader(dataFile);
BufferedReader br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) {
StringTokenizer str = new StringTokenizer(line, ";");
while (str.hasMoreTokens()) {
String name = str.nextToken();
String value = str.nextToken();
String domain = str.nextToken();
String path = str.nextToken();
Date expiry = null;
String dt;
if (!(dt = str.nextToken()).equals("null")) {
SimpleDateFormat formatter =
new SimpleDateFormat("E MMM d HH:mm:ss z yyyy");
expiry = formatter.parse(dt);
}
boolean isSecure = new Boolean(str.nextToken()).
booleanValue();
Cookie ck = new Cookie(name, value, domain, path, expiry, isSecure);
driver.manage().addCookie(ck);
}
}
driver.get("http://demo-store.seleniumacademy.com/customer/account/index/");
assertThat(driver.findElement(By.cssSelector("div.page-title")).getText())
.isEqualTo("MY DASHBOARD");
} catch (Exception ex) {
ex.printStackTrace();
}
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
因此,我们可以直接进入主页而无需再次登录。如您所见,在创建驱动器实例后,我们有以下行:
driver.get("http://demo-store.seleniumacademy.com");
理想情况下,此行应该在我们将 cookie 设置到驱动程序之后可见。但它在顶部的原因是 WebDriver 不允许您直接将 cookie 设置到这个会话中,因为它将这些 cookie 视为来自不同域。尝试删除上一行代码并执行它,您将看到错误。因此,最初,您将尝试访问主页,将驱动程序的域值设置为应用程序服务器域,并加载所有 cookie。当您执行此代码时,最初您将看到应用程序的主页。
因此,通过使用 WebDriver 的 cookie 功能,您可以避免在服务器上输入用户名和密码,并反复验证它们以进行每个测试,从而节省大量时间。
摘要
在本章中,我们讨论了 WebDriver 的各种功能,例如捕获截图和处理Windows和Frames。我们还讨论了同步的隐式和显式等待条件,并使用了导航和 cookie API。利用这些功能将帮助您通过设计更创新的测试框架和测试案例,更有效地测试您的目标 Web 应用程序。在下一章中,我们将探讨Actions API,以使用键盘和鼠标事件执行用户交互。
问题
-
我们可以使用哪些不同的格式来输出截图?
-
我们如何使用 Selenium 切换到另一个浏览器标签?
-
对或错:
defaultContent()方法将切换到之前选定的框架。 -
Selenium 提供了哪些导航方法?
-
我们如何使用 Selenium 添加 cookie?
-
解释一下隐式等待和显式等待之间的区别。
更多信息
您可以查看以下链接,获取关于本章涵盖主题的更多信息:
-
您可以在
seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/ui/ExpectedConditions.html了解更多关于在使用显式等待时如何使用一组预定义的预期条件的信息。 -
您可以在《Selenium 测试工具手册》第四章“使用 Selenium API”和第五章“同步测试”中了解更多关于 WebDriver 特性的信息,由 Unmesh Gundecha 编写,Packt Publications 出版,第 2 版。Selenium Testing Tools Cookbook
第五章:探索 WebDriver 的高级交互
在上一章中,我们讨论了 WebDriver 接口及其功能,包括截图、与窗口、框架、警告、cookie 和同步测试。在本章中,我们将介绍一些在 WebElements 上执行动作的高级方法。我们将学习如何使用 Selenium WebDriver 的 actions API 执行动作,包括以下内容:
-
复杂的鼠标动作,如移动鼠标、双击和拖放
-
键盘快捷键
理解构建和执行动作
我们知道如何执行一些基本动作,例如点击按钮和在文本框中输入文本;然而,有许多场景需要我们同时执行多个动作,例如,保持 Shift 按钮按下并输入大写字母,以及拖放鼠标动作。
让我们看看一个简单的场景。打开 guidebook.seleniumacademy.com/Selectable.html。将出现一个编号为 1 到 12 的瓷砖盒子,如图所示:

如果你使用浏览器开发者工具检查元素,你会看到一个有序列表标签:
<ol id="selectable" class="ui-selectable">
<li class="ui-state-default ui-selectee" name="one">1</li>
<li class="ui-state-default ui-selectee" name="two">2</li>
<li class="ui-state-default ui-selectee" name="three">3</li>
<li class="ui-state-default ui-selectee" name="four">4</li>
<li class="ui-state-default ui-selectee" name="five">5</li>
<li class="ui-state-default ui-selectee" name="six">6</li>
<li class="ui-state-default ui-selectee" name="seven">7</li>
<li class="ui-state-default ui-selectee" name="eight">8</li>
<li class="ui-state-default ui-selectee" name="nine">9</li>
<li class="ui-state-default ui-selectee" name="ten">10</li>
<li class="ui-state-default ui-selectee" name="eleven">11</li>
<li class="ui-state-default ui-selectee" name="twelve">12</li>
</ol>
如果你点击一个数字,其背景颜色将变为橙色。尝试选择 1、3 和 5 号瓷砖。你可以通过按下 Ctrl + 瓷砖 1 + 瓷砖 3 + 瓷砖 5 来完成这个操作。这涉及到执行多个动作,即持续按下 Ctrl 并点击瓷砖 1、3 和 5。我们如何使用 WebDriver 执行这些多个动作?以下代码演示了如何操作:
@Test
public void shouldPerformCompositeAction() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement one = driver.findElement(By.name("one"));
WebElement three = driver.findElement(By.name("three"));
WebElement five = driver.findElement(By.name("five"));
// Add all the actions into the Actions builder.
Actions actions = new Actions(driver);
actions.keyDown(Keys.CONTROL)
.click(one)
.click(three)
.click(five)
.keyUp(Keys.CONTROL);
// Generate the composite action.
Action compositeAction = actions.build();
// Perform the composite action.
compositeAction.perform();
}
现在,如果你参考代码,我们将介绍一个新的名为 Actions 的类。这个 Actions 类是用来模拟所有复杂用户事件的。使用它,测试脚本的开发者可以将所有必要的用户手势组合成一个复合动作。我们已经声明了所有要执行的动作,以实现点击数字 1、3 和 5 的功能。一旦所有动作组合在一起,我们就将其构建成一个复合动作。"Action" 是一个只有 perform() 方法的接口,它执行复合动作。当我们执行测试时,1、3 和 5 号瓷砖将被依次选中。最后,将选中 5 号瓷砖,如图所示:

因此,要使 WebDriver 同时执行多个动作,你需要遵循使用动作类用户界面 API 的三个步骤:分组所有动作,然后构建复合动作,并执行动作。这个过程可以简化为两步,因为 perform() 方法内部调用 build() 方法。所以之前的代码将如下所示:
@Test
public void shouldPerformAction() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement one = driver.findElement(By.name("one"));
WebElement three = driver.findElement(By.name("three"));
WebElement five = driver.findElement(By.name("five"));
// Add all the actions into the Actions builder.
Actions actions = new Actions(driver);
actions.keyDown(Keys.CONTROL)
.click(one)
.click(three)
.click(five)
.keyUp(Keys.CONTROL);
// Perform the action
actions.perform();
}
在前面的代码中,我们直接在 Actions 实例上调用了 perform() 方法,它内部调用 build() 方法来创建一个复合动作并在执行之前创建它。在本章的后续部分,我们将更详细地研究 Actions 类。所有动作基本上分为两类:基于鼠标的动作和基于键盘的动作。在接下来的部分中,我们将讨论 Actions 类中所有特定于鼠标和键盘的动作。
学习基于鼠标的交互
使用动作类可以执行大约八种不同的鼠标动作。我们将看到它们的语法和实际的工作示例。
按偏移量移动的动作
moveByOffset() 方法用于将鼠标从当前位置移动到网页上的另一个点。开发者可以指定鼠标需要移动的 x 距离和 y 距离。当页面加载时,鼠标的初始位置通常是 (0, 0),除非页面有明确的焦点声明。
moveByOffset() 方法的 API 语法如下:
public Actions moveByOffset(int xOffSet, int yOffSet)
在前面的代码中,xOffSet 是一个输入参数,它为 WebDriver 提供了沿 x 轴移动的偏移量。正值用于将光标向右移动,负值用于将光标向左移动。
yOffSet 是一个输入参数,它为 WebDriver 提供了沿 y 轴移动的偏移量。正值用于将光标沿 y 轴向下移动,负值用于将光标向上移动。
当 xOffSet 和 yOffSet 的值导致光标移出文档时,会引发 MoveTargetOutOfBounds 异常。
让我们看看一个实际的工作示例。以下代码的目标是将光标移动到网页上的第 3 个瓷砖上:
@Test
public void shouldMoveByOffSet() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement three = driver.findElement(By.name("three"));
System.out.println("X coordinate: " + three.getLocation().getX()
+ ", Y coordinate: " + three.getLocation().getY());
Actions actions = new Actions(driver);
actions.moveByOffset(three.getLocation().getX() + 1, three.
getLocation().getY() + 1);
actions.perform();
}
输出将如下所示:

我们在坐标中增加了 +1,因为如果你在 Firebug 中观察元素,我们有一个 1 px 的边框样式。边框是一个 CSS 样式属性,当应用于元素时,会在元素周围添加指定颜色和厚度的边框。尽管之前的代码确实将鼠标移动到了第 3 个瓷砖上,但我们没有意识到这一点,因为我们没有在那里执行任何操作。我们将在使用 moveByOffset() 方法与 click() 方法结合时看到这一点。
在当前位置点击的动作
click() 方法用于模拟鼠标在其当前位置的左键点击。此方法实际上并不确定点击的位置或元素。它只是在那个时间点点击任何地方。因此,此方法通常与其他动作结合使用,而不是独立使用,以创建复合动作。
click() 方法的 API 语法如下:
public Actions click().
click() 方法实际上并没有任何关于它在何处执行操作的上下文信息;因此,它不接收任何输入参数。让我们看看 click() 方法的代码示例:
@Test
public void shouldMoveByOffSetAndClick() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement seven = driver.findElement(By.name("seven"));
System.out.println("X coordinate: " + seven.getLocation().getX() +
", Y coordinate: " + seven.getLocation().getY());
Actions actions = new Actions(driver);
actions.moveByOffset(seven.getLocation().getX() + 1, seven.
getLocation().getY() + 1).click();
actions.perform();
}
在上述示例中,我们使用了 moveByOffset() 和 click() 方法的组合,将光标从点 (0, 0) 移动到 7 号瓷砖的位置。因为鼠标的初始位置是 (0, 0),所以 moveByOffset() 方法提供的 x、y 偏移量实际上就是 7 号瓷砖的位置。现在让我们尝试将光标从 1 号瓷砖移动到 11 号瓷砖,然后从那里移动到 5 号瓷砖,看看代码是如何的。在我们进入代码之前,让我们使用 Firebug 检查 Selectable.html 页面。以下是每个瓷砖的样式:
#selectable li {
float: left;
font-size: 4em;
height: 80px;
text-align: center;
width: 100px;
}
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widgetheader .ui-state-default {
background: url("images/ui-bg_glass_75_e6e6e6_1x400.png") repeat-x
scroll 50% 50% #E6E6E6;
border: 1px solid #D3D3D3;
color: #555555;
font-weight: normal;
}
在前面的样式代码中,我们关注于偏移量移动的三个元素是:height、width 和 border 厚度。在这里,height 值是 80px,width 值是 100px,border 值是 1px。使用这三个因素来计算从一个瓷砖导航到另一个瓷砖的偏移量。请注意,任何两个瓷砖之间的边框厚度将产生 2 px,即每个瓷砖各 1 px。以下是一个使用 moveByOffset 和 click() 方法从 1 号瓷砖导航到 11 号瓷砖,然后从那里导航到 5 号瓷砖的代码示例:
@Test
public void shouldMoveByOffSetAndClickMultiple() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement one = driver.findElement(By.name("one"));
WebElement eleven = driver.findElement(By.name("eleven"));
WebElement five = driver.findElement(By.name("five"));
int border = 1;
int tileWidth = 100;
int tileHeight = 80;
Actions actions = new Actions(driver);
//Click on One
actions.moveByOffset(one.getLocation().getX() + border, one.getLocation().getY() + border).click();
actions.build().perform();
// Click on Eleven
actions.moveByOffset(2 * tileWidth + 4 * border, 2 * tileHeight + 4 * border).click();
actions.build().perform();
//Click on Five
actions.moveByOffset(-2 * tileWidth - 4 * border, -tileHeight - 2 * border).
click();
actions.build().perform();
}
点击 WebElement 的操作
我们已经看到了如何通过计算偏移量来点击 WebElement。这个过程可能并不总是需要,尤其是当 WebElement 有自己的标识符时,比如名称或 ID。我们可以使用 click() 方法的另一个重载版本来直接在 WebElement 上点击。
在 WebElement 上点击的 API 语法如下:
public Actions click(WebElement onElement)
此方法输入参数是一个 WebElement 实例,该实例上的 click 操作将被执行。此方法,就像 Actions 类中的所有其他方法一样,将返回一个 Actions 实例。
现在让我们尝试修改之前的代码示例,使用 click(WebElement) 方法而不是使用 moveByOffset() 方法来移动到 WebElement 的位置,并使用 click() 方法点击它:
@Test
public void shouldClickOnElement() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement one = driver.findElement(By.name("one"));
WebElement eleven = driver.findElement(By.name("eleven"));
WebElement five = driver.findElement(By.name("five"));
Actions actions = new Actions(driver);
//Click on One
actions.click(one);
actions.build().perform();
// Click on Eleven
actions.click(eleven);
actions.build().perform();
//Click on Five
actions.click(five);
actions.build().perform();
}
现在,moveByOffset() 方法已经被 click(WebElement) 方法所取代,突然之间,复杂的坐标几何从代码中消失了。如果你是一名测试人员,这是推动你的开发者为 WebElement 提供标识符的另一个好理由。
如果您观察了之前的 moveByOffset 和 click 方法的示例,所有移动鼠标和点击 1、11 和 5 号瓷砖的操作都是分别构建和执行的。这不是我们使用 Actions 类的方式。您实际上可以将所有这些操作一起构建,然后执行。因此,前面的代码将变成如下所示:
@Test
public void shouldClickOnElement() {
driver.get("http://guidebook.seleniumacademy.com/Selectable.html");
WebElement one = driver.findElement(By.name("one"));
WebElement eleven = driver.findElement(By.name("eleven"));
WebElement five = driver.findElement(By.name("five"));
Actions actions = new Actions(driver);
actions.click(one)
.click(eleven)
.click(five)
.build().perform();
}
在当前位置点击并保持操作
clickAndHold()方法是actions类的一个方法,它会在元素上左击并按住,而不释放鼠标的左键。这个方法在执行如拖放等操作时非常有用。这是actions类提供的clickAndHold()方法的变体之一。我们将在下一节讨论另一个变体。
现在打开随书附带的Sortable.html文件。你可以看到瓷砖可以从一个位置移动到另一个位置。现在让我们尝试将瓷砖 3 移动到瓷砖 2 的位置。完成此操作涉及的步骤如下:
-
将光标移动到瓷砖 3 的位置。
-
点击并按住瓷砖 3。
-
将光标移动到这个位置到瓷砖 2 的位置。
现在让我们看看如何使用 WebDriver 的clickAndHold()方法来完成这个操作:
@Test
public void shouldClickAndHold() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
Actions actions = new Actions(driver);
//Move tile3 to the position of tile2
actions.moveByOffset(200, 20)
.clickAndHold()
.moveByOffset(120, 0)
.perform();
}
让我们分析以下代码行:
actions.moveByOffset(200, 20)
.clickAndHold()
.moveByOffset(120, 0)
.perform();
瓷砖移动将与以下截图类似:

首先,我们将光标移动到瓷砖 3 的位置。然后,我们点击并按住瓷砖 3。然后,我们将光标水平移动120px到瓷砖 2 的位置。最后一行执行所有前面的操作。现在在你的 Eclipse 中执行这个操作,看看会发生什么。如果你仔细观察,瓷砖 3 并没有正确地移动到瓷砖 2 的位置。这是因为我们还没有释放左键。我们只是命令 WebDriver 点击并按住,但没有释放。
点击并按住WebElement操作
在上一节中,我们看到了clickAndHold()方法,它会在光标当前位置点击并按住一个WebElement。它不关心它处理的是哪个元素。所以,如果我们想处理网页上的特定WebElement,我们必须首先将光标移动到适当的位置,然后执行clickAndHold()操作。为了避免在几何上移动光标的麻烦,WebDriver 为开发者提供了clickAndHold()方法的另一个变体或重载方法,该方法接受WebElement作为输入。
API 语法如下:
public Actions clickAndHold(WebElement onElement)
此方法的输入参数是要点击并按住的WebElement。返回类型,如Actions类的所有其他方法一样,是Actions实例。现在让我们重构上一节的示例,使用此方法,如下所示:
@Test
public void shouldClickAndHoldElement() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
Actions actions = new Actions(driver);
WebElement three = driver.findElement(By.name("three"));
//Move tile3 to the position of tile2
actions.clickAndHold(three)
.moveByOffset(120, 0)
.perform();
}
唯一的改变是我们已经移除了将光标移动到(200, 20)位置的操作,并提供了WebElement给clickAndHold()方法,该方法将负责识别WebElement。
在当前位置释放操作
现在,在之前的示例中,我们看到了如何点击并按住一个元素。对按住的WebElement必须采取的最终操作是释放它,以便元素可以被放下或从鼠标中释放。release()方法是可以在WebElement上释放左鼠标按钮的方法。
release() 方法的 API 语法如下:public Actions release()。
前面的方法不接收任何输入参数,并返回 Actions 类的实例。
现在,让我们修改之前的代码,使其包含 release 动作:
@Test
public void shouldClickAndHoldAndRelease() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
WebElement three = driver.findElement(By.name("three"));
Actions actions = new Actions(driver);
//Move tile3 to the position of tile2
actions.clickAndHold(three)
.moveByOffset(120, 0)
.release()
.perform();
}
前面的代码将确保鼠标在指定位置释放。
在另一个 WebElement 上释放动作
这是 release() 方法的重载版本。使用它,你实际上可以在另一个 WebElement 的中间释放当前持有的 WebElement。这样,我们就不必计算目标 WebElement 从持有 WebElement 的偏移量。
API 语法如下:
public Actions release(WebElement onElement)
前面方法的输入参数显然是鼠标应该释放到的目标 WebElement。返回类型是 Actions 类的实例。
让我们修改前面的代码示例以使用此方法:
@Test
public void shouldClickAndHoldAndReleaseOnElement() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
WebElement three = driver.findElement(By.name("three"));
WebElement two = driver.findElement(By.name("two"));
Actions actions = new Actions(driver);
//Move tile3 to the position of tile2
actions.clickAndHold(three)
.release(two)
.perform();
}
看看前面的代码有多简单。我们移除了所有的 moveByOffset 代码,并添加了以名为 two 的 WebElement 作为输入参数的 release() 方法。
在不调用 clickAndHold() 方法的情况下调用 release() 或 release(WebElement) 方法将导致未定义的行为。
移动到元素动作
moveToElement() 方法是 WebDriver 的另一种方法,帮助我们将鼠标光标移动到网页上的 WebElement。
moveToElement() 方法的 API 语法如下:
public Actions moveToElement(WebElement toElement)
前面方法的输入参数是鼠标应该移动到的目标 WebElement。现在回到本章的 当前位置点击并保持 动作部分,尝试修改代码以使用此方法。以下是我们在 当前位置点击并保持动作 部分编写的代码:
@Test
public void shouldClickAndHold() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
Actions actions = new Actions(driver);
//Move tile3 to the position of tile2
actions.moveByOffset(200, 20)
.clickAndHold()
.moveByOffset(120, 0)
.perform();
}
在前面的代码中,我们将用 moveToElement(WebElement) 方法替换 moveByOffset(x, y) 方法:
@Test
public void shouldClickAndHoldAndMove() {
driver.get("http://guidebook.seleniumacademy.com/Sortable.html");
WebElement three = driver.findElement(By.name("three"));
Actions actions = new Actions(driver);
//Move tile3 to the position of tile2
actions.moveToElement(three)
.clickAndHold()
.moveByOffset(120, 0)
.perform();
}
在前面的代码中,我们已经移动到第 3 个方块,点击并保持,然后通过指定其偏移量移动到第 2 个方块的位置。如果你愿意,你可以在 perform() 方法之前添加 release() 方法。
实现同一任务可能有多种方式。用户需要根据具体情况选择最合适的方案。
拖放动作
在许多情况下,我们可能需要拖放网页上的组件或 WebElement。我们可以通过使用到目前为止看到的大多数动作来完成此操作。但是 WebDriver 给我们提供了一个方便的即用型方法。让我们看看它的 API 语法。
dragAndDropBy() 方法的 API 语法如下:
public Actions dragAndDropBy(WebElement source, int xOffset,int yOffset)
WebElement 输入参数是要拖动的目标 WebElement,xOffset 参数是要移动的水平偏移量,yOffset 参数是要移动的垂直偏移量。
让我们看看一个代码示例。打开本书提供的 HTML 文件 DragMe.html。它有一个正方形框,如下面的截图所示:

您实际上可以将该矩形拖动到网页上的任何位置。让我们看看如何使用 WebDriver 来实现这一点。以下是一个代码示例:
@Test
public void shouldDrag() {
driver.get("http://guidebook.seleniumacademy.com/DragMe.html");
WebElement dragMe = driver.findElement(By.id("draggable"));
Actions actions = new Actions(driver);
actions.dragAndDropBy(dragMe, 300, 200).perform();
}
在前面的代码中,dragMe 是通过其 id 识别的 WebElement,它被水平拖动 300px 和垂直拖动 200px。以下截图显示了如何从这个位置拖动元素:

拖动操作
dragAndDrop() 方法与 dragAndDropBy() 方法类似。唯一的区别在于,我们不是通过偏移量移动 WebElement,而是将其移动到目标元素上。
dragAndDrop() 方法的 API 语法如下:
public Actions dragAndDrop(WebElement source, WebElement target)
前面方法的输入参数是源 WebElement 和目标 WebElement,返回类型是 Actions 类。
让我们看看一个实际的工作代码示例。打开与本书一起提供的 DragAndDrop.html 文件,其中包含两个正方形框,如图所示:

在这里,我们实际上可以将“拖动我到目标矩形”拖动到“在这里放下”矩形。试试看。让我们看看如何使用 WebDriver 实现这一点:
@Test
public void shouldDragAndDrop() {
driver.get("http://guidebook.seleniumacademy.com/DragAndDrop.html");
WebElement src = driver.findElement(By.id("draggable"));
WebElement trgt = driver.findElement(By.id("droppable"));
Actions actions = new Actions(driver);
actions.dragAndDrop(src, trgt).perform();
}
在前面的代码中,源和目标 WebElement 是通过它们的 ID 识别的,并使用 dragAndDrop() 方法将一个元素拖动到另一个元素上。以下截图显示了将第一个正方形框拖放到第二个框上的脚本:

当前位置的双击操作
接下来,我们将讨论另一种可以使用鼠标执行的操作,doubleClick() 是 WebDriver 提供的另一种非常规方法,用于模拟鼠标的双击。此方法与 click() 方法类似,有两种形式。一种是双击一个 WebElement,我们将在下一节中讨论;另一种是在光标当前位置单击,这将在本节中讨论。
API 语法如下:
public Actions doubleClick()
显然,前面的方法不接收任何输入参数,因为它只是单击当前光标位置并返回一个 Actions 类实例。让我们看看如何将前面的代码转换为使用此方法:
@Test
public void shouldDoubleClick() {
driver.get("http://guidebook.seleniumacademy.com/DoubleClick.html");
WebElement dblClick= driver.findElement(By.name("dblClick"));
Actions actions = new Actions(driver);
actions.moveToElement(dblClick).doubleClick().perform();
}
在前面的代码中,我们使用了 moveToElement(WebElement) 方法将鼠标移动到按钮元素的位置,并在当前位置进行了双击。以下是双击样本页面上的元素后的输出:

WebElement 的双击操作
现在我们已经看到了一个在当前位置双击的方法,我们将讨论 WebDriver 提供的另一种模拟 WebElement 双击的方法。
doubleClick() 方法的 API 语法如下:
public Actions doubleClick(WebElement onElement)
前面方法的输入参数是要双击的目标 WebElement,返回类型是Actions类。
让我们看看一个代码示例。打开DoubleClick.html文件,单击点击“点击我”按钮。你不应该看到任何动作发生。现在双击按钮;你应该看到一个弹出的警告说“双击了!!”。现在我们将尝试使用 WebDriver 做同样的事情。以下是要执行的代码:
@Test
public void shouldDoubleClickElement() {
driver.get("http://guidebook.seleniumacademy.com/DoubleClick.html");
WebElement dblClick = driver.findElement(By.name("dblClick"));
Actions actions = new Actions(driver);
actions.doubleClick(dblClick).perform();
}
执行前面的代码后,你应该看到一个包含“按钮已被双击”的警告对话框。
在 WebElement 上的上下文点击操作
contextClick()方法,也称为右键点击,在许多网页上相当常见。它显示一个类似于以下截图的菜单:

这个上下文菜单可以通过在 WebElement 上右键点击鼠标来访问。WebDriver 为开发者提供了一个使用contextClick()方法模拟该动作的选项。像许多其他方法一样,此方法也有两种变体。一个是点击当前位置,另一个重载方法是点击 WebElement。让我们在这里讨论在 WebElement 上点击的上下文。
contextClick()方法的 API 语法如下:
public Actions contextClick(WebElement onElement)
输入参数显然是要右键点击的 WebElement,返回类型是Actions实例。像我们通常做的那样,现在是时候看看一个代码示例了。如果你打开ContextClick.html文件,你可以在页面上可见的文本上右键点击,这将显示上下文菜单。现在点击任何项都会弹出一个包含已点击项的警告对话框。现在让我们看看如何使用以下代码在 WebDriver 中实现这一点:
@Test
public void shouldContextClick() {
driver.get("http://guidebook.seleniumacademy.com/ContextClick.html");
WebElement contextMenu = driver.findElement(By.id("div-context"));
Actions actions = new Actions(driver);
actions.contextClick(contextMenu)
.click(driver.findElement(By.name("Item 4")))
.perform();
}
在前面的代码中,我们首先在 contextMenu 的 WebElement 上使用contextClick()方法进行了右键点击,然后从上下文菜单中点击了第 4 项。这将弹出一个包含“第 4 项已点击”的警告对话框。
在当前位置的上下文点击操作
现在我们已经看到了在 WebElement 上的上下文点击,是时候探索在当前鼠标位置上的contextClick()方法了。contextClick()方法的 API 语法如下:
public Actions contextClick()
如预期的那样,前面的方法不需要任何输入参数,并返回Actions实例。让我们看看为了使用此方法对前面的示例所需的必要修改。以下是对此进行重构的代码:
@Test
public void shouldContextClickAtCurrentLocation() {
driver.get("http://guidebook.seleniumacademy.com/ContextClick.html");
WebElement contextMenu = driver.findElement(By.id("div-context"));
Actions actions = new Actions(driver);
actions.moveToElement(contextMenu)
.contextClick()
.click(driver.findElement(By.name("Item 4")))
.perform();
}
前面的代码首先将光标移动到div-context WebElement,然后对其进行上下文点击。
学习基于键盘的交互
到目前为止,我们已经看到了可以使用鼠标执行的所有操作。现在,我们需要看看 Actions 类中特定于键盘的一些操作。基本上,Actions 类中有三种特定于键盘的不同操作。它们是 keyUp、keyDown 和 sendKeys 操作,每个操作都有两个重载方法。一个方法是在 WebElement 上直接执行操作,另一个方法是不考虑其上下文来执行方法。
keyDown 和 keyUp 操作
keyDown() 方法用于模拟按下并保持按键的动作。这里我们引用的键是 Shift、Ctrl 和 Alt 键。keyUp() 方法用于释放使用 keyDown() 方法按下的键。keyDown() 方法的 API 语法如下:
public Actions keyDown(Keys theKey) throws IllegalArgumentException
当传递的键不是 Shift、Ctrl 和 Alt 键之一时,会抛出 IllegalArgumentException。keyUp() 方法的 API 语法如下:
public Actions keyUp(Keys theKey)
在一个已经执行了 keyDown 操作的键上执行 keyUp 操作,如果没有正在进行的 keyDown 操作,将会产生一些意外的结果。因此,我们必须确保在执行 keyUp 操作之前已经执行了 keyDown 操作。
sendKeys 方法
这用于在 WebElements(如文本框、文本区域等)中输入字母数字和特殊字符键。这与 WebElement.sendKeys(CharSequence keysToSend) 方法不同,因为这个方法期望在调用之前 WebElements 已经获得焦点。sendkeys() 方法的 API 语法如下:
public Actions sendKeys(CharSequence keysToSend)
我们期望您使用 keyUp、keyDown 和 sendKeys() 方法围绕这些键盘事件实现一些测试脚本。
摘要
在本章中,我们学习了如何使用 actions 类创建一系列操作,并通过 perform() 方法将它们构建成一个组合操作以单次执行。这样,我们可以将一系列复杂用户操作聚合到一个单一功能中,并单次执行。在下一章中,我们将学习 WebDriver 事件以及如何使用 WebDriver 监听和执行高级操作。
问题
-
判断对错 – 拖放操作需要源元素和目标元素。
-
列出我们可以使用 actions API 执行的键盘方法。
-
哪种 actions API 方法可以帮助执行双击操作?
-
使用 actions API,我们如何执行保存选项(即 Ctrl + S)?
-
我们如何使用 actions API 打开上下文菜单?
更多信息
您可以通过以下链接获取更多关于本章所涉及主题的信息:
-
在
github.com/SeleniumHQ/selenium/wiki/Advanced-User-Interactions了解更多关于高级用户交互的信息 -
请参阅第四章节:使用 Selenium API,出自 Unmesh Gundecha 所著的《Selenium 测试工具手册》第二版,以及第六章节:利用高级用户交互 API,出自 Mark Collin 所著的《精通 Selenium WebDriver》,以获取更多关于动作 API 的示例。
第六章:理解 WebDriver 事件
Selenium WebDriver 提供了一个 API 来跟踪使用 WebDriver 执行测试脚本时发生的各种事件。许多导航事件在 WebDriver 内部事件发生之前和之后被触发(例如在导航到 URL 之前和之后,以及浏览器后退导航之前和之后),这些事件可以被跟踪和捕获。为了抛出一个事件,WebDriver 提供了一个名为EventFiringWebDriver的类,为了捕获该事件,它为测试脚本开发者提供了一个名为WebDriverEventListener的接口。测试脚本开发者应该为其提供的接口重写方法提供自己的实现。在本章中,我们将探讨以下主题:
-
如何使用
EventFiringWebDriver监听和处理各种浏览器导航事件 -
如何在测试脚本执行过程中监听和处理被触发的网页元素动作事件
-
向 WebDriver 添加额外的功能以捕获性能或可访问性测试
介绍eventFiringWebDriver和eventListener类
EventFiringWebDriver类是 WebDriver 的一个包装器,它赋予了驱动器触发事件的能力。另一方面,EventListener类等待监听EventFiringWebDriver,并处理所有被分派的事件。可能存在多个监听器等待从EventFiringWebDriver类接收事件触发。所有的事件监听器都应该注册到EventFiringWebDriver类以接收通知。
下面的流程图解释了在执行测试用例期间,如何捕获由EventFiringWebDriver引发的所有事件:

创建EventListener实例
EventListener类处理由EventFiringWebDriver类分派的所有事件。创建EventListener类有两种方式:
-
通过实现
WebDriverEventListener接口。 -
通过扩展 WebDriver 库中提供的
AbstractWebDriverEventListener类。
作为测试脚本开发者,选择哪种方式取决于你。
实现WebDriverEventListener
WebDriverEventListener接口声明了所有的事件方法。一旦EventFiringWebDriver类意识到发生了事件,它就会调用已注册的WebDriverEventListener方法。在这里,我们创建了一个名为IAmTheEventListener的类,并实现了WebDriverEventListener。现在我们需要为其中声明的方法提供实现。目前,在WebDriverEventListener中,有 15 个方法。我们将很快讨论每一个。确保 IDE 为我们提供了这些方法的占位符实现。我们创建的包含所有 15 个重写方法的是以下类(我们为其中几个方法提供了示例实现):
public class IAmTheEventListener implements WebDriverEventListener {
@Override
public void beforeAlertAccept(WebDriver webDriver) {
}
@Override
public void afterAlertAccept(WebDriver webDriver) {
}
@Override
public void afterAlertDismiss(WebDriver webDriver) {
}
@Override
public void beforeAlertDismiss(WebDriver webDriver) {
}
@Override
public void beforeNavigateTo(String url, WebDriver webDriver) {
System.out.println("Before Navigate To " + url);
}
@Override
public void afterNavigateTo(String s, WebDriver webDriver) {
System.out.println("Before Navigate Back. Right now I'm at "
+ webDriver.getCurrentUrl());
}
@Override
public void beforeNavigateBack(WebDriver webDriver) {
}
@Override
public void afterNavigateBack(WebDriver webDriver) {
}
@Override
public void beforeNavigateForward(WebDriver webDriver) {
}
@Override
public void afterNavigateForward(WebDriver webDriver) {
}
@Override
public void beforeNavigateRefresh(WebDriver webDriver) {
}
@Override
public void afterNavigateRefresh(WebDriver webDriver) {
}
@Override
public void beforeFindBy(By by, WebElement webElement, WebDriver webDriver) {
}
@Override
public void afterFindBy(By by, WebElement webElement, WebDriver webDriver) {
}
@Override
public void beforeClickOn(WebElement webElement, WebDriver webDriver) {
}
@Override
public void afterClickOn(WebElement webElement, WebDriver webDriver) {
}
@Override
public void beforeChangeValueOf(WebElement webElement, WebDriver webDriver, CharSequence[] charSequences) {
}
@Override
public void afterChangeValueOf(WebElement webElement, WebDriver webDriver, CharSequence[] charSequences) {
}
@Override
public void beforeScript(String s, WebDriver webDriver) {
}
@Override
public void afterScript(String s, WebDriver webDriver) {
}
@Override
public void onException(Throwable throwable, WebDriver webDriver) {
}
}
扩展AbstractWebDriverEventListener
创建监听器类的第二种方式是通过扩展 AbstractWebDriverEventListener 类。AbstractWebDriverEventListener 是一个实现了 WebDriverEventListener 的抽象类。尽管它并没有为 WebDriverEventListener 接口中的方法提供任何实现,但它创建了一个虚拟实现,这样您创建的监听器类就不必包含所有方法,只需包含您作为测试脚本开发者感兴趣的方法即可。以下是我们创建的一个扩展 AbstractWebDriverEventListener 并为其提供了一些方法实现的类。这样,我们就可以仅覆盖我们感兴趣的方法,而不是我们类中的所有方法:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.events.AbstractWebDriverEventListener;
public class IAmTheEventListener2 extends AbstractWebDriverEventListener {
@Override
public void beforeNavigateTo(String url, WebDriver driver) {
System.out.println("Before Navigate To "+ url);
}
@Override
public void beforeNavigateBack(WebDriver driver) {
System.out.println("Before Navigate Back. Right now I'm at "
+ driver.getCurrentUrl());
}
}
创建 WebDriver 实例
现在我们已经创建了一个监听所有生成事件的监听器类,是时候创建我们的测试脚本类,并让它调用 IAmTheDriver.java。在类创建后,我们在其中声明一个 ChromeDriver 实例:
WebDriver driver = new ChromeDriver();
ChromeDriver 实例将是驱动所有驱动事件的底层驱动实例。这并不是什么新鲜事。下一节中解释的步骤是我们将这个驱动器实例化为 EventFiringWebDriver。
创建 EventFiringWebDriver 和 EventListener 实例
现在我们已经有了基本的驱动实例,在构造 EventFiringWebDriver 实例时将其作为参数传递。我们将使用这个驱动实例来执行所有进一步的用户操作。
现在,使用以下代码,实例化我们之前创建的 EventListener,即 IAmTheEventListener.java 或 IAmTheEventListener2.java 类。这将是一个将所有事件分发的类:
EventFiringWebDriver eventFiringDriver =
new EventFiringWebDriver(driver);
IAmTheEventListener eventListener =
new IAmTheEventListener();
将 EventListener 注册到 EventFiringWebDriver
为了使 EventListener 能够通知事件执行,我们已经将其注册到 EventFiringWebDriver 类。现在 EventFiringWebDriver 类将知道在哪里发送通知。这是通过以下代码行完成的:eventFiringDriver.register(eventListener);
执行和验证事件
现在是测试脚本执行事件的时候了,比如导航事件。让我们首先导航到 Google,然后是 Facebook。我们将使用浏览器的后退导航回到 Google。测试脚本的完整代码如下:
public class IAmTheDriver {
public static void main(String... args){
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
WebDriver driver = new ChromeDriver();
try {
EventFiringWebDriver eventFiringDriver = new
EventFiringWebDriver(driver);
IAmTheEventListener eventListener = new IAmTheEventListener();
eventFiringDriver.register(eventListener);
eventFiringDriver.get("http://www.google.com");
eventFiringDriver.get("http://www.facebook.com");
eventFiringDriver.navigate().back();
} finally {
driver.close();
driver.quit();
}
}
}
在前面的代码中,我们修改了我们的监听器类,以记录从 AbstractWebDriverEventListener 类继承的 navigateTo 和 navigateBack 事件的前后事件。修改后的方法如下:
@Override
public void beforeNavigateTo(String url, WebDriver driver) {
System.out.println("Before Navigate To: " + url
+ " and Current url is: " + driver.getCurrentUrl());
}
@Override
public void afterNavigateTo(String url, WebDriver driver) {
System.out.println("After Navigate To: " + url
+ " and Current url is: " + driver.getCurrentUrl());
}
@Override
public void beforeNavigateBack(WebDriver driver) {
System.out.println("Before Navigate Back. Right now I'm at " + driver.getCurrentUrl());
}
@Override
public void afterNavigateBack(WebDriver driver) {
System.out.println("After Navigate Back. Right now I'm at " + driver.getCurrentUrl());
}
现在如果您执行测试脚本,输出将如下所示:
Before Navigate To: http://www.google.com and Current url is: data:,
After Navigate To: http://www.google.com and Current url is: https://www.google.com/?gws_rd=ssl
Before Navigate To: http://www.facebook.com and Current url is: https://www.google.com/?gws_rd=ssl
After Navigate To: http://www.facebook.com and Current url is: https://www.facebook.com/
Before Navigate Back. Right now I'm at https://www.facebook.com/
After Navigate Back. Right now I'm at https://www.google.com/?gws_rd=ssl
注册多个 EventListeners
我们可以使用 EventFiringWebDriver 注册多个监听器。一旦事件发生,所有注册的监听器都会收到通知。让我们修改我们的测试脚本以注册 IAmTheListener.java 和 IAmTheListener2.java 文件:
public class RegisteringMultipleListeners {
public static void main(String... args){
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
WebDriver driver = new ChromeDriver();
try {
EventFiringWebDriver eventFiringDriver = new
EventFiringWebDriver(driver);
IAmTheEventListener eventListener = new IAmTheEventListener();
IAmTheEventListener2 eventListener2 = new
IAmTheEventListener2();
eventFiringDriver.register(eventListener);
eventFiringDriver.register(eventListener2);
eventFiringDriver.get("http://www.google.com");
eventFiringDriver.get("http://www.facebook.com");
eventFiringDriver.navigate().back();
} finally {
driver.close();
driver.quit();
}
}
}
稍微修改监听器以区分日志语句。现在如果您执行前一个代码,您将看到以下输出:
Before Navigate To: http://www.google.com and Current url is: data:,
Before Navigate To http://www.google.com
After Navigate To: http://www.google.com and Current url is: https://www.google.com/?gws_rd=ssl
Before Navigate To: http://www.facebook.com and Current url is: https://www.google.com/?gws_rd=ssl
Before Navigate To http://www.facebook.com
After Navigate To: http://www.facebook.com and Current url is: https://www.facebook.com/
Before Navigate Back. Right now I'm at https://www.facebook.com/
Before Navigate Back. Right now I'm at https://www.facebook.com/
After Navigate Back. Right now I'm at https://www.google.com/?gws_rd=ssl
探索不同的 WebDriver 事件监听器
我们已经看到了一些在 EventListeners 中执行时被调用的方法,例如,在触发 navigateTo 事件时,会调用导航前后的方法。在这里,我们将看到 WebDriverEventListener 提供的所有方法。
监听 WebElement 值更改
当在 WebElement 上执行 sendKeys() 或 clear() 方法时,会触发此事件。与此事件相关联有两个方法:
public void beforeChangeValueOf(WebElement element, WebDriver driver)
在 WebDriver 尝试更改 WebElement 的值之前,会调用前一个方法。作为参数,将 WebElement 本身传递给方法,以便在更改之前记录元素的值:
public void afterChangeValueOf(WebElement element, WebDriver driver)
前一个方法是第二个与值更改事件相关联的方法,在驱动程序更改 WebElement 的值之后被调用。同样,WebElement 和 WebDriver 作为参数传递给该方法。如果在更改值时发生异常,则此方法不会被调用。
监听被点击的 WebElement
当 WebElement 被点击时,即通过执行 webElement.click() 触发此事件。在 WebDriverEventListener 实现中,有两种方法可以监听此事件:
public void beforeClickOn(WebElement element, WebDriver driver)
当 WebDriver 即将点击特定的 WebElement 时,会调用前一个方法。将要点击的 WebElement 和执行点击操作的 WebDriver 作为参数传递给此方法,以便测试脚本开发者可以解释哪个驱动程序执行了点击操作,以及操作是在哪个元素上执行的:
public void afterClickOn(WebElement element, WebDriver driver)
EventFiringWebDriver 类在点击操作在 WebElement 上执行后通知前一个方法。类似于 beforeClickOn() 方法,此方法也接收 WebElement 和 WebDriver 实例。如果在点击事件期间发生异常,则此方法不会被调用。
监听 WebElement 搜索事件
当 WebDriver 使用 findElement() 或 findElements() 在网页上搜索 WebElement 时,会触发此事件。同样,与此事件相关联有两个方法:
public void beforeFindBy(By by, WebElement element, WebDriver driver)
在 WebDriver 开始在页面上搜索特定的 WebElement 之前,会调用前一个方法。作为参数,它发送定位机制,即要搜索的 WebElement 和执行搜索的 WebDriver 实例:
public void afterFindBy(By by, WebElement element, WebDriver driver)
类似地,EventFiringWebDriver 类在搜索元素结束并且找到元素后调用前一个方法。如果在搜索过程中发生任何异常,则此方法不会被调用,并且会引发异常。
监听浏览器后退导航
正如我们之前看到的,当使用driver.navigation().back()方法时,浏览器后退导航事件会被触发。浏览器在其历史记录中后退一级。就像所有其他事件一样,此事件与两个方法相关联:
public void beforeNavigateBack(WebDriver driver)
在浏览器历史记录回退之前,调用此方法。触发此事件的 WebDriver 作为参数传递给此方法:
public void afterNavigateBack(WebDriver driver)
正如所有在<<event>>之后的after方法一样,前面的方法在导航回退动作被触发时调用。前面的两个方法将不受浏览器导航的影响而被调用;也就是说,如果浏览器没有任何历史记录,你调用此方法,浏览器不会带你到任何历史记录。但是,即使在那种情况下,由于事件被触发,这两个方法也会被调用。
监听浏览器前进导航
此事件与浏览器后退导航非常相似,只是这是浏览器前进导航,因此它使用driver.navigate().forward()。与此事件相关的两个方法是:
-
public void afterNavigateForward(WebDriver driver) -
public void beforeNavigateForward(WebDriver driver)
正如浏览器后退导航一样,这些方法在浏览器是否前进一级的情况下都会被调用。
监听浏览器导航到事件
正如我们之前看到的,此事件在驱动程序执行driver.get(url)时发生。与此事件相关的方法定义如下:
-
public void beforeNavigateTo(java.lang.String url, WebDriver driver) -
public void afterNavigateTo(java.lang.String url, WebDriver driver)
用于驱动程序导航的 URL 作为参数传递给前面的方法,以及触发事件的驱动程序。
监听脚本执行
当驱动程序执行 JavaScript 时,此事件被触发。与此事件相关的方法定义如下:
-
public void beforeScript(java.lang.String script, WebDriver driver) -
public void afterScript(java.lang.String script, WebDriver driver)
前面的方法获取作为字符串执行的 JavaScript,以及执行它的 WebDriver 作为参数。如果在脚本执行过程中发生异常,则不会调用afterScript()方法。
监听异常
当 WebDriver 遇到异常时,此事件发生。例如,如果你尝试使用findElement()搜索一个 WebElement,而该元素在页面上不存在,则驱动程序会抛出异常(NoSuchElementException)。在此时刻,此事件被触发,以下方法会收到通知:
public void onException(java.lang.Throwable throwable, WebDriver driver)
在所有的after<<event>>方法中,我们已经看到,如果驱动程序遇到任何异常,它们将不会被调用。在这种情况下,将调用onException()方法,并将可抛出对象和 WebDriver 对象作为参数传递给它。
使用 EventFiringWebDriver 注销事件监听器
现在,我们已经看到了触发的事件的不同类型,以及通知所有注册监听器的 EventFiringWebDriver 类。如果你在任何时候想让你的某个事件监听器停止监听 EventFiringWebDriver,你可以通过从该驱动器注销来实现。以下 API 从驱动器注销了一个事件监听器:
public EventFiringWebDriver unregister(WebDriverEventListener eventListener)
该方法参数应该是想要退出接收事件通知的事件监听器。
执行可访问性测试
我们可以通过使用诸如 Google 的可访问性开发者工具(github.com/GoogleChrome/accessibility-developer-tools)等工具来执行基本的可访问性检查。我们可以在网页中注入 Google 可访问性测试库 并执行 可访问性审计。这可以在每次调用 afterNavigatTo() 后自动执行。在下面的代码示例中,我们将注入 Google 可访问性开发者工具提供的 axe_testing.js 文件并执行审计,这将打印报告到控制台:
public class IAmTheEventListener2 extends AbstractWebDriverEventListener {
@Override
public void beforeNavigateTo(String url, WebDriver driver) {
System.out.println("Before Navigate To "+ url);
}
@Override
public void beforeNavigateBack(WebDriver driver) {
System.out.println("Before Navigate Back. Right now I'm at "
+ driver.getCurrentUrl());
}
@Override
public void afterNavigateTo(String to, WebDriver driver) {
try {
JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
URL url = new URL("https://raw.githubusercontent.com/GoogleChrome/" +
"accessibility-developer-tools/stable/dist/js/axs_testing.js");
String script = IOUtils.toString(url.openStream(), StandardCharsets.UTF_8);
jsExecutor.executeScript(script);
String report = (String) jsExecutor.executeScript("var results = axs.Audit.run();" +
"return axs.Audit.createReport(results);");
System.out.println("### Accessibility Report for " + driver.getTitle() + "####");
System.out.println(report);
System.out.println("### END ####");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
报告将按如下所示打印到控制台:
### Accessibility Report for Google####
*** Begin accessibility audit results ***
An accessibility audit found
Warnings:
Warning: AX_FOCUS_01 (These elements are focusable but either invisible or obscured by another element) failed on the following element:
#hplogo > DIV > .fOwUFe > A
See https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#-ax_focus_01--these-elements-are-focusable-but-either-invisible-or-obscured-by-another-element for more information.
Warning: AX_TEXT_02 (Images should have an alt attribute) failed on the following element:
#hplogo > DIV > .fOwUFe > A > .fJOQGe
See https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#-ax_text_02--images-should-have-an-alt-attribute-unless-they-have-an-aria-role-of-presentation for more information.
...
*** End accessibility audit results ***
### END ####
本报告包含了一组审计规则,用于检查常见的可访问性问题。
捕获页面性能指标
测量和优化客户端性能对于无缝的用户体验至关重要,这对于使用 AJAX 的 Web 2.0 应用程序来说尤其关键。
捕获关键信息,例如页面加载时间、元素渲染和 JavaScript 代码执行时间,将有助于识别性能缓慢的区域,并优化整体客户端性能。
导航计时 是一个 W3C 标准 JavaScript API,用于测量网络上的性能。该 API 提供了一种简单的方法,可以原生地获取页面导航和加载事件的准确和详细的计时统计信息。它在 Internet Explorer 9、Google Chrome、Firefox 和基于 WebKit 的浏览器上可用。
该 API 通过 window.performance 对象的时间接口属性使用 JavaScript 访问。我们将每次导航到页面时捕获页面加载时间。这可以通过在 IAmTheEventListener2.java 的 afterNavigateTo() 方法中使用 JavaScriptExecutor 调用 window.performance 来完成,如下面的代码片段所示:
@Override
public void afterNavigateTo(String to, WebDriver driver) {
try {
JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
// Get the Load Event End
long loadEventEnd = (Long) jsExecutor.executeScript("return window.performance.timing.loadEventEnd;");
// Get the Navigation Event Start
long navigationStart = (Long) jsExecutor.executeScript("return window.performance.timing.navigationStart;");
// Difference between Load Event End and Navigation Event Start is // Page Load Time
System.out.println("Page Load Time is " + (loadEventEnd - navigationStart)/1000 + " seconds.");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
如前所述的代码中讨论的那样,window.performance 对象为我们提供了在 Browser Window 对象 中可用的性能指标。我们需要使用 JavaScript 来检索这个指标。在这里,我们正在收集 loadEventEnd 时间和 navigationEventStart 时间,并计算它们之间的差异,这将给我们页面加载时间。
摘要
在本章中,您学习了关于EventFiringWebDriver和EventListeners的知识,以及它们如何协同工作,通过帮助开发者调试在测试用例执行过程中的每一步发生的情况,使开发者的生活变得更轻松。您还学习了如何使用 WebDriver 事件在页面上执行不同类型的测试,例如可访问性和客户端性能检查。在下一章中,您将学习更多关于远程 WebDriver 的知识,用于在分布式和并行模式下运行远程机器上的测试,以进行跨浏览器测试。
问题
-
您可以使用
WebDriverEventListener接口来监听 WebDriver 事件——对还是错? -
您如何使用
WebDriverEventListener在调用sendKeys方法之前自动清除输入字段? -
Selenium 支持可访问性测试——对还是错?
更多信息
您可以通过以下链接获取更多关于本章涵盖主题的信息:
-
在
www.w3.org/TR/navigation-timing/上了解更多关于导航时间 API 的信息 -
在
github.com/GoogleChrome/accessibility-developer-tools上找到更多关于谷歌可访问性开发者工具的详细信息
第七章:探索 RemoteWebDriver
到目前为止,我们已经创建了测试用例,并尝试在各个浏览器上执行它们。所有这些测试都是针对安装在测试用例所在本地机器上的浏览器执行的。这并不总是可能的。有很大可能性您可能正在使用 Mac 或 Linux,但想在 Windows 机器上的 IE 上执行测试。在本章中,我们将学习以下主题:
-
使用
RemoteWebDriver在远程机器上执行测试用例 -
JSON 线协议的详细解释
介绍 RemoteWebDriver
RemoteWebDriver是WebDriver接口的实现类,测试脚本开发者可以使用它通过Selenium 独立服务器在远程机器上执行他们的测试脚本。RemoteWebDriver有两个部分:服务器和客户端。在我们开始使用它们之前,让我们回顾一下我们一直在做什么。
以下图表解释了我们到目前为止所做的工作:

使用 WebDriver 客户端库、Chrome 驱动程序(或 IE 驱动程序或 Firefox 的 Gecko 驱动程序)、Chrome 浏览器(或 IE 浏览器或 Firefox 浏览器)的测试脚本位于同一台机器上。浏览器正在加载网络应用程序,该应用程序可能位于远程服务器上,也可能不是;无论如何,这超出了我们讨论的范围。我们将讨论以下测试脚本执行的不同场景:

测试脚本位于本地机器上,而浏览器安装在远程机器上。在这种情况下,RemoteWebDriver就派上用场。如前所述,与RemoteWebDriver相关联有两个组件:服务器和客户端。让我们从Selenium 独立服务器开始。
理解 Selenium 独立服务器
Selenium 独立服务器是一个组件,它监听端口以接收来自RemoteWebDriver客户端的各种请求。一旦它收到请求,它将它们转发到以下任何一个:Chrome 驱动程序、IE 驱动程序或 Firefox 的 Gecko 驱动程序,具体取决于RemoteWebDriver客户端的请求。
下载 Selenium 独立服务器
让我们下载Selenium 独立服务器并开始运行它。您可以从www.seleniumhq.org/download/下载它,但出于我们的目的,让我们下载特定版本的它,因为我们正在使用 WebDriver 版本 3.12.0。此服务器 JAR 文件应下载到包含浏览器的远程机器上。同时,请确保远程机器上已安装 Java 运行时。
运行服务器
在远程机器上打开您的命令行工具,导航到您已下载 JAR 文件的目录。现在,要启动 Selenium 独立服务器,请执行以下命令:
java -jar selenium-server-standalone-3.12.0.jar
以下截图显示了您应该在控制台中看到的内容:

现在服务器已经启动并正在监听 <remote-machine-ip>:4444 地址以接收来自 RemoteWebDriver 客户端的远程连接。之前看到的图像(介绍 RemoteWebDriver 部分的第二张图像)将如下所示:

在远程机器上,Selenium Standalone Server 将在测试脚本和浏览器之间进行接口,如图所示。测试脚本将首先与 Selenium Standalone Server 建立连接,该服务器将把命令转发到远程机器上安装的浏览器。
理解 RemoteWebDriver 客户端
现在我们已经启动并运行了 Selenium Standalone 服务器,是时候我们创建 RemoteWebDriver 客户端了。幸运的是,我们不需要做太多来创建 RemoteWebDriver 客户端。它只是作为 RemoteWebDriver 客户端的语言绑定客户端库。RemoteWebDriver 将将测试脚本请求或命令转换为 JSON 负载,并通过 JSON 线协议将它们发送到 RemoteWebDriver 服务器。
当你在本地执行测试时,WebDriver 客户端库会直接与 Chrome Driver、IE Driver 或 Gecko Driver 通信。现在当你尝试远程执行测试时,WebDriver 客户端库会与 Selenium Standalone Server 通信,而服务器会根据测试脚本请求的 Chrome Driver、IE Driver 或 Gecko Driver(用于 Firefox)与浏览器进行通信,使用 DesiredCapabilities 类。我们将在下一节中探讨 DesiredCapabilities 类。
将现有测试脚本转换为使用 RemoteWebDriver 服务器
让我们以一个我们在本地执行的测试脚本为例;也就是说,测试脚本和浏览器在同一台机器上:
@BeforeClass
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
}
之前的测试脚本创建了一个 Chrome Driver 实例并启动了 Chrome 浏览器。现在,让我们尝试将此测试脚本转换为使用我们之前启动的 Selenium Standalone Server。在我们这样做之前,让我们看看 RemoteWebDriver 的构造函数,如下所示:
RemoteWebDriver(java.net.URL remoteAddress, Capabilities desiredCapabilities)
构造函数的输入参数包括远程机器上运行的 Selenium Standalone Server 的地址(主机名或 IP)以及运行测试所需的需求能力(例如浏览器名称和/或操作系统)。我们将在稍后看到这些需求能力。
现在,让我们修改测试脚本以使用 RemoteWebDriver。将 WebDriver driver = new ChromeDriver(); 替换为以下代码:
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
driver = new RemoteWebDriver(new URL("http://10.172.10.1:4444/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
我们已经创建了一个尝试连接到 http://10.172.10.1:4444/wd/hub 的 RemoteWebDriver 实例,其中 Selenium Standalone Server 正在运行并监听请求。完成此操作后,我们还需要指定测试用例应在哪个浏览器上执行。这可以通过使用 DesiredCapabilities 实例来完成。
对于这个例子,使用的 IP 是 10.172.10.1。然而,在您的案例中,它将是不同的。您需要获取 Selenium Standalone Server 运行的机器的 IP 地址,并替换本书中使用的示例 IP。
在运行测试之前,我们需要通过指定 ChromeDriver 的路径来重启 Selenium Standalone Server。
java -jar -Dwebdriver.chrome.driver=chromedriver selenium-server-standalone-3.12.0.jar
使用RemoteWebDriver运行以下测试将启动 Chrome 浏览器并在其上执行您的测试用例。因此,修改后的测试用例将如下所示:
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
driver = new RemoteWebDriver(new URL("http://10.172.10.1:4444/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
现在从您的本地机器执行此测试脚本,以在RemoteWebDriver客户端和Selenium Standalone Server之间建立连接。服务器将启动 Chrome 浏览器。以下是在服务器运行的控制台中将看到的输出:

它表示正在创建一个新的具有期望能力的会话。一旦建立会话,会话 ID 将被打印到控制台。在任何时候,您都可以通过导航到 Selenium 服务器运行的机器的宿主或 IP 地址http://<hostnameOrIP>:4444/wd/hub来查看所有与Selenium Standalone Server建立的会话。
Selenium Standalone Server 默认情况下监听端口号 4444。我们可以通过传递-port参数来更改默认端口。
它将给出服务器当前正在处理的会话的整个列表。以下是其截图:

这是一个非常基础的门户,允许测试脚本开发者查看服务器创建的所有会话,并对它执行一些基本操作,例如终止会话、会话截图、将脚本加载到会话中,以及查看会话的所有期望能力。以下截图显示了当前会话的所有默认期望能力。
您可以通过悬停在能力链接上查看弹出窗口,如下面的截图所示:

这些是服务器为本次会话隐式设置的默认期望能力。现在我们已经成功地在我们的测试脚本(在一台机器上使用RemoteWebDriver客户端)和另一台机器上的 Selenium Standalone Server 之间建立了连接。远程运行测试脚本的原始图示如下:

使用 RemoteWebDriver 进行 Firefox 测试
使用 Firefox 浏览器执行我们的测试脚本与使用 Chrome 浏览器类似,只是在GeckoDriver的启动方式上有一些变化。
让我们通过将用于 Chrome 浏览器的测试脚本更改为以下脚本,使用"firefox"来实现这一点:
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("firefox");
caps.setCapability("marionette", true);
driver = new RemoteWebDriver(new URL("http://10.172.10.1:4444/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
在尝试执行此代码之前,请重启Selenium Standalone Server以使用GeckoDriver:
java -jar -Dwebdriver.gecko.driver=geckodriver selenium-server-standalone-3.12.0.jar
现在尝试执行前面的测试脚本,你应该看到 Firefox 浏览器被启动并执行你的测试命令。Selenium Standalone Server已启动GeckoDriver,与其建立了连接,并开始执行测试脚本命令。
使用 RemoteWebDriver 进行 Internet Explorer 测试
对于在 Internet Explorer 驱动程序上执行测试,步骤与我们使用 Chrome 和 Firefox 浏览器所做的是相似的。
让我们通过将用于 Chrome 或 Firefox 浏览器的测试脚本更改为以下脚本,使用"internet explorer"来查看这一点:
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("internet explorer");
driver = new RemoteWebDriver(new URL("http://127.0.0.1:4444/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
在尝试执行此代码之前,请重新启动Selenium Standalone Server以使用InternetExplorerDriver:
java -jar -Dwebdriver.ie.driver=InternetExplorerDriver.exe selenium-server-standalone-3.12.0.jar
现在尝试执行前面的测试脚本,你应该看到 Internet Explorer 浏览器被启动并执行你的测试命令。Selenium Standalone Server已启动InternetExplorerDriver,与其建立了连接,并开始执行测试脚本命令。
理解 JSON 线协议
在许多地方,我们提到 WebDriver 使用 JSON 线协议在客户端库和不同的驱动程序(即 Chrome Driver、IE Driver、Gecko Driver 等)实现之间进行通信。在本节中,我们将确切了解它是什么,以及客户端库应该实现哪些不同的 JSON API 来与驱动程序通信。
JavaScript 对象表示法(JSON)用于表示具有复杂数据结构的对象。它主要用于在 Web 服务器和客户端之间传输数据。它已成为各种基于 REST 的 Web 服务的行业标准,为 XML 提供了一种强大的替代方案。
一个示例 JSON 文件,保存为.json文件,将如下所示:
{
"firstname":"John",
"lastname":"Doe",
"address":{
"streetnumber":"678",
"street":"Victoria Street",
"city":"Richmond",
"state":"Victoria",
"country":"Australia"
} "phone":"+61470315430"
}
客户端可以将一个人的详细信息以先前的 JSON 格式发送到服务器,服务器可以解析它,然后创建一个用于其执行的人对象实例。稍后,服务器可以将响应以 JSON 格式发送回客户端,客户端可以使用这些数据创建一个类的对象。将对象数据转换为 JSON 格式以及将 JSON 格式数据转换为对象的过程分别称为序列化和反序列化,这在基于 REST 的 Web 服务中相当常见。
WebDriver 使用相同的方法在客户端库(语言绑定)和驱动程序之间进行通信,例如 Firefox Driver、IE Driver 和 Chrome Driver。同样,RemoteWebDriver客户端和Selenium Standalone Server使用 JSON 线协议相互通信。但是,所有这些驱动程序都在幕后使用它,隐藏了所有实现细节,使我们的生活变得更简单。以下是我们可以在网页上执行的各种操作的 API 列表:
/status /session /sessions /session/:sessionId /session/:sessionId/timeouts /session/:sessionId/timeouts/async_script /session/:sessionId/timeouts/implicit_wait /session/:sessionId/window_handle /session/:sessionId/window_handles /session/:sessionId/url /session/:sessionId/forward /session/:sessionId/back /session/:sessionId/refresh /session/:sessionId/execute /session/:sessionId/execute_async /session/:sessionId/screenshot /session/:sessionId/ime/available_engines /session/:sessionId/ime/active_engine
. . .
. . . /session/:sessionId/touch/flick /session/:sessionId/touch/flick /session/:sessionId/location /session/:sessionId/local_storage /session/:sessionId/local_storage/key/:key /session/:sessionId/local_storage/size /session/:sessionId/session_storage /session/:sessionId/session_storage/key/:key /session/:sessionId/session_storage/size /session/:sessionId/log /session/:sessionId/log/types /session/:sessionId/application_cache/status
完整的文档可在code.google.com/p/selenium/wiki/JsonWireProtocol找到。客户端库会将你的测试脚本命令转换为 JSON 格式,并发送请求到相应的 WebDriver API。WebDriver 将解析这些请求,并在网页上采取必要的行动。让我们以一个例子来看一下。假设你的测试脚本有如下代码:driver.get("http://www.google.com");。
客户端库将通过构建 JSON 有效载荷(JSON 文档)并将其发布到适当的 API 来将此转换为 JSON。在这种情况下,处理 driver.get(URL) 方法的 API 是 /session/:sessionId/url。
以下代码展示了在请求发送到驱动程序之前,客户端库层幕后发生的情况;请求被发送到运行在 10.172.10.1:4444 的 RemoteWebDriver 服务器:
HttpClient httpClient = new DefaultHttpClient();
HttpPost postMethod = new HttpPost("http://10.172.10.1:4444/wd/hub/session/"+sessionId+"/url");
JSONObject jo=new JSONObject();
jo.put("url","http://www.google.com");
StringEntity input = new StringEntity(jo.toString());
input.setContentEncoding("UTF-8");
input.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
postMethod.setEntity(input);
HttpResponse response = httpClient.execute(postMethod);
Selenium Standalone Server 将将该请求转发给驱动程序;驱动程序将在浏览器中加载的测试下,以先前格式执行到达的测试脚本命令。
以下图表显示了每个阶段的数据流:

上述图表显示了以下内容:
-
第一阶段是测试脚本和客户端库之间的通信。在它们之间流动的数据或命令是对驱动程序
get()方法的调用:driver.get("http://www.google.com");。 -
客户端库在接收到上述命令后,会将其转换为 JSON 格式,并与
Selenium Standalone Server进行通信。 -
接下来,
Selenium Standalone Server将 JSON 有效载荷请求转发给 Chrome Driver。 -
Chrome Driver 将与 Chrome 浏览器进行原生通信,然后浏览器将发送请求以加载所需的 URL。
摘要
在本章中,我们学习了 RemoteWebDriver 以及如何使用 Selenium Standalone Server 和 RemoteWebDriver 客户端在另一台机器上远程执行测试脚本。这使得 Selenium WebDriver 测试可以在具有不同浏览器和操作系统组合的远程机器上执行。我们还探讨了 JSON 通信协议以及客户端库如何在幕后发送和接收请求和响应。
在下一章中,我们将扩展 Selenium Standalone Server 和 RemoteWebDriver 的使用,以创建一个用于跨浏览器和分布式测试的 Selenium Grid。
问题
-
使用 Selenium,我们可以在远程机器上执行测试——对还是错?
-
用于在远程机器上运行测试的驱动程序类是哪一个?
-
解释期望能力。
-
在 Selenium 测试和 Selenium Standalone Server 之间使用的是哪种协议?
-
Selenium Standalone Server 使用的是默认端口是什么?
更多信息
你可以查看以下链接以获取有关本章涵盖主题的更多信息:
- Selenium WebDriver W3C 规范解释了 WebDriver 协议以及所有端点:
www.w3.org/TR/webdriver/
第八章:设置 Selenium Grid
现在我们已经了解了 RemoteWebDriver 是什么以及它是如何工作的,我们准备学习 Selenium Grid。在本章中,我们将涵盖以下主题:
-
为什么我们需要 Selenium Grid
-
什么是 Selenium Grid
-
我们如何使用 Selenium Grid
-
使用 Selenium Grid 的测试用例
-
配置 Selenium Grid
探索 Selenium Grid
让我们通过分析一个场景来尝试理解为什么我们需要 Selenium Grid。您有一个需要在以下浏览器-机器组合上测试的 Web 应用程序:
-
Windows 10 上的 Google Chrome
-
macOS 上的 Google Chrome
-
Windows 10 上的 Internet Explorer 11
-
Linux 上的 Firefox
我们可以简单地修改上一章中创建的测试脚本,并指向在每个这些组合(即 Windows 10、macOS 或 Linux)上运行的 Selenium Standalone Server,如下面的代码所示。
Windows 10:
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setPlatform(Platform.WIN10);
WebDriver driver = new RemoteWebDriver(new URL("http://<win_10_ip>:4444/wd/hub"), capabilities);
macOS:
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setPlatform(Platform.MAC);
WebDriver driver = new RemoteWebDriver(new URL("http://<mac_os_ip>:4444/wd/hub"), capabilities);
Linux:
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setPlatform(Platform.LINUX);
WebDriver driver = new RemoteWebDriver(new URL("http://<linux_ip>:4444/wd/hub"), capabilities);
在前面的代码中,您的测试脚本与托管目标平台和目标浏览器的机器紧密耦合。如果 Windows 10 主机发生变化,您应该重构您的测试脚本以处理这种情况。这不是设计测试的理想方式。您的测试脚本应该关注 Web 应用程序的功能,而不是执行这些测试脚本所使用的底层基础设施。应该有一个中心点来管理所有不同的环境。为了解决这个问题,我们使用了Selenium Grid。
Selenium Grid提供了一个跨浏览器测试环境,具有多个不同的平台(如 Windows、Mac 和 Linux)来执行测试。Selenium Grid 从中心点管理,称为中心节点。中心节点拥有所有不同测试平台的信息,称为节点(具有所需操作系统和浏览器版本并连接到中心节点的机器)。中心节点根据测试请求的能力将节点分配给执行测试,如下面的图所示:

在前面的图中,有一个中心节点,四个不同平台的节点,以及存放测试脚本的机器。测试脚本将与中心节点通信,并请求执行的目标平台。中心节点将具有目标平台的节点分配给测试脚本。节点执行测试脚本并将结果发送回中心节点,中心节点再将结果转发给测试脚本。这就是 Selenium Grid 的外观以及它在高层次上的工作方式。
现在我们已经从理论上了解了 Selenium Grid 的工作方式,让我们看看在其中作为中心节点和节点工作的内容。幸运的是,因为我们正在处理 Selenium Grid,我们可以使用我们在上一章中使用的相同的远程 WebDriver 服务器,同时作为 Selenium Grid 使用。如果您还记得,我们使用seleniumserver-standalone-3.12.0.jar作为 Selenium Standalone 服务器启动。我们可以使用相同的 JAR 文件在中心节点机器上以中心节点模式启动,并在节点机器上启动 JAR 文件的副本以节点模式运行。尝试在您的 JAR 文件上执行以下命令:
java –jar selenium-server-standalone-3.12.0.jar –help
以下输出显示了如何在网格环境中使用服务器:

您将看到两个选项:将其用作独立服务器,它充当远程 WebDriver,以及将其用于网格环境,这描述了 Selenium Grid。在本章中,我们将使用此 JAR 文件作为 Selenium Grid。
理解中心节点
中心节点是 Selenium Grid 的中心点。它记录了所有已连接并属于特定网格的可用节点。中心节点是一个以中心节点模式运行的 Selenium Standalone 服务器,默认情况下监听机器的4444端口。测试脚本将尝试连接到这个端口的中心节点,就像任何远程 WebDriver 一样。中心节点将负责重新路由测试脚本流量到适当的测试平台节点。让我们看看如何启动一个中心节点。导航到您 Selenium 服务器 JAR 文件的位置,并执行以下命令:
java -jar selenium-server-standalone-3.12.0.jar -role hub
执行此操作将在中心节点模式下启动您的服务器。默认情况下,服务器开始监听4444端口;然而,您可以将服务器启动在您选择的端口上。假设您想将服务器启动在端口1111上;可以按照以下方式完成:
java -jar selenium-server-standalone-3.12.0.jar -role hub –port 1111
以下截图显示了在端口1111上启动的网格中心节点的控制台输出:

所有测试脚本都应该连接到这个端口上的中心节点。现在启动您的浏览器,连接到在端口1111上托管您的中心节点的机器。在这里,托管我的中心节点的机器的 IP 地址是 192.168.0.101。
您在浏览器上应该看到的是以下截图所示:

它显示了正在用作网格中心节点的服务器的版本。现在点击控制台链接以导航到网格控制台:

如您所见,页面讨论了许多配置参数。我们将在配置 Selenium Grid部分讨论这些配置参数。所以,您现在已经学会了如何在端口上启动网格并监听连接。
理解节点
由于我们的中心正在运行,现在是时候启动一个节点并将其连接到中心了。在这个例子中,我们将配置一台安装了 Chrome 的 macOS 机器。因此,如果任何测试脚本请求中心 macOS 平台和 Chrome 浏览器,中心将选择这个节点。让我们看看我们如何启动节点。启动节点并注册到中心的命令如下:
java –jar selenium-server-standalone-3.12.0.jar –role node –hub http://192.168.0.101:1111/grid/register
这将在节点模式下启动 Selenium 服务器,并将此节点注册到已启动的中心:

如果你回到浏览器上的 Grid 控制台,你会看到以下内容:

前面的截图显示了 http://192.168.0.101:16784 的节点 URL,在这种情况下,它运行在 Mac 平台上。默认情况下,每个节点列出的浏览器数量为 11:Firefox 有 5 个,Chrome 有 5 个,IE 有 1 个。这可以通过指定browser选项来覆盖,我们将在配置 Selenium Grid部分看到。
同样,在 Windows 上启动另一个节点,并使用启动 macOS 节点相同的命令将其注册到中心。
修改现有的测试脚本以使用 Selenium Grid
到目前为止,我们已经看到了在本地机器或 Selenium Standalone 服务器上运行的测试脚本。在 Selenium Grid 上执行测试脚本与在 Remote WebDriver 上执行测试非常相似,只是你还需要提及 Grid 的平台详细信息。
让我们看看一个使用 Remote WebDriver 服务器的测试脚本:
public class SearchTest {
WebDriver driver;
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setPlatform(Platform.MAC);
driver = new RemoteWebDriver(new URL("http://192.168.0.101:1111/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
现在,尝试执行前面的测试脚本,并观察中心节点和节点的日志输出。中心的输出日志如下:

在中心端发生的步骤顺序如下:
-
中心收到创建新会话的请求,
platform=MAC, browserName=chrome。 -
它验证可用的节点,以匹配
capabilities请求。 -
如果可用,它将与节点主机创建一个新的会话;如果不可以,它将拒绝测试脚本中的请求,表示所需的特性与任何已注册的节点都不匹配。
-
如果在前一步中与节点主机创建了一个会话,则创建一个新的测试槽会话并将测试脚本交给节点。同样,你应在中心的控制台日志中看到的输出如下:

在节点上执行的步骤顺序如下:
-
节点主机使用请求的期望特性创建一个新的会话。这将启动浏览器。
-
它在启动的浏览器上执行测试脚本的步骤。
-
它结束会话并将结果转发到中心,中心再将结果发送到测试脚本。
请求非注册特性
当测试脚本请求一个未在中心节点注册的能力时,中心节点将拒绝测试脚本的请求。让我们修改前面的测试脚本,请求使用 Opera 浏览器而不是 Chrome。测试脚本应如下所示:
@BeforeMethod
public void setup() throws MalformedURLException {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("opera");
caps.setPlatform(Platform.MAC);
driver = new RemoteWebDriver(new URL("http://192.168.0.101:1111/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
中心节点检查是否有任何节点与所需的特性匹配。如果没有找到(如本例所示),它将通过抛出CapabilityNotPresentOnTheGridException异常来拒绝测试脚本的请求,如下面的截图所示:

如果节点忙碌,排队请求
默认情况下,您可以向任何节点发送五个测试脚本请求。尽管可以更改此配置,但让我们看看当节点已经处理了五个请求,并且您通过中心节点对该节点发起另一个请求时会发生什么。中心节点将一直轮询该节点,直到从节点获得一个空闲的测试槽位。测试脚本在这段时间内将被等待。中心节点表示没有空闲槽位来建立与同一节点的第六个会话。同时,在节点主机上,节点试图为五个请求创建会话,并开始执行测试脚本。
在创建会话后,将启动五个 Chrome 窗口,并在其上执行测试脚本。在处理前五个测试脚本请求后,中心节点将与节点建立等待的第六个会话,并将第六个请求提供服务。
处理具有匹配能力的两个节点
Selenium Grid 提供了许多配置选项来控制在执行测试脚本时节点和中心节点的行为。我们将在下面讨论它们。
配置 Selenium Grid
Selenium Grid 提供了许多配置选项来控制在执行测试脚本时节点和中心节点的行为。我们将在下面讨论它们。
指定节点配置参数
在本节中,我们将讨论节点的配置参数。
设置节点支持的浏览器
如我们之前所见,当我们使用一个中心节点注册一个节点时,默认情况下,该节点会显示支持五个 Firefox 浏览器的实例、五个 Chrome 浏览器的实例和一个 Internet Explorer 的实例,无论该节点实际上是否支持它们。但为了使用您选择的浏览器注册您的节点,Selenium Grid 提供了一个浏览器选项,我们可以通过它来实现这一点。假设我们希望我们的节点注册支持 Firefox、Chrome 和 Safari;我们可以使用以下命令来完成:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -browser browserName=firefox -browser browserName=chrome -browser browserName=safari
网格控制台看起来如下所示:

设置节点超时
此参数是在将节点注册到中心节点时设置的。提供给这些参数的值是中心节点在终止节点上的测试脚本执行之前可以等待的时间(以秒为单位),如果测试脚本在节点上不执行任何类型的活动。
配置节点超时的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -nodeTimeout 300
这里,我们已注册了一个节点,其节点超时值为 300 秒。因此,如果节点在 300 秒内没有任何活动,中心节点将终止测试脚本。
设置浏览器实例的限制
我们已经看到,默认情况下,有 11 个浏览器实例被注册到节点上。我们已经看到了如何注册我们自己的浏览器。在本节中,我们将看到我们可以在节点上允许多少个这样的浏览器实例。为了进行控制,Selenium Grid 提供了一个配置参数,称为maxInstances,通过它可以指定我们希望节点提供的特定浏览器实例的数量。执行此操作的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -browser "browserName=firefox,max Instances=3" -browser "browserName=chrome,maxInstances=3" -browser "browserName=safari,maxInstances=1"
这里,我们正在注册一个提供三个 Firefox 实例、三个 Chrome 实例和一个 Safari 实例的节点。
自动重新注册节点
如果节点注册到中心节点后,中心节点崩溃或重启,所有已注册节点的信息都将丢失。手动回到每个节点并重新注册它们将变得繁琐。如果我们还没有意识到中心节点已重启,影响将更加严重,因为所有测试脚本都会因此失败。因此,为了处理这种情况,Selenium Grid 提供了一个配置参数给节点,通过它可以指定节点在指定时间后自动重新注册到中心节点。如果没有指定,重新注册的默认时间为五秒。这样,我们真的不必担心;即使中心节点崩溃或重启,我们的节点也会每五秒尝试重新注册。
如果您想修改这个时间间隔,要处理的配置参数是registerCycle。指定命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -registerCycle 10000
启动时在节点日志控制台看到的输出如下:
17:47:01.231 INFO - starting auto register thread. Will try to register every 10000 ms.
17:47:01.232 INFO - Registering the node to hub :http://192.168.0.1:1111/grid/register
节点将尝试每 1,000 毫秒注册到中心节点。
设置节点健康检查时间
使用这个配置参数,我们可以指定中心节点可以多频繁地轮询节点以检查其可用性。用于实现此目的的参数是nodePolling。通过在节点级别指定此参数到中心节点,每个节点可以指定自己的健康检查频率。配置节点的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -nodePolling 10
现在,中心节点将每 10 秒轮询此节点一次,以检查其可用性。
注销不可用的节点
虽然nodePolling配置会使中心节点频繁轮询节点,但unregisterIfStillDownAfter配置将允许中心节点在轮询没有产生预期结果时注销节点。假设一个节点已关闭,中心节点尝试轮询该节点并无法连接到它。在这种情况下,中心节点将轮询多长时间以确定节点的可用性由unregisterIfStillDownAfter参数决定。超过这个时间,中心节点将注销节点。
执行该操作的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register -nodePolling 5 -unregistIfStillDownAfter 20000
在这里,中心节点将每五秒轮询一次节点;如果节点宕机,轮询将持续 20 秒,即中心节点将轮询四次,然后从网格中注销该节点。
设置浏览器超时
此配置是为了让节点知道在浏览器似乎挂起之前它应该等待多长时间来结束测试脚本会话。在此时间之后,节点将终止浏览器会话并开始下一个等待的测试脚本。此配置参数为browserTimeout。指定该参数的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role node -hub http://192.168.0.1:1111/grid/register –browserTimeout 60
因此,这些是一些你可以在节点端指定以更好地控制 Selenium Grid 环境的配置参数。
中心节点配置参数
本节讨论了在中心节点上的一些配置参数。
等待匹配所需能力
如我们之前所见,当测试脚本请求具有所需能力的测试平台时,如果中心节点找不到具有所需能力的合适节点,它将拒绝请求。
修改throwOnCapabilityNotPresent参数的值可以改变这种行为。默认情况下,它设置为true,这意味着如果中心节点找不到具有该能力的合适节点,它将拒绝请求。但将此参数设置为false将排队请求,中心节点将等待直到网格中添加了具有该能力的节点。必须调用的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role hub -port 1111 -throwOnCapabilityNotPresent false
现在,中心节点不会拒绝请求,而是将请求放入队列中,等待直到请求的平台可用。
定制的能力匹配器
默认情况下,中心节点将使用org.openqa.grid.internal.utils.DefaultCapabilityMatcher类来匹配请求的节点。如果你不喜欢DefaultCapabilityMatcher类的实现逻辑,你可以扩展该类,实现自己的CapabilityMatcher类,并在其中提供自己的逻辑。
一旦开发完成,你可以要求中心节点使用该类来匹配节点与能力,使用一个名为capabilityMatcher的配置参数。实现此功能的命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role hub -port 1111 -capabilityMatcher com.yourcomp.CustomCapabilityMatcher
中心节点将使用你在CustomCapabilityMatcher类中定义的逻辑来识别要分配给测试脚本请求的节点。
新会话的等待超时
当一个能力匹配的节点正忙于执行其他测试脚本时,最新的测试脚本将等待节点可用。默认情况下,没有等待超时;也就是说,测试脚本将无限期地等待节点可用。为了改变这种行为,并让测试脚本在有限时间内未获得节点时抛出异常,Selenium Grid 打开一个配置,使测试脚本能够这样做。控制该行为的配置参数是newSessionWaitTimeout。该命令如下:
java -jar selenium-server-standalone-3.12.0.jar -role hub -port 1111 -newSessionWaitTimeout 120000
在这里,测试脚本将在抛出异常说它无法获取执行自身的节点之前等待两分钟。
指定配置的不同方式
有两种方式可以将配置参数指定给 Selenium Grid 的中心节点和节点。第一种是我们一直看到的方式;即通过命令行指定配置参数。第二种方式是通过提供一个包含所有这些配置参数的 JSON 文件来实现。
节点配置文件(例如,nodeConfig.json)——一个典型的包含所有配置参数的 JSON 文件——看起来类似于以下内容:
{
"class": "org.openqa.grid.common.RegistrationRequest",
"capabilities": [
{
"seleniumProtocol": "WebDriver",
"browserName": "internet explorer",
"version": "10",
"maxInstances": 1,
"platform" : "WINDOWS"
}
],
"configuration": {
"port": 5555,
"register": true,
"host": "192.168.1.102",
"proxy": "org.openqa.grid.selenium.proxy.
DefaultRemoteProxy",
"maxSession": 2,
"hubHost": "192.168.1.100",
"role": "webdriver",
"registerCycle": 5000,
"hub": "http://192.168.1.101:111/grid/register",
"hubPort": 1111,
"remoteHost": "http://192.168.1.102:5555"
}
}
一旦这些文件配置完成,可以使用以下命令将它们提供给节点和中心节点:
java -jar selenium-server-standalone-3.12.0.jar -role node -nodeConfig nodeconfig.json
这样,你可以使用 JSON 文件指定你的中心节点和节点的配置。
使用基于云的网格进行跨浏览器测试
要设置用于跨浏览器测试的 Selenium Grid,你需要设置具有不同浏览器和操作系统的物理或虚拟机。这需要投资必要的硬件、软件和支持来运行测试实验室。你还需要投入努力,确保该基础设施使用最新版本和补丁更新。并非每个人都能承担这些成本和努力。
而不是投资和建立跨浏览器的测试实验室,你可以轻松地将虚拟测试实验室外包给第三方云服务提供商进行跨浏览器测试。Sauce Labs 和 BrowserStack 是领先的基于云的跨浏览器测试云服务提供商。这两者都支持超过 400 种不同的浏览器和操作系统配置,包括移动和平板设备,并支持在它们的云中运行 Selenium WebDriver 测试。
在这里,我们将设置并运行 Sauce Labs 云中的测试。如果你想要使用 BrowserStack 运行测试,步骤是类似的。
让我们使用 Sauce Labs 设置和运行一个测试。你需要一个免费的 Sauce Labs 账户才能开始。在 Sauce Labs 上注册一个免费账户saucelabs.com/,并获取用户名和访问密钥。Sauce Labs 提供所有必要的硬件和软件基础设施,以便你在云中运行测试。你可以在登录后从“我的账户”页面获取访问密钥:

让我们在 Sauce Labs 云上创建一个新的测试来执行。我们需要将 Sauce 用户名和访问密钥添加到测试中,并将网格地址更改为 Sauce Labs 网格地址,而不是本地 Selenium 网格,如下面的代码示例所示:
public class BmiCalculatorTest {
WebDriver driver;
@BeforeMethod
public void setUp() throws Exception {
String SAUCE_USER = "upgundecha";
String SAUCE_KEY = "5768f2a9-33be-4ebd-9a5f-3826d7c38ec9";
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platform", "OS X 10.9");
caps.setCapability("browserName", "Safari");
caps.setCapability("name", "BMI Calculator Test");
driver = new RemoteWebDriver(
new URL(MessageFormat.format("http://{0}:{1}@ondemand.saucelabs.com:80/wd/hub'",
SAUCE_USER, SAUCE_KEY)), caps);
driver.get("http://bit.ly/1zdNrFZ");
}
@Test
public void testBmiCalc() {
WebElement height = driver.findElement(By.name("heightCMS"));
height.sendKeys("181");
WebElement weight = driver.findElement(By.name("weightKg"));
weight.sendKeys("80");
WebElement calculateButton = driver.findElement(By.id("Calculate"));
calculateButton.click();
WebElement bmi = driver.findElement(By.name("bmi"));
assertEquals(bmi.getAttribute("value"), "24.4");
WebElement bmi_category = driver.findElement(By.name("bmi_category"));
assertEquals(bmi_category.getAttribute("value"), "Normal");
}
@AfterMethod
public void tearDown() throws Exception {
driver.quit();
}
}
当你执行测试时,它将连接到 Sauce Lab 的中心节点并请求所需的操作系统和浏览器配置。Sauce Labs 的云管理软件会自动分配一个虚拟机,以便在我们的测试中运行在给定的配置上。我们可以在以下截图所示的仪表板上监控此运行:

我们可以进一步深入会话,查看运行期间确切发生了什么。它提供了 Selenium 命令、截图、日志以及多个标签页上的执行视频的详细信息,如下面的截图所示:

Selenium 详细信息窗口
您还可以通过使用 Sauce Connect 工具测试安全托管在内部服务器上的应用程序。Sauce Connect 在您的机器和 Sauce 云之间创建一个安全隧道。
摘要
在本章中,我们学习了 Selenium Grid 的相关知识,包括如何使 hub 和 node 工作,更重要的是,如何配置您的 Selenium Grid 以更好地控制环境和基础设施。Selenium Grid 通过覆盖操作系统和浏览器的组合,将使应用程序能够进行跨浏览器测试。我们还看到了如何使用云服务,如 Sauce Labs,在远程云环境中执行测试。
在下一章中,我们将学习如何使用页面对象模式创建可重用和模块化的测试。
问题
-
哪个参数可以用来指定节点可以支持多少浏览器实例?
-
解释如何使用 Selenium Grid 来支持跨浏览器测试。
-
使用 RemoteWebDriver 运行 Selenium Grid 上的测试时,您需要指定哪个 URL?
-
Selenium Grid Hub 是否充当负载均衡器?—— 对或错?
更多信息
您可以通过以下链接获取有关本章涵盖主题的更多信息:
- 在
www.seleniumhq.org/docs/07_selenium_grid.jsp上了解更多关于 Selenium Grid 的信息
第九章:PageObject 模式
到目前为止,我们已经看到了 WebDriver 的各种 API,并学习了如何使用它们来执行我们在测试的 Web 应用程序上的各种操作。我们创建了许多使用这些 API 的测试,并且它们会持续执行以验证应用程序。然而,随着你的测试套件的增长,你的测试和代码的复杂性也会增加。这成为了一个挑战,特别是关于你的脚本和代码的可维护性。你需要设计一个可维护的、模块化的和可重用的测试代码,这样随着你添加更多的测试覆盖率,它也能扩展。在本章中,我们将探讨 PageObject 模式来构建一个高度可维护的测试套件。我们将涵盖以下主题:
-
PageObject 模式设计是什么?
-
设计 PageObjects 的良好实践
-
PageObject 模式的扩展
-
一个端到端示例
一个写得不错的测试脚本只要目标 Web 应用程序不改变就能正常工作。但是一旦你的 Web 应用程序中的一个或多个页面发生变化,作为一个测试脚本开发者,你不应该处于不得不在数百个不同地方重构你的测试脚本的位置。让我们通过一个例子更好地理解这个陈述。我们将通过在一个 WordPress 博客上工作来尝试通过这一章。在我们开始之前,我希望你创建一个 WordPress 博客(wordpress.com/about)或者使用你现有的其中一个。
为我们的 WordPress 博客创建测试用例
在这里,我们使用一个 WordPress 博客:demo-blog.seleniumacademy.com/wp/。在我们开始讨论 PageObject 模式之前,让我们为它创建三个测试用例。
测试用例 1 – 向我们的 WordPress 博客添加新帖子
以下测试脚本将登录我们的 WordPress 博客的管理门户并添加一个新的博客帖子:
@Test
public void testAddNewPost() {
WebElement email = driver.findElement(By.id("user_login"));
WebElement pwd = driver.findElement(By.id("user_pass"));
WebElement submit = driver.findElement(By.id("wp-submit"));
email.sendKeys("admin");
pwd.sendKeys("$$SUU3$$N#");
submit.click();
// Go to AllPosts page
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
// Add New Post
WebElement addNewPost = driver.findElement(By.linkText("Add New"));
addNewPost.click();
// Add New Post's Content
WebElement title = driver.findElement(By.id("title"));
title.click();
title.sendKeys("My First Post");
driver.switchTo().frame("content_ifr");
WebElement postBody = driver.findElement(By.id("tinymce"));
postBody.sendKeys("This is description");
driver.switchTo().defaultContent();
// Publish the Post
WebElement publish = driver.findElement(By.id("publish"));
publish.click();
}
以下是在执行前一个代码时遵循的步骤序列:
-
登录 WordPress
管理门户。 -
前往所有帖子页面。
-
点击添加新帖子按钮。
-
通过提供标题和描述添加新帖子。
-
发布帖子。
测试用例 2 – 从我们的 WordPress 博客中删除帖子
以下测试脚本将登录我们的 WordPress 博客并删除一个现有帖子:
@Test
public void testDeleteAPost() {
WebElement email = driver.findElement(By.id("user_login"));
WebElement pwd = driver.findElement(By.id("user_pass"));
WebElement submit = driver.findElement(By.id("wp-submit"));
email.sendKeys("admin");
pwd.sendKeys("$$SUU3$$N#");
submit.click();
// Go to AllPosts page
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
// Click on the post to be deleted
WebElement post = driver.findElement(By.linkText("My First Post"));
post.click();
// Delete Post
WebElement publish = driver.findElement(By.linkText("Move to Trash"));
publish.click();
}
以下是在删除帖子之前,前一个测试脚本所遵循的步骤序列:
-
登录 WordPress
管理门户。 -
前往所有帖子页面。
-
点击要删除的帖子。
-
删除帖子。
测试用例 3 – 在我们的 WordPress 博客上统计帖子数量
以下测试脚本将统计我们 WordPress 博客上当前可用的所有帖子:
@Test
public void testPostCount() {
WebElement email = driver.findElement(By.id("user_login"));
WebElement pwd = driver.findElement(By.id("user_pass"));
WebElement submit = driver.findElement(By.id("wp-submit"));
email.sendKeys("admin");
pwd.sendKeys("$$SUU3$$N#");
submit.click();
// Count the number of posts
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
WebElement postsContainer = driver.findElement(By.id("the-list"));
List postsList = postsContainer.findElements(By.
tagName("tr"));
Assert.assertEquals(postsList.size(), 1);
}
以下是在我们的博客上统计当前可用的帖子数量的前一个测试脚本所遵循的步骤序列:
-
登录
管理门户。 -
前往所有帖子页面。
-
统计可用的帖子数量。
在前面的三个测试脚本中,我们登录 WordPress 并执行操作,例如创建帖子、删除帖子或计算现有帖子的数量。想象一下,登录页面上的一个元素的 ID 已经更改,我们必须在所有三个不同的测试用例中修改它;或者,如果所有帖子页面已经更改,我们必须编辑所有三个测试用例以反映新的更改。如果你有 50 个测试用例,每次目标应用程序有更改时都要更改每个测试用例是非常困难的。为此,你需要设计一个测试框架,将你需要在测试用例中进行的更改降到最低。PageObject 模式是一种可以用来设计你的测试框架的设计模式。
PageObject 模式是什么?
每当我们为测试 Web 应用程序设计自动化框架时,我们必须接受这样一个事实:目标应用程序及其元素肯定会发生变化。一个高效的框架是那种需要最小重构来适应目标应用程序中新的变化的框架。让我们尝试将前面的测试场景构建到 PageObject 设计模式模型中。让我们首先开始构建登录页面的 PageObject。它应该看起来像以下这样:
public class AdminLoginPage {
WebDriver driver;
WebElement email;
WebElement password;
WebElement submit;
public AdminLoginPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin");
email = driver.findElement(By.id("user_login"));
password = driver.findElement(By.id("user_pass"));
submit = driver.findElement(By.id("wp-submit"));
}
public void login() {
email.sendKeys("admin");
password.sendKeys("$$SUU3$$N#");
submit.click();
}
}
因此,所有属于登录过程的元素都列在AdminLoginPage类中,并且有一个名为login()的方法,用于管理这些元素的填充并提交登录表单。因此,这个AdminLoginPage类将代表 WordPress 的管理员登录页面,将页面上的所有元素作为成员变量,以及可以在页面上执行的所有操作作为方法。现在,让我们看看我们需要如何重构测试用例以使用我们新创建的 PageObject。让我们考虑以下testAddNewPost测试用例:
@Test
public void testAddNewPost() {
AdminLoginPage admLoginPage = new AdminLoginPage(driver);
admLoginPage.login();
// Go to New Posts page
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
WebElement addNewPost = driver.findElement(By.linkText("Add New"));
addNewPost.click();
// Add New Post
driver.switchTo().frame("content_ifr");
WebElement postBody = driver.findElement(By.id("tinymce"));
postBody.sendKeys("This is description");
driver.switchTo().defaultContent();
WebElement title = driver.findElement(By.id("title"));
title.click();
title.sendKeys("My First Post");
WebElement publish = driver.findElement(By.id("publish"));
publish.click();
}
在前面的测试用例中,登录管理页面的全部代码仅包含两行:
AdminLoginPage admLoginPage = new AdminLoginPage(driver);
admLoginPage.login();
导航到管理员登录页面,识别元素,为元素提供值,以及提交表单——所有这些操作都由 PageObject 处理。因此,从现在开始,测试用例不需要为管理员页面上的任何更改进行重构。你只需更改 PageObject,所有使用此 PageObject 的测试用例将开始使用这些新更改,甚至不知道它们已经发生。
现在你已经看到了 PageObject 的样子,Selenium 库提供了更多方便的方式来实现你的 PageObjects。让我们看看它们。
使用@FindBy注解
PageObject 中的一个元素用@FindBy注解标记。它用于指示 WebDriver 在页面上定位该元素。它接受定位机制(即通过Id、Name或Class Name)和该定位机制的元素值作为输入。
使用@FindBy注解有两种方式:
使用方法 1 如下所示:
@FindBy(id="user_login")
WebElement userId;
使用方法 2 如下所示:
@FindBy(how=How.ID, using="user_login")
WebElement userId;
前两种用法将 WebDriver 指向使用定位机制 ID 并具有 user_login 值的元素,并将该元素分配给 userId WebElement。在用法 2 中,我们使用了 How 枚举。这个枚举支持我们 By 类支持的所有不同的定位机制。How 枚举中支持枚举常量如下:
-
类名
-
CSS
-
ID
-
ID_OR_NAME
-
链接文本
-
名称
-
部分链接文本
-
标签名
-
XPATH
使用 @FindBy 注解,我们将看到我们的 AdminLoginPage 类是如何变化的:
public class AdminLoginPage {
WebDriver driver;
@FindBy(id="user_login")
WebElement email;
@FindBy(id="user_pass")
WebElement password;
@FindBy(id="wp-submit")
WebElement submit;
public AdminLoginPage(WebDriver driver){
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin");
}
public void login(){
email.sendKeys("pageobjectpattern@gmail.com");
password.sendKeys("webdriver123");
submit.click();
}
}
当测试用例在构造函数中实例化前面的类时,我们使用构造函数中指定的以下代码导航到 WordPress 登录页面:
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin");
一旦将驱动状态设置为该页面,所有使用 FindBy 声明的元素,即 email、password 和 submit,都将通过 FindBy 注解中指定的定位机制由 WebDriver 初始化。
理解 PageFactory
WebDriver 库提供的另一个重要类,用于支持 PageObject 模式的是 PageFactory 类。一旦 PageObject 类使用 FindBy 注解声明了元素,就可以使用 PageFactory 类实例化该 PageObject 类及其元素。这个类支持一个名为 initElements 的静态方法。此方法的 API 语法如下:
initElements(WebDriver driver, java.lang.Class PageObjectClass)
现在,让我们看看如何在我们的测试用例中使用它来创建 AdminLoginPage:
public class TestAddNewPostUsingPageObjects { public static void main(String... args){
AdminLoginPage loginPage= PageFactory
.initElements(driver, AdminLoginPage.class);
loginPage.login();
PageFactory 类实例化了 AdminLoginPage 类,并给它提供了驱动实例。AdminLoginPage 页面对象将驱动实例导航到 URL (demo-blog.seleniumacademy.com/wp/wp-admin,在这种情况下),然后填充所有带有 FindBy 注解的元素。
PageObjects 设计的良好实践
因此,现在你已经看到了 PageObject 的简单实现,是时候考虑一些设计 PageObjects 的良好实践了。
将网页视为一个服务提供商
从高层次来看,当你查看一个网络应用程序中的页面时,你会发现它是由各种用户服务聚合而成的。例如,如果你查看我们 WordPress 管理控制台中的所有帖子页面,其中有许多部分:

在前面的截图中的 All Posts 页面,用户可以执行以下五个活动:
-
添加一篇新帖子。
-
编辑所选帖子。
-
删除所选帖子。
-
通过类别过滤帖子。
-
在所有帖子中搜索文本。
上述活动是所有帖子页面提供给其用户的服务。因此,你的 PageObject 也应该为测试用例提供这些服务,这是 PageObject 的用户。所有帖子 PageObject 的代码应如下所示:
public class AllPostsPage {
WebDriver driver;
@FindBy(id = "the-list")
WebElement postsContainer;
@FindBy(id = "post-search-input")
WebElement searchPosts;
@FindBy(id = "cat")
WebElement viewByCategories;
public AllPostsPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
}
public void createANewPost(String title, String description) {
}
public void editAPost(String title) {
}
public void deleteAPost(String postTitle) {
}
public void filterPostsByCategory(String category) {
}
public void searchInPosts(String searchText) {
}
}
现在,我们已经将页面中识别的服务映射到我们的 PageObject 中的方法。当测试用例想要执行一个服务时,它将得到 PageObject 的帮助来完成这个任务。
总是寻找隐含的服务
页面上的某些服务可以非常清楚地在其上识别。还有一些服务在页面上不可见,但却是隐含的。例如,在所有帖子页面上,我们仅通过查看页面就识别了五个服务。但是,假设您的测试用例想要知道现有帖子的数量;这个信息在所有帖子页面上是可用的,我们必须确保您的 PageObject 提供这个隐含服务。现在您扩展 All Posts 页面的 PageObject 以包含这个隐含服务,如下所示:
public class AllPostsPage {
WebDriver driver;
@FindBy(id = "the-list")
WebElement postsContainer;
@FindBy(id = "post-search-input")
WebElement searchPosts;
@FindBy(id = "cat")
WebElement viewByCategories;
public AllPostsPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
}
public void createANewPost(String title, String description) {
}
public void editAPost(String title) {
}
public void deleteAPost(String postTitle) {
}
public void filterPostsByCategory(String category) {
}
public void searchInPosts(String searchText) {
}
public int getAllPostsCount(){
}
}
现在您的测试用例可以使用相同的 PageObject 来使用与所有帖子页面相关的隐含服务。
在 PageObject 中使用 PageObject
将会有很多需要在使用 PageObject 内部使用 PageObject 的情况。让我们通过所有帖子页面上的一个场景来分析这一点。当您点击“新增”来添加新帖子时,浏览器实际上会导航到另一个页面。因此,您必须创建两个 PageObject,一个用于所有帖子页面,另一个用于新增页面。将 PageObject 设计成模拟目标应用程序的确切行为将使事情非常清晰且相互独立。您可能可以通过几种不同的方式导航到新增页面。为新增页面创建一个自己的 PageObject 并在需要的地方使用它将使您的测试框架遵循良好的面向对象原则,并使测试框架的维护变得容易。让我们看看在 PageObject 内部使用 PageObject 将是什么样子。
新增帖子 PageObject
AddNewPost PageObject 添加新帖子,如下面的代码所示:
public class AddNewPostPage {
WebDriver driver;
@FindBy(id = "content_ifr")
WebElement newPostContentFrame;
@FindBy(id = "tinymce")
WebElement newPostContentBody;
@FindBy(id = "title")
WebElement newPostTitle;
@FindBy(id = "publish")
WebElement newPostPublish;
public AddNewPostPage(WebDriver driver) {
this.driver = driver;
}
public void addNewPost(String title, String descContent) {
newPostTitle.click();
newPostTitle.sendKeys(title);
driver.switchTo().frame(newPostContentFrame);
newPostContentBody.sendKeys(descContent);
driver.switchTo().defaultContent();
newPostPublish.click();
}
}
AllPostsPage PageObject
AllPostsPage PageObject 处理所有帖子页面,如下面的代码所示:
public class AllPostsPage {
WebDriver driver;
@FindBy(id = "the-list")
WebElement postsContainer;
@FindBy(id = "post-search-input")
WebElement searchPosts;
@FindBy(id = "cat")
WebElement viewByCategories;
@FindBy(linkText = "Add New")
WebElement addNewPost;
public AllPostsPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
}
public void createANewPost(String title, String description) {
addNewPost.click();
AddNewPostPage newPost = PageFactory.initElements(driver,
AddNewPostPage.class);
newPost.addNewPost(title, description);
}
public void editAPost(String title) {
}
public void deleteAPost(String title) {
}
public void filterPostsByCategory(String category) {
}
public void searchInPosts(String searchText) {
}
public int getAllPostsCount() {
}
}
现在,正如您在AllPostsPage PageObject 中看到的,我们在createNewPost()方法中实例化了AddNewPage PageObject。因此,我们正在使用一个 PageObject 来使用另一个 PageObject,并尽可能使行为接近目标应用程序。
将 PageObject 中的方法视为服务而不是用户操作
有时候可能会对哪些方法构成 PageObject 感到困惑。我们之前看到每个 PageObject 都应该包含用户服务作为其方法。但很常见的是,我们在几个测试框架中看到一些 PageObject 的实现,它们将用户操作作为方法。那么“用户服务”和“用户操作”之间的区别是什么?正如我们之前看到的,WordPress 管理控制台上的用户服务的一些例子如下:
-
创建一个新的帖子
-
删除一个帖子
-
编辑一个帖子
-
在帖子中进行搜索
-
过滤帖子
-
计算所有现有帖子
所有的前述服务都讨论了目标应用程序的各种功能。现在,让我们看看一些User Actions的示例:
-
鼠标点击
-
在文本框中输入文本
-
导航到页面
-
点击复选框
-
从下拉列表中选择一个选项
上述列表展示了页面上的某些User Actions示例。它们在许多应用程序中都是通用的。您的 PageObject 不是为了向测试用例提供User Actions,而是提供用户服务。因此,您的 PageObject 中的每个方法都应该映射到目标页面提供给用户的某个服务。为了完成一个用户服务,PageObject 方法应该包含许多User Actions。
几个用户操作组合在一起以完成一个用户服务。
以下是一个示例,说明如果您的 PageObject 使用用户操作而不是用户服务来提供方法,它将看起来像什么;让我们看看AddNewPage PageObject 将是什么样子:
public class AddNewPost {
WebDriver driver;
@FindBy(id = "content_ifr")
WebElement newPostContentFrame;
@FindBy(id = "tinymce")
WebElement newPostContentBody;
@FindBy(id = "title")
WebElement newPostTitle;
@FindBy(id = "publish")
WebElement newPostPublish;
public AddNewPost(WebDriver driver) {
this.driver = driver;
System.out.println(driver.getCurrentUrl());
}
public void typeTextinTitle(String title) {
newPostTitle.sendKeys(title);
}
public void clickPublishButton() {
newPostPublish.click();
}
public void typeTextinContent(String descContent) {
driver.switchTo().frame(newPostContentFrame);
newPostContentBody.sendKeys(descContent);
}
}
因此,在AddNewPage PageObject 的代码中,我们有三种不同的方法来完成三种不同的用户操作。调用对象现在应该调用以下方法,而不是仅仅调用addNewPage(String title, String description)方法:
typeTextinTitle(String title)
typeTextinContent(String description)
clickPublishButton()
前述用户操作是完成添加新帖子用户服务的三个不同的用户操作。这些方法的调用者还应该记住这些用户操作需要调用的顺序;也就是说,clickPublishButton()方法应该始终放在最后。这给测试用例和其他试图向系统中添加新帖子的 PageObjects 引入了不必要的复杂性。因此,用户服务将隐藏大多数实现细节,从而降低维护测试用例的成本。
动态识别一些 Web 元素
在所有 PageObjects 中,我们使用@FindBy注解初始化了在对象实例化期间将要使用的元素。始终识别完成用户服务所需的所有页面元素并将它们分配给 PageObject 中的成员变量是很好的。然而,并不总是可能做到这一点。例如,如果您想编辑所有帖子页面上的特定帖子,在 PageObject 初始化期间,将页面上的每个帖子映射到 PageObject 的成员变量并不是强制性的。当您有大量帖子时,PageObject 初始化将花费不必要的时间将帖子映射到成员变量,即使我们并不使用它们。此外,我们甚至不知道需要映射多少成员变量才能映射所有帖子。所有帖子页面的 HTML 如下所示:

有一个通过the-list识别的根元素,其中包含 WordPress 博客中的所有帖子。在这个元素内部,我们可以看到有 Post1、Post2 和 Post3。因此,为所有三个帖子初始化 PageObject 并不是一个最佳解决方案。您可以使用映射到根元素的成员变量初始化 PageObject,并且每当需要时,目标帖子将从其中检索。
让我们看看以下AllPostsPage PageObject,它以这种方式实现了其editPost()方法:
public void editAPost(String presentTitle,
String newTitle, String description){
List<WebElement> allPosts
= postsContainer.findElements(By.className("rowtitle"));
for(WebElement ele : allPosts){
if(ele.getText().equals(presentTitle)){
Actions builder = new Actions(driver);
builder.moveToElement(ele);
builder.click(driver.findElement(
By.cssSelector(".edit>a")));
// Generate the composite action.
Action compositeAction = builder.build();
// Perform the composite action.
compositeAction.perform();
break;
}
}
EditPost editPost
= PageFactory.initElements(driver, EditPost.class);
editPost.editPost(newTitle, description);
}
注意,在前面的代码中,只有根元素通过the-list被识别;包含所有帖子的 All Posts 页面中的元素被映射到名为pageContainer的成员变量,在AllPostsPage PageObject 中。只有在需要时,在editAPost()方法中才会提取目标帖子。这样,您的 PageObject 初始化不需要花费太多时间,并且已经映射了所有必要的元素。
将页面特定的细节从测试脚本中分离出来
PageObject 模式设计的最终目的是将页面特定的细节,例如页面元素 ID 和我们在应用程序中到达特定页面的方式,从测试脚本中分离出来。使用 PageObject 模式构建您的测试框架应该允许您保持测试脚本非常通用,并且不需要在页面实现细节更改时进行修改。最后,每当对网页进行更改时,例如登录页面,对于使用此页面的 50 个测试脚本,理想情况下需要进行的更改数量应该是 0。只需更改 PageObject 就可以处理适应所有测试的新更改。
理解可加载组件
可加载的组件是 PageObject 模式的扩展。WebDriver 库中的LoadableComponent类将帮助测试用例开发者确保页面或页面组件已成功加载。它极大地减少了调试测试用例的努力。PageObject 应该扩展这个 LoadableComponent 抽象类,因此它必须提供以下两个方法的实现:
protected abstract void load()
protected abstract void isLoaded() throws java.lang.Error
在load()和isLoaded()方法中必须加载的页面或组件决定了页面或组件是否已完全加载。如果没有完全加载,它会抛出一个错误。
现在我们修改AdminLoginPage PageObject 以扩展 LoadableComponent 类,并看看它的样子,如下面的代码所示:
public class AdminLoginPageUsingLoadableComponent extends LoadableComponent<AdminLoginPageUsingLoadableComponent> {
WebDriver driver;
@FindBy(id = "user_login")
WebElement email;
@FindBy(id = "user_pass")
WebElement password;
@FindBy(id = "wp-submit")
WebElement submit;
public AdminLoginPageUsingLoadableComponent(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public AllPostsPage login(String username, String pwd) {
email.sendKeys(username);
password.sendKeys(pwd);
submit.click();
return PageFactory.initElements(driver, AllPostsPage.class);
}
@Override
protected void load() {
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin");
}
@Override
protected void isLoaded() throws Error {
Assert.assertTrue(driver.getCurrentUrl().contains("wp-admin"));
}
}
需要加载的 URL 在load()方法中指定,而isLoaded()方法验证是否加载了正确的页面。现在,您在测试用例中需要进行的更改如下:
AdminLoginPageUsingLoadableComponent loginPage = new AdminLoginPageUsingLoadableComponent(driver).get();
LoadableComponent类中的get()方法将确保通过调用isLoaded()方法来加载组件。
在 WordPress 端到端示例上工作
既然我们已经知道了 PageObjects 是什么,现在是时候看看一个端到端示例,该示例与测试 WordPress 管理控制台进行交互和测试。首先,我们将看到所有 PageObjects,然后是使用它们的测试用例。
看看所有的 PageObjects
首先让我们看看所有参与测试 WordPress 管理控制台的 PageObjects。
AdminLoginPage PageObject
AdminLoginPage PageObject 处理登录页面。如果目标应用中的页面有任何更改,此对象需要重构,使用以下代码:
package com.packt.webdriver.chapter9.pageObjects;
import org.openqa.selenium.WebDriver;
public class AdminLoginPage {
WebDriver driver;
@FindBy(id = "user_login")
WebElement email;
@FindBy(id = "user_pass")
WebElement password;
@FindBy(id = "wp-submit")
WebElement submit;
public AdminLoginPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin");
}
public AllPostsPage login(String username, String pwd) {
email.sendKeys(username);
password.sendKeys(pwd);
submit.click();
return PageFactory.initElements(driver,
AllPostsPage.class);
}
}
AdminLoginPage PageObject 的构造函数接受 WebDriver 实例。这将允许测试框架在整个测试脚本执行过程中以及 PageObjects 中使用相同的驱动实例;因此,浏览器和 Web 应用的状态得以保留。您将看到所有 PageObjects 都有类似的构造函数。除了构造函数之外,AdminLoginPage PageObject 提供了 login(String username, String pwd) 服务。此服务允许测试脚本登录到 WordPress 博客,并返回 AllPostsPage PageObject。在返回 AllPostsPage PageObject 的实例之前,PageFactory PageObject 将初始化 AllPostsPage PageObject 的所有 WebElements。因此,登录服务的所有实现细节都隐藏在测试脚本中,并且它可以与 AllPostsPage PageObject 一起工作。
AllPostsPage PageObject
AllPostsPage PageObject 处理所有帖子页面,使用以下代码:
public class AllPostsPage {
WebDriver driver;
@FindBy(id = "the-list")
WebElement postsContainer;
@FindBy(id = "post-search-input")
WebElement searchPosts;
@FindBy(id = "cat")
WebElement viewByCategories;
@FindBy(linkText = "Add New")
WebElement addNewPost;
public AllPostsPage(WebDriver driver) {
this.driver = driver;
driver.get("http://demo-blog.seleniumacademy.com/wp/wp-admin/edit.php");
}
public void createANewPost(String title, String description) {
addNewPost.click();
AddNewPostPage newPost = PageFactory.initElements(driver,
AddNewPostPage.class);
newPost.addNewPost(title, description);
}
public void editAPost(String presentTitle, String newTitle,
String description) {
goToParticularPostPage(presentTitle);
EditPostPage editPost = PageFactory.initElements(driver,
EditPostPage.class);
editPost.editPost(newTitle, description);
}
public void deleteAPost(String title) {
goToParticularPostPage(title);
DeletePostPage deletePost =
PageFactory.initElements(driver, DeletePostPage.class);
deletePost.delete();
}
public void filterPostsByCategory(String category) {
}
public void searchInPosts(String searchText) {
}
public int getAllPostsCount() {
List<WebElement> postsList = postsContainer.findElements(By.tagName("tr"));
return postsList.size();
}
private void goToParticularPostPage(String title) {
List<WebElement> allPosts
= postsContainer.findElements(By.className("title"));
for (WebElement ele : allPosts) {
if (ele.getText().equals(title)) {
Actions builder = new Actions(driver);
builder.moveToElement(ele);
builder.click(driver.findElement(
By.cssSelector(".edit>a")));
// Generate the composite action.
Action compositeAction = builder.build();
// Perform the composite action.
compositeAction.perform();
break;
}
}
}
}
AllPostsPage PageObject 提供了六个服务:
-
创建帖子
-
编辑帖子
-
删除帖子
-
通过分类过滤帖子
-
在帖子中搜索文本
-
计算可用的帖子数量
一旦测试脚本通过 AdminLoginPage PageObject 的登录服务获取到此 PageObject 的实例,它就可以使用此 PageObject 的六个服务之一并进行测试。如果任何实现细节发生变化,例如导航到特定帖子或此页面上 WebElement 的 ID,测试脚本实际上不必担心。修改此 PageObject 将将更改应用到 WordPress 博客中。
AddNewPostPage PageObject
AddNewPostPage PageObject 处理向博客添加新帖子,使用以下代码:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class AddNewPostPage {
WebDriver driver;
@FindBy(id = "content_ifr")
WebElement newPostContentFrame;
@FindBy(id = "tinymce")
WebElement newPostContentBody;
@FindBy(id = "title")
WebElement newPostTitle;
@FindBy(id = "publish")
WebElement newPostPublish;
public AddNewPostPage(WebDriver driver) {
this.driver = driver;
}
public void addNewPost(String title, String descContent) {
newPostTitle.click();
newPostTitle.sendKeys(title);
driver.switchTo().frame(newPostContentFrame);
newPostContentBody.sendKeys(descContent);
driver.switchTo().defaultContent();
newPostPublish.click();
}
}
AddNewPostPage PageObject 在 AllPostsPage PageObject 的 createANewPost 服务中被实例化。此 PageObject 提供一个名为 addNewPost 的服务,该服务接受帖子的 title 和 description 输入,并使用它们在博客中发布新帖子。
EditPostPage PageObject
EditPostPage PageObject 处理编辑现有帖子,使用以下代码:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class EditPostPage {
WebDriver driver;
@FindBy(id = "content_ifr")
WebElement newPostContentFrame;
@FindBy(id = "tinymce")
WebElement newPostContentBody;
@FindBy(id = "title")
WebElement newPostTitle;
@FindBy(id = "publish")
WebElement newPostPublish;
public EditPostPage(WebDriver driver) {
this.driver = driver;
System.out.println(driver.getCurrentUrl());
}
public void editPost(String title, String descContent) {
newPostTitle.click();
newPostTitle.clear();
newPostTitle.sendKeys(title);
driver.switchTo().frame(newPostContentFrame);
newPostContentBody.clear();
newPostContentBody.sendKeys(descContent);
driver.switchTo().defaultContent();
newPostPublish.click();
}
}
EditPostPage 页面对象与 AddNewPostPage 页面对象类似,并在 AllPostsPage 页面对象的 editAPost 服务中实例化。这提供了一个名为 editPost 的服务来编辑现有帖子。新的 title 和 description 作为输入参数传递给此服务。
DeletePostPage 页面对象
DeletePostPage 页面对象使用以下代码处理删除现有帖子:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class DeletePostPage {
WebDriver driver;
@FindBy(linkText = "Move to Trash")
WebElement moveToTrash;
public DeletePostPage(WebDriver driver) {
this.driver = driver;
System.out.println(driver.getCurrentUrl());
}
public void delete() {
moveToTrash.click();
}
}
DeletePostPage 页面对象类似于 AddNewPostPage 和 EditPostPage 页面对象,并在 AllPostsPage 页面对象的 deleteAPost 服务中实例化。这提供了一个名为 delete 的服务来删除现有帖子。正如你所见,AddNewPostPage、EditPostPage 和 DeletePostPage 页面对象都带你到同一个页面。因此,将这三个页面对象合并为一个提供添加、编辑和删除帖子服务的页面对象是有意义的。
查看测试用例
现在是查看使用 PageObjects 与 WordPress 管理控制台交互的测试用例的时候了。
添加新帖子
此测试用例使用以下代码处理向博客添加新帖子:
package com.example;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class WordPressBlogTestsWithPageObject {
WebDriver driver;
String username = "admin";
String password = "$$SUU3$$N#";
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
}
@Test
public void testAddNewPost() {
AdminLoginPage loginPage =
PageFactory.initElements(driver, AdminLoginPage.class);
AllPostsPage allPostsPage = loginPage.login(username, password);
allPostsPage.createANewPost("Creating New Post using PageObjects",
"Its good to use PageObjects");
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
以下是在先前的测试脚本中执行步骤的顺序,以测试如何向 WordPress 博客添加新帖子:
-
测试脚本创建了一个 ChromeDriver 实例,因为它打算测试在 Chrome 浏览器上添加新帖子到博客的场景。
-
它创建了一个
AdminLoginPage页面对象的实例,该实例使用之前步骤中创建的相同驱动程序实例。 -
一旦获得了
AdminLoginPage页面对象的实例,它使用login服务登录到 WordPress 管理控制台。login服务作为回报,向测试脚本提供了一个AllPostsPage页面对象的实例。 -
测试脚本使用之前步骤中获得的
AllPostsPage页面对象的实例来使用All Posts page提供的许多服务之一。在这种情况下,它使用了createANewPost服务。
编辑帖子
此测试用例使用以下代码处理和编辑博客中的帖子测试:
@Test
public void testEditPost() {
AdminLoginPage loginPage =
PageFactory.initElements(driver, AdminLoginPage.class);
AllPostsPage allPostsPage = loginPage.login(username, password);
allPostsPage.editAPost("Creating New Post using PageObjects",
"Editing Post using PageObjects", "Test framework low maintenance");
}
以下是在先前的测试脚本中执行步骤的顺序,以测试如何向 WordPress 博客添加新帖子:
-
它创建了一个
AdminLoginPage页面对象的实例,该实例使用之前步骤中创建的相同驱动程序实例。 -
一旦获得了
AdminLoginPage页面对象的实例,它使用login服务登录到 WordPress 管理控制台。login服务作为回报,向测试脚本提供了一个AllPostsPage页面对象的实例。 -
测试脚本使用之前步骤中获得的
AllPostsPage页面对象的实例来使用All Posts页面提供的许多服务之一。在这种情况下,它使用了createANewPost服务。
删除帖子
此测试用例使用以下代码处理删除帖子:
@Test (dependsOnMethods = "testEditPost")
public void testDeletePost() {
AdminLoginPage loginPage =
PageFactory.initElements(driver, AdminLoginPage.class);
AllPostsPage allPostsPage = loginPage.login(username, password);
allPostsPage.deleteAPost("Editing Post using PageObjects");
}
以下是在先前的测试脚本中执行的一系列步骤,以测试 WordPress 博客中删除帖子的操作:
-
它创建了一个使用之前步骤中创建的相同驱动实例的
AdminLoginPage页面对象实例。 -
一旦获得
AdminLoginPage页面对象的实例,它使用login服务登录 WordPress 管理控制台。login服务作为回报,向测试脚本提供一个AllPostsPage页面对象实例。 -
测试脚本使用在上一步骤中获得的
AllPostsPage页面对象的实例来使用所有帖子页面提供的众多服务之一。在这种情况下,它使用了deleteAPost服务。
计数帖子
此测试用例涉及使用以下代码计算博客中当前可用的帖子数量:
@Test
public void testCountPost() {
AdminLoginPage loginPage =
PageFactory.initElements(driver, AdminLoginPage.class);
AllPostsPage allPostsPage = loginPage.login(username, password);
Assert.assertEquals(allPostsPage.getAllPostsCount(), 1);
}
以下是在先前的测试脚本中执行的一系列步骤,以测试 WordPress 博客中帖子数量的计数:
-
它创建了一个使用之前步骤中创建的驱动实例的
AdminLoginPage页面对象实例。 -
一旦获得
AdminLoginPage页面对象的实例,它使用login服务登录 WordPress 管理控制台。login服务作为回报,向测试脚本提供一个AllPostsPage页面对象实例。 -
测试脚本使用在上一步骤中获得的
AllPostsPage页面对象的实例来使用所有帖子页面提供的众多服务之一。在这种情况下,它使用了getAllPostsCount服务。
摘要
在本章中,我们学习了 PageObject 模式以及如何使用 PageObjects 实现测试框架。它具有许多优点。PageObject 模式和LoadableComponents类提供了一个能够轻松适应目标应用更改的测试框架,而无需更改任何测试用例。我们应该始终记住,一个设计良好的测试框架总是能够适应目标应用的更改。在下一章中,我们将探讨使用Appium测试 iOS 和 Android 移动应用。
问题
-
如何初始化使用 PageFactory 实现的 PageObject?
-
使用哪个类可以实现验证页面是否加载的方法?
-
@FindBy 支持哪些
By class方法? -
当使用 PageFactory 时,如果你给 WebElement 变量赋予与 ID 或 name 属性相同的名称,那么你不需要使用@FindBy 注解——对还是错?
更多信息
你可以查看以下链接以获取有关本章涵盖主题的更多信息:
-
测试设计注意事项:
www.seleniumhq.org/docs/06_test_design_considerations.jsp -
Selenium 中的自动化:PageObjectModel 和 PageFactory:
www.toptal.com/selenium/test-automation-in-selenium-using-page-object-model-and-page-factory
第十章:使用 Appium 在 iOS 和 Android 上进行移动测试
在所有前面的章节中,我们一直在处理在桌面浏览器中加载的 Web 应用程序。但随着移动用户数量的增加,今天的商业企业必须在其移动设备上为用户提供服务。在本章中,您将学习以下内容:
-
不同类型的移动应用程序和测试工具
-
如何使用 Selenium WebDriver 测试移动应用程序,特别是使用 Appium
-
在 Android 和 iOS 上测试移动应用程序
-
使用基于云的设备实验室进行真实设备测试
Appium是一个开源的移动自动化框架,用于使用 Selenium WebDriver 的 JSON 线协议在 iOS 和 Android 平台上测试移动应用程序。Appium 取代了 Selenium 2 中用于测试移动 Web 应用程序的 iPhoneDriver 和 AndroidDriver API。
移动应用程序的不同形式
一个应用程序可以在移动平台上以三种不同的形式触达用户:
-
原生应用:原生应用纯粹针对目标移动平台。它们使用平台支持的语言开发,并且与底层的 SDK 紧密相关。对于 iOS,应用程序使用 Objective-C 或 Swift 编程语言开发,并依赖于 iOS SDK;同样,对于 Android 平台,它们使用 Java 或 Kotlin 开发,并依赖于 Android SDK。
-
m.site:也称为移动网站,它是您 Web 应用程序的迷你版,在您的移动设备浏览器上加载。在 iOS 设备上,它可以是 Safari 或 Chrome,在 Android 设备上,它可以是 Android 默认浏览器或 Chrome。例如,在您的 iOS 或 Android 设备上,打开浏览器并输入www.facebook.com。在页面加载之前,您会观察到从www.facebook.com到m.facebook.com的 URL 重定向发生。Facebook 应用程序服务器意识到请求是从移动设备发起的,并开始提供移动网站而不是桌面网站。这些 m 网站使用 JavaScript 和 HTML5 来开发,就像您的正常 Web 应用程序一样:

- 混合应用:混合应用是原生应用和 Web 应用的结合。当您开发原生应用时,其中一些部分会加载 HTML 网页到应用中,试图让用户感觉他们正在使用原生应用。它们通常在原生应用中使用 WebView 来加载网页。
现在,作为一名测试脚本开发者,您必须在各种移动设备上测试所有这些不同的应用程序。
可用的软件工具
为了自动化在移动设备上测试您的应用程序,有许多软件工具可供选择。以下是一些基于 Selenium WebDriver 构建的工具:
-
Appium:一个基于 Selenium 的跨平台和跨技术移动测试框架,适用于原生、混合和移动 Web 应用程序。Appium 允许使用和扩展现有的 Selenium WebDriver 框架来构建移动测试。由于它使用 Selenium WebDriver 驱动测试,我们可以使用任何存在 Selenium 客户端库的语言来创建测试。您可以在不更改底层驱动程序的情况下,针对 Android 和 iOS 平台创建和执行测试脚本。Appium 还可以与 Firefox OS 平台协同工作。在本章的其余部分,我们将看到如何与 Appium 协同工作。
-
Selendroid:此驱动程序类似于 iOSDriver,可以在 Android 平台上执行您的原生、混合和 m.site 应用程序测试脚本。它使用 Google 提供的本地 UI Automator 库。测试脚本通过 JSON 线协议与 Selendroid 驱动程序通信,同时使用其最喜欢的客户端语言绑定。
使用 Appium 自动化 iOS 和 Android 测试
Appium 是一种流行的广泛使用的工具,可用于自动化 Android 和 iOS 平台的移动应用测试。它可以用于自动化原生、m.sites 和混合应用程序。它内部使用 WebDriver 的 JSON 线协议。
自动化 iOS 应用程序测试
对于自动化 iOS 应用测试,Appium 使用 XCTest 或 UI Automation(适用于较旧的 iOS 版本):
-
XCTest:您可以使用 XCTest 创建和运行针对为 iOS 9.3 及更高版本构建的 iOS 应用程序的单元测试、性能测试和 UI 测试。它与 Xcode 的测试工作流程集成,用于测试 iOS 应用程序。Appium 内部使用 XCTest 自动化 iOS 应用程序。
-
UI Automation:对于测试为 iOS 9.3 及以下版本开发的 iOS 应用,您需要使用 UI Automation。Appium 通过 JSON 线协议从测试脚本接收命令。Appium 将这些命令发送到 Apple Instruments,以便在模拟器或真实设备上启动的应用中执行。在此过程中,Appium 将 JSON 命令转换为仪器可以理解的 UI Automation JavaScript 命令。仪器负责在模拟器或设备上启动和关闭应用。
Appium 作为远程 WebDriver,通过 JSON 线协议接收来自您的测试脚本的命令。这些命令被传递给 XCTest 或 Apple Instruments,以便在模拟器或真实设备上启动的应用中执行。此过程在以下图中展示:

在模拟器或设备上对您的应用执行命令后,目标应用会将响应发送给 XCTest 或 UI Automation Instrument,这些响应以 JavaScript 响应格式传输到 Appium。Appium 将响应转换为 Selenium WebDriver JSON 线协议响应,并将其发送回您的测试脚本。
使用 Appium 进行 iOS 自动化测试的主要优势如下:
-
它使用 iOS 平台支持的 XCTest 或苹果本身提供的 UI Automation 库和工具。
-
尽管你使用的是 JavaScript 库,但你、测试脚本开发者和你的测试脚本实际上并没有真正绑定到它。你可以使用自己的 Selenium WebDriver 客户端语言绑定,如 Java、Ruby 或 Python,来开发你的测试脚本。Appium 将为你处理将它们转换为 JavaScript。
-
你不需要修改你的原生或混合应用以进行测试目的。
自动化 Android 应用程序测试
自动化你的 Android 应用的测试与自动化 iOS 应用的测试类似。除了你的目标平台在变化之外,你的测试脚本不会经历任何变化。以下图显示了工作流程:

再次,Appium 作为一个远程 WebDriver,通过 JSON 协议接收来自你的测试脚本的命令。这些命令被传递给 Android SDK 附带的 Google UI Automator 来在模拟器或真实设备上执行。在命令传递给 UI Automator 之前,Appium 将 JSON 命令转换为 UI Automator 可以理解的命令。这个 UI Automator 将在模拟器或真实设备上启动你的应用,并开始执行你的测试脚本命令。在模拟器或设备上执行命令后,目标应用将响应发送给 UI Automator,该响应以 UI Automator 响应格式传输到 Appium。Appium 将 UI Automator 响应转换为 Selenium WebDriver JSON 协议响应,并将它们发送回你的测试脚本。
这是帮助你理解 Appium 如何与 Android 和 iOS 设备协同工作以执行你的测试命令的高级架构。
Appium 的先决条件
在我们开始讨论 Appium 的一些工作示例之前,我们需要为 iOS 和 Android 平台安装一些先决工具。我们需要设置 Xcode 和 Android Studio 来完成这项任务,我将在这个例子中使用 macOS。
设置 Xcode
要设置 Xcode,我们将执行以下步骤:
-
你可以从
developer.apple.com/xcode/下载最新的 Xcode。 -
下载后,安装并打开它。
-
导航到“首选项”|“组件”以下载和安装命令行工具和 iOS 模拟器,如下面的截图所示:

如果你使用的是真实设备,你需要在设备上安装一个 provisioning profile,并启用 USB 调试。
尝试启动 iPhone 模拟器并验证它是否工作。你可以通过导航到 Xcode | 打开开发者工具 | iOS 模拟器来启动模拟器。模拟器应该看起来与以下截图所示相似:

设置 Android SDK
您需要从 developer.android.com/studio/ 安装 Android SDK。下载并安装 Android Studio。
启动已安装的 Android Studio。现在下载任何 API 级别为 27 的 Android,并安装它。您可以通过导航到“工具”|“SDK 管理器”来完成此操作。您应该会看到类似于以下截图的内容:

在这里,我们正在安装 API 级别为 27 的 Android 8.1。
创建 Android 模拟器
如果您想在 Android 模拟器上执行测试脚本,您必须创建一个。要创建一个,我们将执行以下步骤:
- 在 Android Studio 中,通过导航到“工具”|“AVD 管理器”来打开 AVD 管理器。它将启动 AVD 管理器,如下面的截图所示:

- 通过点击“创建虚拟设备...”按钮创建一个新的虚拟设备或模拟器。您应该会看到一个窗口,该窗口将获取您所需的所有必要信息,如下面的截图所示:

- 启动模拟器以查看它是否已成功创建。Android 虚拟设备启动可能需要几分钟。下面的截图显示了一个已启动的 Android 模拟器:

安装 Appium
您可以从 appium.io/ 下载 Appium。点击下载 Appium 按钮以下载适用于您工作站平台的 Appium。在这里,我使用的是 Mac,因此它将下载 Appium DMG 文件。
将 Appium 复制到 Applications 文件夹,并尝试启动它。第一次启动时,它会要求您的授权以运行 iOS 模拟器,如下面的截图所示:

点击“启动以启动服务器”按钮。默认情况下,它将在 http://localhost:4723 上启动。这是您的测试脚本应将测试命令定向到的远程 URL。
自动化 iOS
现在我们已经运行了 Appium,因此让我们创建一个测试,该测试将检查 iPhone Safari 浏览器上的搜索测试。让我们使用 DesiredCapabilities 类为 Appium 提供能力,以便在 iPhone X 和 iOS 11.4 上运行测试,如下面的代码所示:
public class SearchTest {
private WebDriver driver;
@BeforeTest
public void setUp() throws Exception {
// Set the desired capabilities for iOS- iPhone X
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "iOS");
caps.setCapability("platformVersion", "11.4");
caps.setCapability("deviceName", "iPhone X");
caps.setCapability("browserName", "safari");
// Create an instance of AndroidDriver for testing on Android platform
// connect to the local Appium server running on a different machine
// We will use WebElement type for testing the Web application
driver = new IOSDriver<>(new URL(
"http://192.168.0.101:4723/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
WebElement lookingGlassIcon =
driver.findElement(By
.cssSelector("a.skip-search span.icon"));
lookingGlassIcon.click();
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
List<WebElement> searchItems = new WebDriverWait(driver, 30)
.until(ExpectedConditions
.presenceOfAllElementsLocatedBy(By
.cssSelector("h2.product-name a")));
assertThat(searchItems.size())
.isEqualTo(3);
}
@AfterTest
public void tearDown() throws Exception {
// Close the browser
driver.quit();
}
}
如您所见,前面的代码与 RemoteWebDriver 的测试脚本类似,但有一些不同。以下代码描述了这些差异:
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "iOS");
caps.setCapability("platformVersion", "11.4");
caps.setCapability("deviceName", "iPhone X");
caps.setCapability("browserName", "safari");
Appium Java 客户端库提供了 IOSDriver 类,它支持在 iOS 平台上执行测试,以便使用 Appium 运行测试。然而,为了使 Appium 能够使用所需的平台,我们需要传递一组所需的配置。platformName 配置能力被 Appium 用于决定测试脚本应该在哪个平台上执行。在这个例子中,我们使用了 iPhone X 模拟器。要在 iPad 上运行测试,我们可以指定 iPad 模拟器。
在真实设备上运行测试时,我们需要指定设备能力中 iPhone 或 iPad 的值。Appium 将选择通过 USB 连接到 Mac 的设备。我们最后使用的期望能力是 browserName,它由 Appium 用于启动 Safari 浏览器。
为 Android 自动化
在 Android 上测试应用程序与在 iOS 上测试类似。对于 Android,我们将使用真实设备而不是仿真器(在 Android 社区中,仿真器被称为仿真器)。我们将使用相同的应用程序在 Android 的 Chrome 浏览器上测试。
对于这个例子,我正在使用三星 Galaxy S4 Android 手机。我们需要在设备上安装 Google Chrome 浏览器。如果您设备上没有预装 Google Chrome,可以在 Google 的 Play 商店获取。接下来,我们需要将设备连接到运行 Appium 服务器的机器。让我们运行以下命令以获取连接到机器的仿真器或设备的列表:
./adb devices
Android Debug Bridge(ADB)是 Android SDK 中可用的命令行工具,允许您与仿真器实例或连接到您的计算机的实际 Android 设备通信。./adb devices命令将显示连接到主机的所有 Android 设备的列表,如下面的输出所示:
List of devices attached
4df1e76f39e54f43 device
让我们修改为 iOS 创建的脚本,以使用 Android 的功能和 AndroidDriver 类在真实 Android 设备上执行测试,如下面的代码所示:
public class MobileBmiCalculatorTest {
private WebDriver driver;
@BeforeTest
public void setUp() throws Exception {
// Set the desired capabilities for Android Device
DesiredCapabilities caps = DesiredCapabilities.android();
caps.setCapability("deviceOrientation", "portrait");
caps.setCapability("platformVersion", "8.1");
caps.setCapability("platformName", "Android");
caps.setCapability("browserName", "Chrome");
// Create an instance of AndroidDriver for testing on Android platform
// connect to the local Appium server running on a different machine
// We will use WebElement type for testing the Web application
driver = new AndroidDriver<WebElement>(new URL(
"http://192.168.0.101:4723/wd/hub"), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
在前面的例子中,我们将platformName能力值分配给了Android,这将由 Appium 用于在 Android 上运行测试。由于我们想在 Android 的 Chrome 浏览器上运行测试,我们在代码的浏览器能力部分提到了 Chrome。我们做出的另一个重要改变是使用 Appium Java 客户端库中的AndroidDriver类。
Appium 将使用adb返回的设备列表中的第一个设备,如下面的截图所示。它将使用我们提到的期望能力,并在设备上启动 Chrome 浏览器并开始执行测试脚本命令。
使用 Device Cloud 在真实设备上运行测试
Appium 支持在移动模拟器、仿真器和真实设备上进行测试。要设置一个包含真实设备的移动测试实验室,需要资本投资以及设备和基础设施的维护。手机制造商几乎每天都会发布新的手机型号和操作系统更新,而您的应用程序必须与新发布兼容。
为了更快地应对这些变化并保持投资最小,我们可以使用基于云的移动测试实验室。有许多供应商,如亚马逊网络服务、BrowserStack 和 Sauce Labs,提供基于云的实时移动设备实验室来执行测试,无需对真实设备进行任何前期投资。你只需为测试使用的时间付费。这些供应商还允许你在他们的设备云中运行使用 Appium 的自动化测试。
在本节中,我们将探索 BrowserStack 以在其真实设备云上运行测试:
-
你需要一个带有Automate功能订阅的 BrowserStack 账户。你可以在
www.browserstack.com/注册一个免费试用账户。 -
我们需要根据设备组合从 BrowserStack 获取所需的配置能力。BrowserStack 根据所选的设备和平台组合提供能力建议。访问
www.browserstack.com/automate/java并选择一个操作系统和设备:

- 根据你的选择,BrowserStack 将使用你的用户名和访问密钥自动生成代码:

我们将不会使用第 3 步中建议的代码,而是将我们的测试更改为以下代码所示。记住,你需要使用自动生成的代码中显示的用户名和访问密钥:
public class SearchTest {
private WebDriver driver;
@BeforeTest
public void setUp() throws Exception {
String USERNAME = "username";
String AUTOMATE_KEY = "access_key";
String URL = "https://" + USERNAME + ":"
+ AUTOMATE_KEY + "@hub-cloud.browserstack.com/wd/hub";
// Set the desired capabilities for iPhone X
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("browserName", "iPhone");
caps.setCapability("device", "iPhone X");
caps.setCapability("realMobile", "true");
caps.setCapability("os_version", "11.0");
driver = new RemoteWebDriver(new URL(URL), caps);
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test
public void searchProduct() {
WebElement lookingGlassIcon =
driver.findElement(By
.cssSelector("a.skip-search span.icon"));
lookingGlassIcon.click();
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
List<WebElement> searchItems = new WebDriverWait(driver, 30)
.until(ExpectedConditions
.presenceOfAllElementsLocatedBy(By
.cssSelector("h2.product-name a")));
assertThat(searchItems.size())
.isEqualTo(3);
}
@AfterTest
public void tearDown() throws Exception {
// Close the browser
driver.quit();
}
}
从你的 IDE 执行测试,它将在 BrowserStack 云中运行。你可以在 BrowserStack 仪表板中监控测试,其中将显示使用的配置能力、每个步骤的状态、控制台日志、网络日志、Appium 日志和执行的视频:

摘要
在本章中,我们讨论了企业如何在移动平台上接触其用户的不同方式。我们还了解了使用 Selenium WebDriver 创建的各种软件工具。最后,我们介绍了一个即将推出的软件工具,并修改了我们的测试脚本以与 iOS 和 Android 平台兼容。
在下一章节中,我们将了解如何使用TestNG创建参数化和数据驱动的测试。这将帮助我们重用测试并增加测试覆盖率。
问题
-
移动应用有哪些不同类型?
-
Appium Java 客户端库为测试 iOS 和 Android 应用程序提供了哪些类?
-
列出通过 USB 端口连接到计算机的 Android 设备的命令是什么?
-
Appium 服务器默认使用哪个端口?
更多信息
你可以查看以下链接以获取有关本章涵盖主题的更多信息:
- 想了解更多关于使用 Appium 的示例,请访问其网站和 GitHub 论坛
appium.io/和github.com/appium/appium/tree/master/sample-code/java
第十一章:使用 TestNG 进行数据驱动测试
在本章中,我们将了解如何使用 TestNG 和 Selenium WebDriver 创建数据驱动测试。我们将探讨以下主题:
-
什么是数据驱动测试?
-
使用 TestNG 套件参数来参数化测试。
-
使用 TestNG 数据提供者进行数据驱动测试。
-
使用 CSV 和 Excel 文件格式来存储和读取测试数据。
数据驱动测试概述
通过采用数据驱动测试方法,我们可以通过使用来自外部数据源(而不是每次运行测试时都使用硬编码的值)的输入和预期值来驱动测试,从而使用单个测试验证不同的测试用例集或测试数据。这在当我们有类似的测试,这些测试由相同的步骤组成,但在输入数据、预期值或应用程序状态上有所不同时非常有用。以下是一个具有不同组合的登录测试用例集的示例:
| 描述 | 测试数据 | 预期输出 |
|---|---|---|
| 测试有效的用户名和密码 | 一对有效的用户名和密码 | 用户应该以成功消息登录应用程序 |
| 测试无效的用户名和密码 | 一个无效的用户名和密码 | 应向用户显示登录错误 |
| 有效的用户名和无效的密码 | 一个有效的用户名和一个无效的密码 | 应向用户显示登录错误 |
我们可以创建一个单一的脚本,它可以处理测试数据和前表中所示的条件。通过使用数据驱动测试方法,我们通过使用来自外部源(如 CSV 或电子表格文件)的数据替换硬编码的测试数据,将测试数据与测试逻辑分离。这也帮助创建可重用的测试,这些测试可以与不同的数据集一起运行,这些数据集可以保留在测试之外。数据驱动测试还有助于提高测试覆盖率,因为我们可以在最小化需要编写和维护的测试代码量的同时处理多个测试条件。
数据驱动测试的好处如下:
-
我们可以在最小化需要编写和维护的测试代码量的同时获得更大的测试覆盖率
-
它使得创建和运行大量测试条件变得非常容易
-
测试数据可以在应用程序准备好测试之前设计和创建
-
数据表也可以用于手动测试
Selenium WebDriver 作为一种纯浏览器自动化 API,不提供内置功能来支持数据驱动测试。然而,我们可以通过使用测试框架(如 JUnit 或 TestNG)来添加对数据驱动测试的支持。在这本书中,我们使用 TestNG 作为我们的测试框架,我们将在以下章节中使用 TestNG 的参数化功能来创建数据驱动测试。
使用套件参数参数化测试
在第一章“介绍 WebDriver 和 WebElements”中,我们创建了一个搜索测试,该测试对正在测试的应用程序执行简单搜索。此测试搜索给定产品并验证标题。我们使用硬编码的值phones进行搜索,如下面的代码片段所示:
@Test
public void searchProduct() {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys("Phones");
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: 'Phones'");
}
我们可以参数化这些值,并使用 TestNG 的套件参数功能将它们提供给测试方法。这将有助于从测试方法中移除硬编码的值,并将它们移动到 TestNG 套件文件中。参数化值可以在多个测试中使用。当我们需要更改这些值时,我们不必去每个测试并做出更改,而是可以简单地更改套件文件中的这些值。
现在,让我们看看从套件文件中使用 TestNG 参数的步骤。在第一章中,我们创建了一个testng.xml文件,该文件位于src/test/resources/suites文件夹中。让我们修改这个文件并添加参数声明,如下面的代码片段所示:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Chapter 1" verbose="1">
<listeners>
<listener class-name="com.vimalselvam.testng.listener.ExtentTestNgFormatter"/>
</listeners>
<test name="Search Test">
<parameter name="searchWord" value="phones"/>
<parameter name="items" value="3"/>
<classes>
<class name="com.example.SearchTest"/>
</classes>
</test>
</suite>
我们可以使用<parameter>标签在 TestNG 套件文件中添加参数。我们必须为参数提供name和value属性。在本例中,我们创建了两个参数:searchWord和items。这些参数存储搜索词和应用程序为该搜索词返回的预期项目数量。
现在,让我们修改测试以使用参数而不是硬编码的值。首先,我们需要在测试方法的@Test注解之前使用@Parameters注解。在@Parameters注解中,我们需要提供套件文件中声明的参数的确切名称和顺序。在本例中,我们将提供searchWord和items。我们还需要向测试方法添加参数以及所需的数据类型,以映射 XML 参数。在本例中,我们向searchProduct()测试方法添加了String searchWord和int Items参数。最后,我们需要在测试方法中将硬编码的值替换为参数,如下面的代码片段所示:
@Parameters({"searchWord", "items"})
@Test
public void searchProduct(String searchWord, int items) {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
// use searchWord parameter value from XML suite file
searchBox.sendKeys(searchWord);
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: '" + searchWord + "'");
List<WebElement> searchItems = driver
.findElements(By.xpath("//h2[@class='product-name']/a"));
assertThat(searchItems.size())
.isEqualTo(items);
}
我们必须通过 testng.xml 文件运行参数化测试,以便 TestNG 读取套件文件中定义的参数并将值传递给测试方法。
在执行过程中,TestNG 将使用 XML 套件文件中定义的参数,并使用@Parameters注解将这些参数映射到测试方法中的 Java 参数的相同顺序。它将通过在测试方法中添加的参数传递套件文件中的参数值。如果 XML 和@Parameters注解之间的参数数量不匹配,TestNG 将抛出异常。
在下一节中,我们将看到程序化参数化,它为我们提供了运行具有多行测试数据的测试的能力。
使用数据提供程序参数化测试
虽然套件参数对于简单的参数化很有用,但它们不足以创建具有多个测试数据值的数据驱动测试,并从外部文件(如属性文件、CSV、Excel 或数据库)中读取数据。在这种情况下,我们可以使用Data Provider来提供需要测试的值。Data Provider是在测试类中定义的一个方法,它返回一个对象数组的数组。此方法使用@DataProvider注解。
让我们修改前面的测试以使用Data Provider。现在,我们将使用由搜索返回的单个searchWord的三种组合以及预期的items计数。我们将在@BeforeMethod注解之前在SearchTest类中添加一个名为provider()的新方法,如下面的代码所示:
public class SearchTest {
WebDriver driver;
@DataProvider(name = "searchWords")
public Object[][] provider() {
return new Object[][]{
{"phones", 3},
{"music", 5},
{"iphone 5s", 0}
};
}
@BeforeMethod
public void setup() {
...
}
...
}
当一个方法被@DataProvider注解时,它通过传递测试数据到测试用例而成为一个数据提供方法。除了@DataProvider注解之外,我们还需要为data provider提供一个名称。在这个例子中,我们将其命名为searchWords。
接下来,我们需要更新searchTest()测试方法以链接到data provider。这可以通过以下步骤完成:
-
在
@Test注解中提供data provider的名称 -
向
searchProduct方法添加两个参数String searchWord和int items -
使用方法参数替换硬编码的值:
public class SearchTest {
WebDriver driver;
@DataProvider(name = "searchWords")
public Object[][] provider() {
...
}
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test(dataProvider = "searchWords")
public void searchProduct(String searchWord, int items) {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys(searchWord);
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: '" + searchWord + "'");
List<WebElement> searchItems = driver
.findElements(By.xpath("//h2[@class='product-name']/a"));
assertThat(searchItems.size())
.isEqualTo(items);
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
provider()方法将成为数据提供方法,它返回由searchWords和预期的items计数组合的对象数组,TestNG 将传递数据行数组到测试方法。
TestNG 将执行四次测试,使用不同的测试组合。TestNG 在测试执行结束时还会生成一个格式良好的报告。以下是一个使用定义的值通过 TestNG 进行测试结果的示例。searchProduct测试执行了三次,如下面的截图所示:

从 CSV 文件读取数据
我们看到了一个简单的数据驱动测试 TestNG。测试数据被硬编码在测试脚本代码中。这可能会变得难以维护。建议我们将测试数据与测试脚本分开存储。通常,我们使用生产环境中的数据来进行测试。这些数据可以导出为 CSV 格式。我们可以在数据提供者方法中读取这些 CSV 文件,并将数据传递给测试,而不是硬编码的对象数组。
在这个例子中,我们将使用 OpenCSV 库来读取 CSV 文件。OpenCSV 是一个简单的 Java 库,用于在 Java 中读取 CSV 文件。您可以在opencsv.sourceforge.net/找到更多关于 OpenCSV 的详细信息。
首先,在src/test/resources/data文件夹中创建一个名为data.csv的 CSV 文件,并复制以下searchWords和items的组合:
searchWord,items
phones,3
music,5
iphone 5s,0
接下来,我们需要将 OpenCSV 依赖项添加到 Maven pom.xml 文件中。对于此示例,我们将使用最新版本 3.4,如下面的代码片段所示:
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>3.4</version>
</dependency>
最后,我们需要修改测试类中的 provider() 方法,以读取 CSV 文件的内容,并将其作为对象的数组返回,如下面的代码所示:
public class SearchTest {
WebDriver driver;
@DataProvider(name = "searchWords")
public Iterator<Object[]> provider() throws Exception {
CSVReader reader = new CSVReader(
new FileReader("./src/test/resources/data/data.csv")
, ',', '\'', 1);
List<Object[]> myEntries = new ArrayList<Object[]>();
String[] nextLine;
while ((nextLine = reader.readNext()) != null) {
myEntries.add(nextLine);
}
reader.close();
return myEntries.iterator();
}
@BeforeMethod
public void setup() {
System.setProperty("webdriver.chrome.driver",
"./src/test/resources/drivers/chromedriver");
driver = new ChromeDriver();
driver.get("http://demo-store.seleniumacademy.com/");
}
@Test(dataProvider = "searchWords")
public void searchProduct(String searchWord, String items) {
// find search box and enter search string
WebElement searchBox = driver.findElement(By.name("q"));
searchBox.sendKeys(searchWord);
WebElement searchButton =
driver.findElement(By.className("search-button"));
searchButton.click();
assertThat(driver.getTitle())
.isEqualTo("Search results for: '" + searchWord + "'");
List<WebElement> searchItems = driver
.findElements(By.xpath("//h2[@class='product-name']/a"));
assertThat(searchItems.size())
.isEqualTo(Integer.parseInt(items));
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
在 provide 方法中,将使用 OpenCSV 库的 CSVReader 类来解析 CSV 文件。我们需要提供 CSV 文件的路径、分隔符字符和标题行号(在获取数据时会跳过),如下面的代码片段所示:
@DataProvider(name = "searchWords")
public Iterator<Object[]> provider() throws Exception {
CSVReader reader = new CSVReader(
new FileReader("./src/test/resources/data/data.csv")
, ',', '\'', 1);
List<Object[]> myEntries = new ArrayList<Object[]>();
String[] nextLine;
while ((nextLine = reader.readNext()) != null) {
myEntries.add(nextLine);
}
reader.close();
return myEntries.iterator();
}
在前面的代码中,我们将读取 CSV 文件的每一行,将其复制到对象的数组中,并将其返回给测试方法。测试方法将对 CSV 文件中的每一行执行。
从 Excel 文件中读取数据
为了维护测试用例和测试数据,Microsoft Excel 是测试人员的首选工具。与 CSV 文件格式相比,Excel 提供了众多功能和一种结构化的方式来存储数据。测试人员可以轻松地在 Excel 电子表格中创建和维护测试数据表。
让我们在 src/test/resources/data 文件夹中创建一个名为 data.xlsx 的 Excel 电子表格,内容如下:

在本节中,我们将使用 Excel 电子表格作为数据源。我们将使用 Apache 基金会开发的 Apache POI API 来操作 Excel 电子表格。
让我们修改 provider() 方法,使用一个名为 SpreadsheetData 的辅助类来读取 Excel 文件的内容:
@DataProvider(name = "searchWords")
public Object[][] provider() throws Exception {
SpreadsheetData spreadsheetData = new SpreadsheetData();
return spreadsheetData.getCellData("./src/test/resources/data/data.xlsx");
}
SpreadsheetData 类,这个类包含在本书的源代码包中。这个类支持旧版的 .xls 和较新的 .xlsx 格式:
public class SpreadsheetData {
public String[][] getCellData(String path) throws InvalidFormatException, IOException {
FileInputStream stream = new FileInputStream(path);
Workbook workbook = WorkbookFactory.create(stream);
Sheet s = workbook.getSheetAt(0);
int rowcount = s.getLastRowNum();
int cellcount = s.getRow(0).getLastCellNum();
String data[][] = new String[rowcount][cellcount];
for (int rowCnt = 1; rowCnt <= rowcount; rowCnt++) {
Row row = s.getRow(rowCnt);
for (int colCnt = 0; colCnt < cellcount; colCnt++) {
Cell cell = row.getCell(colCnt);
try {
if (cell.getCellType() == cell.CELL_TYPE_STRING) {
data[rowCnt - 1][colCnt] = cell.getStringCellValue();
} else {
data[rowCnt - 1][colCnt] = String.valueOf(cell.getNumericCellValue());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return data;
}
}
当测试执行时,provider() 方法将创建 SpreadsheetData 类的一个实例。SpreadsheetData 类将按行逐行读取 Excel 电子表格的内容到一个集合中,并将这个集合返回给 provider() 方法:
InputStream spreadsheet = new FileInputStream("./src/test/resources/data/data.xlsx");
return new SpreadsheetData(spreadsheet).getData();
对于 provider() 方法返回的测试数据集合中的每一行,测试运行器将实例化测试用例类,将测试数据作为参数传递给测试类构造函数,然后执行测试类中的所有测试。
摘要
在本章中,我们学习了使用 TestNG 功能创建参数化和数据驱动测试的重要技术。这将帮助您以最小的编码努力和增加的测试覆盖率创建高度可维护和健壮的测试。我们还探讨了从 CSV 和 Excel 格式读取数据的方法。
问题
-
解释数据驱动测试是什么。
-
Selenium 支持数据驱动测试——对或错?
-
TestNG 中创建数据驱动测试的两种方法是什么?
-
解释 TestNG 中的
DataProvider方法。
进一步信息
您可以查看以下链接以获取有关本章涵盖主题的更多信息:
-
在
testng.org/doc/documentation-main.html#parameters了解更多关于 TestNG 数据驱动特性的信息 -
在
poi.apache.org/了解更多关于 Apache POI 库的信息
第十二章:评估
第一章
- 对或错:Selenium 是一个浏览器自动化库。
正确。
- Selenium 提供了哪些不同的定位机制类型?
不同的定位机制类型包括 ID、Name、ClassName、TagName、Link、LinkText、CSS Selector 和 XPATH。
- 对或错:使用
getAttribute()方法,我们也可以读取 CSS 属性吗?
错误。getCssValue() 方法用于读取 CSS 属性。
- 可以在 WebElement 上执行哪些操作?
执行的动作包括点击、输入(sendKeys)和提交。
- 我们如何确定复选框是被选中还是未选中?
通过使用 isSelected() 方法。
第二章
- WebDriver 成为 W3C 规范的意义是什么?
WebDriver 现在是一个 W3C 规范。这意味着浏览器必须完全遵守世界万维网联盟(简称 W3C)设定的 WebDriver 规范,并将由浏览器厂商原生支持,HTML5 和 CSS 是其他突出的 W3C 规范。
- 对或错:WebDriver 是一个接口。
正确。
- 哪些浏览器支持无头测试?
Google Chrome 和 Mozilla Firefox。
- 我们如何使用 Chrome 测试移动网站?
通过使用移动仿真功能。
第三章
- 哪个版本的 Java Streams API 被引入?
Java 8。
- 解释 Streams API 的过滤函数。
Java Stream API 提供了一个 filter() 方法来根据给定的谓词过滤流元素。假设我们想获取页面上所有可见的链接元素,我们可以使用 filter() 方法以以下方式返回列表:
List<WebElement> visibleLinks = links.stream()
.filter(item -> item.isDisplayed())
.collect(Collectors.toList());
- Streams API 的哪种方法会从
filter()函数返回匹配元素的数量?
count()。
- 我们可以使用
map()函数通过属性值过滤 WebElements 列表:对或错?
错误。
第四章
- 我们可以使用哪些不同的格式来输出截图?
OutputType 接口支持 BASE64、BYTES 和 FILE 格式的截图类型。
- 我们如何使用 Selenium 切换到另一个浏览器标签页?
我们可以使用 driver.switchTo().window() 方法切换到另一个浏览器标签页。
- 对或错:
defaultContent()方法将切换到之前选定的框架。
错误。defaultContent() 方法将切换到页面。
- Selenium 提供了哪些导航方法?
Navigate 接口提供了 to()、back()、forward() 和 refresh() 方法。
- 我们如何使用 Selenium 添加一个 cookie?
我们可以使用 driver.manage().addCookie(Cookie cookie) 方法添加一个 cookie。
- 解释隐式等待和显式等待之间的区别。
一旦设置,隐式等待将在整个 WebDriver 实例的生命周期内可用。当调用 findElement 时,它将在设定的持续时间内等待元素。如果元素在设定时间内没有出现在 DOM 中,它将抛出 NoSuchElementFound 异常。
另一方面,显式等待用于等待特定条件发生(例如,元素的可见性或不可见性、标题的变化、元素属性的变化、元素变为可编辑或自定义条件)。与隐式等待不同,显式等待将轮询 DOM 以满足条件,而不是等待固定的时间。如果条件在定义的超时之前得到满足,它将退出,否则将抛出异常。我们可以使用 ExpectedConditions 类中的各种预定义条件与显式等待一起使用。
第五章
- 对或错 – 拖放操作需要源元素和目标元素。
对。
- 列出我们可以使用 actions API 执行的键盘方法。
sendKeys()、keyUp() 和 keyDown()。
- 哪个 actions API 方法可以帮助执行双击操作?
doubleClick(WebElement target)。
- 使用 actions API,我们如何执行保存选项(即 Ctrl + S)?
new Actions(driver) .sendKeys(Keys.chord(Keys.CONTROL, "s")) .perform();。
- 我们如何使用 actions API 打开上下文菜单?
通过调用 contextClick() 方法。
第六章
- 您可以使用 WebDriverEventListener 接口来监听 WebDriver 事件:对或错?
对。
- 如何使用 WebDriverEventListener 在调用
sendKeys方法之前自动清除输入字段?
我们可以在 beforeChangeValueOf() 事件处理器中调用 WebElement.clear() 方法。
- Selenium 支持可访问性测试:对或错?
错误。Selenium 不支持可访问性测试
第七章
- 对或错:使用 Selenium,我们可以在远程机器上执行测试 -
对。
- 用于在远程机器上运行测试的驱动类是哪个?
RemoteWebDriver 类。
- 解释
DesiredCapabilities类。
DesiredCapabilities 类用于指定 RemoteWebDriver 所需的浏览器能力。例如,我们可以在 DesiredCapabilities 中指定浏览器名称、操作系统和版本,并将其传递给 RemoteWebDriver。Selenium Standalone Server 将将配置的能力与可用的节点匹配,并在匹配的节点上运行测试。
- Selenium 测试和 Selenium Standalone Server 之间使用的是哪种协议?
JSON-Wire。
- Selenium Standalone Server 使用的默认端口是什么?
端口 4444。
第八章
- 哪个参数可以用来指定节点可以支持多少浏览器实例?
maxInstances。
- 解释如何使用 Selenium Grid 来支持跨浏览器测试。
使用 Selenium Grid,我们可以为各种浏览器和操作系统组合设置节点,并在分布式架构中运行测试。根据测试提供的功能,Selenium Grid 选择适当的节点并在所选节点上执行测试。我们可以根据所需的跨浏览器测试矩阵组合添加所需数量的节点。
- 使用
RemoteWebDriver运行 Selenium Grid 测试时,需要指定哪个 URL?
http://gridHostnameOrIp:4444/wd/hub。
- Selenium Grid Hub 充当负载均衡器:对还是错?
真的。Selenium Grid Hub 根据节点的可用性在多个节点上分配测试。
第九章
- 如何初始化使用 PageFactory 实现的 PageObject?
PageFactory.initElements(driver, pageObjectClass)。
- 我们可以使用哪个类来实现验证页面是否加载的方法?
LoadableComponent。
- @FindBy 支持哪些
By class方法?
ID、Name、ClassName、TagName、Link、PartialLinkText、CSS Selector 和 XPATH。
- 当使用 PageFactory 时,如果你将 WebElement 变量的名称命名为与 ID 或 name 属性相同的名称,那么你不需要使用
@FindBy注解:对还是错?
真的。你可以使用与 id 或 name 属性值相同的名称来声明 WebElement 变量,PageFactory 将无需使用@FindBy注解来解析它。
第十章
- 有哪些不同类型的移动应用程序?
原生、混合和移动 Web 应用程序。
- Appium Java 客户端库为测试 iOS 和 Android 应用程序提供了哪些类?
AndroidDriver和IOSDriver。
- 列出通过 USB 端口连接到计算机的 Android 设备的命令是什么?
adb devices。
- Appium 服务器使用的默认端口是什么?
端口4723。
第十一章
- 解释数据驱动测试。
数据驱动是一种测试自动化框架方法,其中输入测试数据以表格格式或电子表格格式存储,单个测试脚本读取数据表中的每一行,这可以是一个独特的测试用例,并执行步骤。这使测试脚本可重用,并通过不同的测试数据组合增加了测试覆盖率。
- 对还是错:Selenium 支持数据驱动测试。
错误。
- TestNG 中有哪些方法可以创建数据驱动测试?
TestNG 提供了两种数据驱动测试方法:套件参数和数据提供者。
- 解释 TestNG 中的 DataProvider 方法。
TestNG 中的 DataProvider 方法是一个特殊的方法,用@DataProvider注解标记。它返回一个对象数组。我们可以返回从任何格式(如 CSV 或 Excel)读取的表格数据,以使用数据提供者测试测试用例。


浙公网安备 33010602011771号