精通-JavaFX10-全-

精通 JavaFX10(全)

原文:zh.annas-archive.org/md5/ed6aa077da1688755465ac62dfa875c2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是关于 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、处理 Cookie 以及处理窗口和框架。

第五章,探索高级交互 API,将深入探讨 WebDriver 可以在网页的 WebElements 上执行更高级的操作,例如将元素从一个页面的一个框架拖放到另一个框架,以及在 WebElements 上右键单击/上下文单击。我们相信您会发现这一章很有趣。

第六章,理解 WebDriver 事件,将处理 WebDriver 的事件处理方面。举几个例子,事件可以是 WebElement 上的值变化,浏览器后退导航调用,脚本执行完成等。我们将使用这些事件来运行可访问性和性能检查。

第七章,探索 RemoteWebDriver,将讨论使用 RemoteWebDriver 和 Selenium Standalone Server 从您的机器上执行远程机器上的测试。您可以使用 RemoteWebDriver 类与远程机器上的 Selenium Standalone Server 通信,以在远程机器上运行的所需浏览器上执行命令。其流行的用例之一是浏览器兼容性测试。

第八章,设置 Selenium Grid,将讨论 Selenium 的一个重要且有趣的功能,名为 Selenium Grid。使用它,您可以使用 Selenium Grid 在分布式计算机网络上执行自动化测试。我们将配置一个 Hub 和节点以进行跨浏览器测试。这也使得并行运行测试和在分布式架构中运行测试成为可能。

第九章,页面对象模式,将讨论一个知名的设计模式,称为页面对象模式。这是一个经过验证的模式,将帮助您更好地掌握自动化框架和场景,以实现更好的可维护性。

第十章,使用 Appium 在 iOS 和 Android 上进行移动测试,将向您介绍如何使用 WebDriver 通过 Appium 自动化 iOS 和 Android 平台的测试脚本。

第十一章,使用 TestNG 进行数据驱动测试,将讨论使用 TestNG 的数据驱动测试技术。使用数据驱动测试方法,我们可以使用多组测试数据重用测试,以获得额外的覆盖率。

为了充分利用本书

预期读者对编程有一个基本的概念,最好是使用 Java,因为我们将通过代码示例带读者了解 WebDriver 的几个功能。本书需要以下软件:

  1. Oracle JDK8

  2. Eclipse IDE

  3. Maven 3

  4. Google Chrome

  5. Mozilla Firefox

  6. Windows 上的 Internet Explorer 或 Edge

  7. Apple Safari

  8. 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并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择 SUPPORT 标签。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载完成后,请确保使用最新版本解压缩或提取文件夹。

  • 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 TestNG 注解。

代码块设置如下:

<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"));

粗体: 表示新术语、重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇在文本中显示如下。以下是一个示例:“要运行测试,在代码编辑器中右键单击,然后选择运行为 | TestNG 测试,如以下截图所示。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上以任何形式发现了我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问packtpub.com

第一章:介绍 WebDriver 和 Web 元素

在本章中,我们将简要介绍 Selenium,其各种组件,例如 Appium,然后继续探讨网页的基本组件,包括各种类型的 Web 元素。我们将学习不同的方法在网页上定位 Web 元素并对它们执行各种用户操作。本章将涵盖以下主题:

  • Selenium 测试工具的各个组件

  • 使用 Maven 和 TestNG 在 Eclipse 中设置项目

  • 在网页上定位 Web 元素

  • 可以在 Web 元素上执行的操作

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 WebDriver 使用 JSON-Wire 协议(也称为客户端 API)接受命令,并将它们发送到由特定驱动程序类(如 ChromeDriver、FirefoxDriver 或 IEDriver)启动的浏览器。这是通过特定于浏览器的浏览器驱动程序实现的。它按照以下顺序工作:

  1. 驱动程序监听来自 Selenium 的命令

  2. 它将这些命令转换为浏览器的原生 API

  3. 驱动程序接收原生命令的结果,并将结果发送回 Selenium:

图片

我们可以使用 Selenium WebDriver 执行以下操作:

  • 创建健壮的基于浏览器的回归自动化

  • 在许多浏览器和平台上缩放和分发脚本

  • 使用您喜欢的编程语言创建脚本

Selenium WebDriver 提供了一套特定于语言的绑定(客户端库),用于驱动浏览器。WebDriver 附带了一组更好的 API,通过其实现类似于面向对象编程,满足了大多数开发者的期望。WebDriver 正在一段时间内积极开发,你可以看到许多与 Web 以及移动应用程序的先进交互。

Selenium 客户端 API 是一种特定于语言的 Selenium 库,它为 Java、C#、Python、Ruby 和 JavaScript 等编程语言提供了一致的 Selenium API。这些语言绑定允许测试启动 WebDriver 会话并与浏览器或 Selenium 服务器通信。

Selenium 服务器

Selenium 服务器允许我们在远程机器上运行的浏览器实例上并行运行测试,从而将测试负载分散到多台机器上。我们可以创建一个 Selenium Grid,其中一个服务器作为 Hub 运行,管理节点池。我们可以配置我们的测试以连接到 Hub,然后 Hub 获取一个空闲的节点,该节点与我们需要运行的测试所需的浏览器相匹配。Hub 有一个节点列表,提供对浏览器实例的访问,并允许测试像负载均衡器一样使用这些实例。Selenium Grid 通过集中管理不同类型的浏览器、它们的版本和操作系统配置,使我们能够在多台机器上并行执行测试。

Selenium IDE

Selenium IDE 是一个 Firefox 插件,允许用户以 Selenese 格式记录、编辑、调试和回放测试,该格式是在 Selenium 核心版本中引入的。它还提供了将测试转换为 Selenium RC 或 Selenium WebDriver 格式的功能。我们可以使用 Selenium IDE 执行以下操作:

  • 使用记录和回放创建快速简单的脚本,或在探索性测试中使用它们

  • 创建脚本以辅助自动化辅助的探索性测试

  • 创建宏以在网页上执行重复性任务

由于 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 规范得到了浏览器厂商的支持;您可以看到许多与网页以及移动应用的高级交互,例如文件处理和触摸 API。

使用 Appium 测试移动应用

在 Selenium 3 中引入的一个主要区别是引入了 Appium 项目。原本属于 Selenium 2 的移动测试功能现在已移至一个名为 Appium 的独立项目中。

Appium 是一个开源的移动自动化框架,用于使用 JSON-Wire 协议和 Selenium WebDriver 在 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 IDE for Java Developers。

除了 Eclipse 和 Maven,我们还将使用 TestNG 作为项目的测试框架。TestNG 库将帮助我们定义测试用例、测试夹具和断言。我们需要通过 Eclipse Marketplace 安装 TestNG 插件。

让我们按照以下步骤配置 Eclipse 和 Maven 以使用 Selenium WebDriver 开发测试:

  1. 启动 Eclipse IDE。

  2. 通过选择 Eclipse 主菜单中的文件 | 新建 | 其他来创建一个新的项目。

  3. 在“新建”对话框中,选择 Maven | Maven 项目,如图所示,然后点击“下一步”:

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

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

  1. Eclipse 将创建名为chapter1的项目,其结构(在包资源管理器中)类似于以下截图所示:

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

  1. 将以下代码片段中突出显示的 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>
  1. 在包资源管理器中选择src/test/java,然后右键单击以显示菜单。选择新建 | 其他,如图所示:

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

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

  1. 这将在 com.example 包中创建名为NavigationTest.java的类,并使用 TestNG 注解如@Test@BeforeMethod@AfterMethod,以及beforeMethodafterMethod方法:

  1. 使用以下代码修改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注解标记的是测试方法。我们将通过 WebDriver 接口的get()方法传递应用程序的 URL。这将导航到浏览器中的网站。我们将通过调用 TestNG 的Assert.assertEquals方法和 WebDriver 接口的getTitle()方法来检查页面标题。

最后,afterMethod()@AfterMethod TestNG 注解标记,将关闭浏览器窗口。

我们需要从 sites.google.com/a/chromium.org/chromedriver/downloads 下载并复制 chromedriver 可执行文件。根据您计算机上安装的 Google Chrome 浏览器版本以及操作系统下载适当的版本。将可执行文件复制到/src/test/resources/drivers文件夹中。

要运行测试,请在代码编辑器中右键单击并选择“运行 As | 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"/>

在前面的代码中,typename是具有textUsername值的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)树,页面显示的元素定义了哪些属性或属性,以及应用程序如何通过浏览器使用为页面编写的 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)。这将显示一个搜索框。只需输入XPathCSS Selector,匹配的元素将在树中突出显示,如下面的截图所示:

Chrome 开发者工具还提供了一个功能,您可以通过在树中右键单击所需的元素并从弹出菜单中选择“复制 XPath”选项来获取元素的 XPath。

与 Mozilla Firefox 和 Google Chrome 类似,您将在任何主要浏览器中找到类似的开发者工具,包括 Microsoft Internet Explorer 和 Edge。

浏览器开发者工具在测试脚本开发过程中非常有用。这些工具可以帮助您找到需要交互的元素的位置详情,作为测试的一部分。这些工具解析页面代码,并以层次树的形式显示信息。

网页上的 WebElements 可能不会声明所有属性。选择用于在网页上唯一标识 WebElement 的属性的测试脚本的开发者。

使用By定位机制

By是通过findElement()方法或findElements()方法传递给定位机制,以在网页上获取相应的 WebElement(s)。有八种不同的定位机制;也就是说,有八种不同的识别方式

在网页上的 HTML 元素。它们通过 ID、Name、ClassName、TagName、LinkText、PartialLinkText、XPath 和 CSS Selector 定位。

By.id()方法

在网页上,每个元素都通过一个 ID 属性唯一标识,该属性是可选的。ID 可以由 Web 应用程序的开发者手动分配,或者由应用程序动态生成。动态生成的 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 文件中声明的所有样式。现在,让我们在我们的主页上尝试这个操作。我们将尝试使用按钮的类名来让 WebDriver 识别搜索按钮并点击它。

首先,为了获取搜索按钮的类名,正如我们所知,我们将使用开发者工具来获取它。获取后,将定位机制更改为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属性指定了buttonsearch-button值:

<button type="submit" title="Search" class="button search-button"><span><span>Search</span></span></button>

我们必须使用class属性的某个值与By.className方法一起使用。在这种情况下,我们可以使用buttonsearch-button中的任何一个,只要它能唯一标识元素即可。

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 代码看起来是这样的:

这个我的账户是链接文本。所以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定位机制来识别我的账户链接。

linkTextpartialLinkText方法是区分大小写的。

By.partialLinkText()方法

By.partialLinkText定位机制是By.linkText定位器的扩展。如果你不确定整个链接文本或者只想使用链接文本的一部分,你可以使用这个定位器来识别链接元素。所以,让我们修改之前的例子,只使用链接的部分文本;在这种情况下,我们将使用网站页脚中的隐私政策链接中的 Privacy:

代码看起来是这样的:

@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标签名搜索元素,它将导致多个 WebElements,因为主页面上有九个按钮。因此,在尝试使用标签名定位元素时,始终建议使用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'],这将返回锚元素。该元素位于根元素内部的第三个级别,并且具有 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 定位机制稍微快一些。以下是一些常用的语法,用于识别元素:

  • 要使用具有 #flrs ID 的 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 属性标识的锚元素。

与 WebElements 交互

在上一节中,我们看到了如何通过使用不同的定位方法在网页上定位 WebElements。在这里,我们将看到可以在 WebElement 上执行的所有不同用户操作。不同的 WebElements 将有不同的操作可以执行。例如,在一个文本框元素中,我们可以输入一些文本或清除已经输入的文本。同样,对于按钮,我们可以点击它,获取它的尺寸等,但我们不能在按钮中输入文本,对于链接,我们也不能在它里面输入文本。所以,尽管所有操作都列在了一个 WebElement 接口中,但使用目标元素支持的操作是测试脚本开发者的责任。如果我们尝试在 WebElement 上执行错误操作,我们不会看到任何异常或错误抛出,也不会看到任何操作被执行;WebDriver 会静默忽略这些操作。

现在,让我们通过查看它们的 Javadocs 和代码示例,逐个分析每个操作。

获取元素属性和属性

在本节中,我们将学习从 WebElement 接口检索值和属性的各种方法。

getAttribute() 方法

getAttribute 方法可以在所有 WebElements 上执行。记住,我们在 WebElements 部分已经看到了 WebElement 的属性。HTML 属性是 HTML 元素的修饰符。它们通常是出现在元素起始标签中的键值对。例如:

  <label name="Username" id="uname">Enter Username: </label>

在前面的代码中,nameid 是属性或属性键,而 Usernameuname 是属性值。

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 搜索框的 nameidclassplaceholder 属性的属性值。前面代码的输出将是以下内容:

 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-familybackground-colorcolor 等。当您想通过测试脚本验证应用于您的 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 中调用。这将返回 WebElement 的 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操作适用于textboxtextareaHTML 元素。这用于将文本输入到文本框中。这将模拟用户键盘,并将文本输入到 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()方法

清除操作类似于sendKeys()方法,适用于textboxtextarea元素。这用于使用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() 操作可以在 FormForm 元素内的元素上执行。这是用来将网页表单提交给托管 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() 方法,比如 About 链接,它不是任何表单的一部分。我们应该看到会抛出 NoSuchElementException。所以,当你在一个 WebElement 上使用 submit() 方法时,请确保它属于 Form 元素。

检查 WebElement 状态

在前面的章节中,我们学习了如何检索值和在对 WebElements 执行操作。现在,我们将了解如何检查 WebElement 的状态。我们将探讨检查 WebElement 是否在浏览器窗口中显示、是否可编辑,以及如果 WebElement 是单选按钮或复选框,我们可以确定它是选中还是未选中的方法。让我们看看如何使用 WebElement 接口中的可用方法。

isDisplayed() 方法

isDisplayed 操作验证元素是否在网页上显示,并且可以在所有 WebElements 上执行。isDisplayed() 方法的 API 语法如下:

boolean isDisplayed()

前面的方法返回一个 Boolean 值,指定目标元素是否在网页上显示。以下是对搜索框是否显示的验证代码,在这种情况下显然应该返回 true:

@Test
public void elementStateExample() {
    WebElement searchBox = driver.findElement(By.name("q"));
    System.out.println("Search box is displayed: "
            + searchBox.isDisplayed());
}

前面的代码使用了 isDisplayed() 方法来确定元素是否在网页上显示。前面的代码对搜索框返回 true

Search box is displayed: true

isEnabled() 方法

isEnabled 操作验证元素是否在网页上启用,并且可以在所有 WebElements 上执行。isEnabled() 方法的 API 语法如下:

boolean isEnabled()

前面的方法返回一个 Boolean 值,指定目标元素是否在网页上启用。以下是对搜索框是否启用的验证代码,在这种情况下显然应该返回 true:

@Test
public void elementStateExample() {
    WebElement searchBox = driver.findElement(By.name("q"));
    System.out.println("Search box is enabled: "
            + searchBox.isEnabled());
}

前面的代码使用了 isEnabled() 方法来确定元素是否在网页上启用。前面的代码对搜索框返回 true:

Search box is enabled: true 

isSelected() 方法

如果一个元素在网页上被选中,isSelected方法返回一个boolean值,并且只能在单选按钮、选择框中的选项和复选框 WebElement 上执行。在其他元素上执行时,它将返回falseisSelected()方法的 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 上的测试。

问题

  1. 对错:Selenium 是一个浏览器自动化库吗?

  2. Selenium 提供了哪些不同类型的定位机制?

  3. 对错:使用getAttribute()方法,我们也可以读取 CSS 属性吗?

  4. 我们可以在一个 WebElement 上执行哪些操作?

  5. 我们如何确定复选框是被选中还是未选中?

更多信息

您可以通过以下链接获取更多关于本章涵盖主题的信息:

  • www.w3.org/TR/webdriver/阅读 WebDriver 规范。

  • 在《精通 Selenium WebDriver》一书中,由 Mark Collin 著,Packt Publishing 出版,您可以了解更多关于在第一章,创建更快的反馈循环中使用 TestNG 和 Maven 的内容。

  • 在《Selenium 测试工具食谱》第 2 版中,由 Unmesh Gundecha 著,Packt Publishing 出版的书中,您可以了解更多关于元素交互的内容,包括第二章“寻找元素”和第三章“与元素一起工作”。

第二章:可用的不同 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 的特定驱动程序实现。

  • 使用浏览器选项类以 Headless 模式执行测试并使用自定义配置文件。

  • 使用 Google Chrome 进行移动仿真。

Firefox 驱动程序。

Selenium 3.0 中对 Firefox Driver 的实现进行了更改。从 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 窗口将被关闭。

使用 Headless 模式。

Headless 模式是使用 Selenium WebDriver 进行自动化测试时运行 Firefox 的一种非常有用的方式。在 Headless 模式下,Firefox 运行得像正常一样,只是你看不到 UI 组件。这使得 Firefox 运行更快,测试运行更高效,尤其是在 CI(持续集成)环境中。

我们可以通过配置FirefoxOptions类以 Headless 模式运行 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 的说法,以下是可以存储在配置文件中的不同属性:

  • 书签和浏览历史

  • 密码

  • 站点特定首选项

  • 搜索引擎

  • 个人词典

  • 自动完成历史

  • 下载历史

  • Cookies

  • DOM 存储

  • 安全证书设置

  • 安全设备设置

  • 下载操作

  • 插件 MIME 类型

  • 存储会话

  • 工具栏自定义

  • 用户样式

要创建、重命名或删除配置文件,您必须执行以下步骤:

  1. 打开 Firefox 配置文件管理器。为此,在命令提示符终端中,导航到 Firefox 的安装目录;通常,如果您在 Windows 上,它位于程序文件中。导航到可以找到firefox二进制文件的位置,并执行以下命令:
      /path/to/firefox -p

它将打开配置文件管理器,看起来如下面的截图所示:

图片

注意,在执行上述命令之前,您需要确保关闭所有当前运行的 Firefox 实例。

  1. 使用“创建配置文件...”按钮创建另一个配置文件,使用“重命名配置文件...”按钮重命名现有配置文件,使用“删除配置文件...”按钮删除一个。

因此,回到我们的 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 信息的 String。

现在,要创建具有相同配置文件的浏览器,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 配置文件,以及我们如何为 Firefox Driver 创建自己的自定义配置文件。现在,让我们看看我们如何在创建的配置文件中设置我们的首选项以及 FirefoxDriver 将它们存储在哪里。

根据 Mozilla 的说法,Firefox 首选项是任何可以由用户设置的值或定义的行为。这些值被保存到首选项文件中。如果你通过导航到帮助 | 故障排除信息并点击显示文件夹按钮来打开配置文件目录,你会看到两个首选项文件:prefs.jsuser.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);

这个 Firefox Driver 将它们视为冻结首选项,不允许测试脚本开发者更改它们。然而,前面列表中有一些首选项是 FirefoxDriver 允许你更改的,我们很快就会看到。

设置首选项

现在我们将学习如何设置我们自己的偏好。作为一个例子,我们将看到如何更改浏览器的用户代理。这些天,许多网络应用都有一个主站以及一个移动站/m.站。应用将验证传入请求的用户代理,并决定是否作为普通站或移动站的服务器。因此,为了从你的笔记本电脑或桌面浏览器测试你的移动站,你只需更改你的用户代理。让我们看一个代码示例,我们可以使用 FirefoxDriver 更改我们 Firefox 浏览器的 user-agent 偏好,并向 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_certswebdriver_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,现在如果我们打开 Firefox 实例的配置目录中的 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。以下截图显示了我们的示例应用程序在 iPhone 的 Chrome 中的显示方式。我们可以在 Chrome 浏览器中使用以下步骤开始移动模拟:

  1. 在 Chrome 浏览器中导航到示例网络应用程序:

图片

  1. 打开开发者工具。选择蓝色移动设备图标,然后选择设备。在这个例子中,我们选择了 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,它将包含 deviceMetricsuserAgent 字符串。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/ 下载 Internet Explorer 的 IEDriver 服务器可执行文件。

然后,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 有许多其他功能。以下是一个列表,解释了为什么使用它:

功能 要设置的值 目的
初始浏览器 URL URL,例如,www.google.com 此功能使用 URL 值设置驱动程序在打开浏览器时应导航到的浏览器。
通过忽略安全域来引入易变性 是或否 这定义了 IEDriverServer 是否应该忽略浏览器安全域设置。
使用原生事件 是或否 这告诉 IEDriver 服务器在执行鼠标或键盘操作时是使用原生事件还是 JavaScript 事件。
需要窗口焦点 是或否 如果设置为 True,IE 浏览器窗口将获得焦点。这在执行原生事件时特别有用。
启用持久悬停 是或否 如果设置为 True,IEDriver 将持续触发鼠标悬停事件。这在克服 IE 处理鼠标悬停事件的问题上尤为重要。
确保 IE 会话清洁 是或否 如果为 True,它将清除 IE 所有实例的所有 cookies、缓存、历史记录和保存的表单数据。
通过服务器设置 IE 代理 是或否 如果为 True,则使用 IEDriver 服务器上的代理服务器设置。如果为 False,则使用 WindowsProxyManager 来确定代理服务器。

Edge 驱动

Microsoft Edge 是与 Microsoft Windows 10 一起推出的最新网络浏览器。Microsoft 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/)):

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 属性指定可执行文件的路径,这与我们在本章前面看到的其他浏览器配置类似。

我们还使用 EdgeOptions 类将页面加载策略设置为 eager:

EdgeOptions options = new EdgeOptions();
options.setPageLoadStrategy("eager");

当导航到新的页面 URL 时,Selenium WebDriver 默认会在将控制权传递给下一个命令之前等待页面完全加载。这在大多数情况下都很好,但在需要加载大量第三方资源的页面上可能会导致长时间的等待。使用 eager 页面加载策略可以使测试执行更快。eager 页面加载策略将等待直到 DOMContentLoaded 事件完成,即仅下载和解析 HTML 内容,但其他资源,如图片,可能仍在加载。然而,这可能会引入元素动态加载时的不稳定性。

Safari 驱动器

随着 Selenium 3.0 和 WebDriver 成为 W3C 标准,Apple 现在在浏览器中内置了 SafariDriver。我们不需要单独下载它。但是,为了与 Selenium WebDriver 一起使用,我们必须从 Safari 的主菜单设置 Develop | 允许远程自动化选项,如下面的截图所示:

图片

允许远程自动化

编写 Safari 浏览器的第一个测试脚本

这很简单。以下是用 Safari 驱动器编写的测试脚本:

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。

问题

  1. WebDriver 成为 W3C 规范的意义是什么?

  2. 对或错:WebDriver 是一个接口?

  3. 哪些浏览器支持无头测试?

  4. 我们如何使用 Chrome 测试移动网站?

更多信息

您可以查看以下链接以获取有关本章涵盖主题的更多信息:

第三章:使用 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() 方法从集合中获取数据流。例如,我们有一个在标题部分显示的示例网络应用程序支持的编程语言下拉列表。让我们将其捕获到一个 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);
}

使用 streams API,我们可以通过在 languages 数组列表上调用 .stream() 方法来获取数据流,并按以下方式打印成员:

languages.stream().forEach(System.out::println);

获取数据流后,我们调用了 forEach() 方法,传递了对每个元素想要执行的操作,即使用 System.out.println 方法在控制台上输出成员值。

一旦从集合中获取了数据流,我们就可以使用该数据流来处理集合的元素或成员。

Stream.filter()

我们可以使用 filter() 方法来过滤数据流。以下代码展示了如何从 languages 列表中获取的数据流中过滤以 E 开头的项目:

stream.filter( item -> item.startsWith("E") ); 

filter() 方法接受一个谓词(Predicate)作为参数。谓词接口包含一个名为 boolean test(T t) 的函数,它接受一个参数并返回一个布尔值。在先前的示例中,我们将 lambda 表达式 item -> item.startsWith("E") 传递给了 test() 函数。

当在数据流上调用 filter() 方法时,传递给 filter() 函数的过滤条件将被内部存储。项目不会立即被过滤。

传递给 filter() 函数的参数决定了数据流中哪些项目应该被处理以及哪些应该被排除。如果 Predicate.test() 函数对一个项目返回 true,则意味着它应该被处理。如果返回 false,则该项目不会被处理。在先前的示例中,test() 函数将对所有以字符 E 开头的项目返回 true

Stream.sort()

我们可以通过调用 sort() 函数来对数据流进行排序。以下代码展示了如何使用 sort() 函数对 languages 列表进行排序:

languages.stream().sorted();

这将按字母顺序对元素进行排序。我们可以提供一个 lambda 表达式来使用自定义比较逻辑对元素进行排序。

Stream.map()

Streams 提供了一个 map() 方法,将流中的元素映射到另一种形式。让我们拿之前的例子,将语言列表的元素转换为大写,如下所示:

languages.stream().map(item -> item.toUpperCase());

这将把语言集合中所有的字符串元素映射到它们的 uppercase 等效值。同样,这实际上并没有执行映射;它只是为映射配置了流。一旦调用了其中一个流处理方法,映射(和过滤)将会被执行。

Stream.collect()

Streams 提供了 collect() 方法,作为 Stream 接口上的其他方法之一,用于流处理。当调用 collect() 方法时,将执行过滤和映射,并且那些操作的结果对象将被收集。让我们拿之前的例子,获取一个新的语言 uppercase 列表,如下面的代码所示:

List<String> upperCaseLanguages = languages.stream()
        .map(item -> item.toUpperCase())
        .collect(Collectors.toList());

System.out.println(upperCaseLanguages);

此示例创建了一个流,添加了一个映射来将字符串转换为 uppercase,并收集所有对象到一个新的列表中。我们也可以使用过滤或排序方法,并根据过滤方法中应用的条件收集结果列表。

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));

我们可以通过传递比较属性(在这种情况下,是价格),使用 .getPrice() 方法来调用 .min() 函数。.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

使用 Stream API 与 Selenium WebDriver

现在我们已经介绍了 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 的代码。这有助于你以函数式编程风格编写代码,使其更加流畅和易读。Stream 对于处理 WebElements 列表非常有用。我们可以通过 Stream 轻松地收集和过滤数据。

在下一章中,我们将探讨 WebDriver 的截图、窗口和框架处理、同步以及管理 cookie 的功能。

问题

  1. 哪个版本的 Java Streams API 被引入?

  2. 解释 Streams API 中的 filter 函数。

  3. Streams API 中的哪种方法会从 filter()函数返回匹配元素的数量?

  4. 我们可以使用map()函数通过属性值过滤 WebElements 列表:对还是错?

更多信息

你可以查看以下链接,了解更多关于本章所涵盖主题的信息:

第四章:探索 WebDriver 的功能

到目前为止,我们已经探讨了用户可以使用 WebDriver 在网页上执行的各种基本和高级交互。在本章中,我们将讨论 WebDriver 的不同功能和特性,这些功能和特性使测试脚本开发者能够更好地控制 WebDriver,从而更好地控制正在测试的 Web 应用程序。本章将要涵盖的功能如下:

  • 截图

  • 定位目标窗口和 iFrames

  • 探索导航

  • 等待 Web 元素加载

  • 处理 Cookie

让我们立即开始,不要有任何延迟。

截图

在 WebDriver 库中,对网页进行截图是一个非常有用的功能。当测试用例失败,你想要查看测试用例失败时应用程序的状态时,这非常有用。WebDriver 库中的 TakesScreenShot 接口由所有不同的 WebDriver 变体实现,例如 Firefox Driver、Internet Explorer Driver、Chrome Driver 等。

TakesScreenShot 功能在所有浏览器中默认启用。由于这是一个只读功能,用户无法切换它。在我们查看使用此功能的代码示例之前,我们应该看看 TakesScreenShot 接口的一个重要方法——getScreenshotAs().

getScreenshotAs() 的 API 语法如下:

public X getScreenshotAs(OutputType target)           

在这里,OutputType 是 WebDriver 库的另一个接口。我们可以要求 WebDriver 以三种不同的格式输出截图:BASE64BYTES(原始数据)和 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() 方法来截取网页截图并将其保存到文件格式。我们可以从目标文件夹中打开保存的图像并检查它。

定位目标窗口和 Frames

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(): 这相当于对话框上的 OK 按钮操作。当在对话框上执行 accept() 操作时,将调用相应的 OK 按钮操作。

  • void dismiss(): 这相当于点击 CANCEL 操作按钮。

  • 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 为开发者提供的用于模拟一些浏览器操作的各种方法。

等待 Web 元素加载

如果你之前有 UI 自动化经验,我敢肯定你遇到过这样的情况:你的测试脚本因为网页仍在加载而无法在网页上找到元素。这可能是由于各种原因造成的。一个经典的例子是当应用服务器或 Web 服务器由于资源限制而响应页面太慢时;另一种情况可能是当你在一个非常慢的网络中访问页面时。原因可能是当你的测试脚本尝试找到它时,网页上的元素尚未加载。这就是你必须计算和配置测试脚本等待 Web 元素在网页上加载的平均等待时间的地方。

WebDriver 为测试脚本开发者提供了一个非常实用的功能来管理等待时间。"等待时间"是指你的驱动程序在放弃并抛出NoSuchElementException之前,将等待 Web 元素加载的时间。记住,在第一章介绍 WebDriver 和 Web 元素中,我们讨论了findElement(By by)方法,当它无法找到目标 Web 元素时会抛出NoSuchElementException

你可以通过两种方式让 WebDriver 等待 WebElement。它们是隐式等待时间显式等待时间。隐式超时对所有 Web 元素都是通用的,并且与它们关联着一个全局超时周期,但显式超时可以配置为单个 Web 元素。让我们在这里讨论每一个。

隐式等待时间

当你想为正在测试的应用程序配置 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的方法,在这里你指定当在网页上搜索一个 WebElements 时,如果它不是立即出现,驱动器应该等待多长时间。WebDriver 会定期在网页上轮询 WebElements,直到之前方法指定的最大等待时间结束。在前面的代码中,10 秒是驱动器将等待任何 WebElements 在浏览器上加载的最大时间。如果在这个时间段内加载,WebDriver 将继续执行其余代码;否则,它将抛出NoSuchElementException

当你想指定一个最大等待时间时,请使用此方法,这在你的 Web 应用程序中的大多数 WebElements 中通常是常见的。影响你页面性能的各种因素包括网络带宽、服务器配置等等。基于这些条件,作为你的 WebDriver 测试用例的开发者,你必须确定一个最大隐式等待时间的值,以确保你的测试用例不会执行得太久,同时也不会频繁超时。

显式等待时间

隐式超时适用于网页上的所有 WebElements。但是,如果你有一个特定的 WebElements 在你的应用程序中,你希望等待非常长的时间,这种方法可能不起作用。将隐式等待时间设置为这个非常长的时间段将会延迟整个测试套件的执行。因此,你必须为特定的情况,比如这个 WebElements,做出例外。为了处理这类场景,WebDriver 为 WebElements 提供了一个显式等待时间。

那么,让我们看看如何使用以下代码使用 WebDriver 等待特定的 WebElements:

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");

理想情况下,此行应该在我们将 cookies 设置到驱动程序之后可见。但它在顶部的原因是 WebDriver 不允许你直接将 cookies 设置到这个会话中,因为它将这些 cookies 视为来自不同域的。尝试删除上一行代码并执行它,你就会看到错误。所以,最初,你将尝试访问主页,将驱动程序的域值设置为应用服务器域,并加载所有 cookies。当你执行此代码时,最初你会看到应用的主页。

因此,通过使用 WebDriver 的 cookies 功能,你可以避免在服务器上输入用户名和密码,并且为每次测试再次验证它们,从而节省大量时间。

摘要

在本章中,我们讨论了 WebDriver 的各种功能,例如捕获屏幕截图和处理WindowsFrames。我们还讨论了同步的隐式和显式等待条件,并使用了导航和 cookies API。使用这些功能将帮助你更有效地测试目标 Web 应用,通过设计更创新的测试框架和测试用例。在下一章中,我们将探讨Actions API,以使用键盘和鼠标事件执行用户交互。

问题

  1. 我们可以使用哪些不同的格式来输出屏幕截图?

  2. 我们如何使用 Selenium 切换到另一个浏览器标签页?

  3. 对或错:defaultContent()方法将切换到之前选定的框架。

  4. Selenium 提供了哪些导航方法?

  5. 我们如何使用 Selenium 添加一个 cookie?

  6. 解释隐式等待和显式等待之间的区别。

更多信息

你可以查看以下链接,获取本章涵盖主题的更多信息:

第五章:探索 WebDriver 的高级交互

在上一章中,我们讨论了 WebDriver 接口及其功能,包括截图、与窗口、框架、警报、cookie 和测试同步。在本章中,我们将介绍一些在 Web 元素上执行动作的高级方法。我们将学习如何使用 Selenium WebDriver 的动作 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轴向下移动,负值用于将光标向上移动。

xOffSetyOffSet的值导致光标移出文档时,会抛出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 像素的边框。边框是一个 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() 方法提供的 xy 偏移量实际上就是第 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;
}

在前面的样式代码中,我们关注于偏移移动的三个元素是:heightwidthborder 厚度。在这里,height 值是 80pxwidth 值是 100pxborder 值是 1px。使用这三个因素来计算从一个瓦片导航到另一个瓦片的偏移量。请注意,任何两个瓦片之间的边框厚度将产生 2 px,即每个瓦片 1 px。以下使用 moveByOffsetclick() 方法从瓦片 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)

此方法的输入参数是应该执行 click 操作的 WebElement 实例。此方法,就像 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 提供标识符的好理由。

如果你观察了之前的 moveByOffsetclick 方法的示例,所有移动鼠标和点击瓦片 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() 方法是动作类中的另一种方法,它会在元素上执行左键点击并保持,而不释放鼠标左键。当执行拖放等操作时,此方法将非常有用。这是动作类提供的 clickAndHold() 方法的变体之一。我们将在下一节讨论其他变体。

现在打开书中附带 的 Sortable.html 文件。你可以看到瓷砖可以从一个位置移动到另一个位置。现在让我们尝试将瓷砖 3 移动到瓷砖 2 的位置。完成此操作所涉及的步骤如下:

  1. 将光标移动到瓷砖 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,其中应该放下持有的 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()

显然,前面的方法不接收任何输入参数,因为它只是点击当前光标位置并返回一个动作类实例。让我们看看如何将前面的代码转换为使用此方法:

@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 文件,并 单次 点击 Click Me 按钮。你不应该看到任何动作发生。现在双击按钮;你应该看到一个提示说双击了 !!。现在我们将尝试使用 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();
}

在前面的代码中,我们首先使用 contextClick() 方法在 contextMenu WebElement 上进行了右键点击,然后从上下文菜单中点击了第 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类中有三种特定于键盘的不同动作,它们是keyUpkeyDownsendKeys动作,每个动作都有两个重载方法。一个方法是在WebElement上直接执行动作,另一个方法是不考虑其上下文来执行方法。

keyDownkeyUp动作

keyDown()方法用于模拟按下并保持键的动作。我们在这里引用的键是ShiftCtrlAlt键。keyUp()方法用于释放使用keyDown()方法按下的键。keyDown()方法的 API 语法如下:

public Actions keyDown(Keys theKey) throws IllegalArgumentException

当传入的键不是ShiftCtrlAlt键之一时,会抛出IllegalArgumentException异常。keyUp()方法的 API 语法如下:

public Actions keyUp(Keys theKey)

在一个键上执行keyUp动作,而该键上尚未执行keyDown动作,可能会导致一些意外的结果。因此,我们必须确保在执行keyDown动作之后执行keyUp动作。

sendKeys方法

这用于在 Web 元素(如文本框、文本区域等)中输入字母数字和特殊字符键。这与WebElement.sendKeys(CharSequence keysToSend)方法不同,因为这个方法期望在调用之前 Web 元素已经获得焦点。sendkeys()方法的 API 语法如下:

public Actions sendKeys(CharSequence keysToSend)

我们期望你使用keyUpkeyDownsendKeys()方法编写一些关于这些键盘事件的测试脚本。

摘要

在本章中,我们学习了如何使用动作类创建一系列动作,并将它们构建成一个组合动作,通过perform()方法一次性执行。这样,我们可以将一系列复杂的用户动作聚合到一个单一的功能中,并一次性执行。在下一章中,我们将学习 WebDriver 事件以及如何使用 WebDriver 监听和执行高级动作。

问题

  1. 对或错——拖放动作需要源元素和目标元素。

  2. 列出我们可以使用 actions API 执行的键盘方法。

  3. 哪个 actions API 方法可以帮助执行双击操作?

  4. 使用 actions API,我们如何执行保存选项(即Ctrl + S)?

  5. 我们如何使用 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.javaIAmTheEventListener2.java 类。这将是一个将所有事件分发的类:

EventFiringWebDriver eventFiringDriver = 
        new EventFiringWebDriver(driver);
IAmTheEventListener eventListener = 
        new IAmTheEventListener();

将 EventListener 注册到 EventFiringWebDriver

为了让 EventListener 通知事件执行,我们已经将 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 类的 navigateTonavigateBack 事件之前和之后记录。修改后的方法如下:

@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.javaIAmTheListener2.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 值变化

当在sendKeys()clear()方法上执行时,如果 WebElement 的值发生变化,就会发生此事件。与此事件相关联有两种方法:

public void beforeChangeValueOf(WebElement element, WebDriver driver)

在 WebDriver 尝试更改 WebElement 的值之前,会调用先前的方法。作为参数,会将 WebElement 本身传递给该方法,以便在更改之前记录元素的值:

public void afterChangeValueOf(WebElement element, WebDriver driver)

先前的方法是与值更改事件相关联的第二个方法,它在 WebDriver 更改 WebElement 的值之后被调用。同样,将 WebElement 和 WebDriver 作为参数发送给该方法。如果在更改值时发生异常,则不会调用此方法。

监听被点击的 WebElement

当通过执行webElement.click()来点击 WebElement 时,会发生此事件。在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)

正如在所有after <<event>>方法中一样,当触发导航后退操作时,前面的方法会被调用。前面的两个方法将在浏览器导航无关的情况下被调用;也就是说,如果浏览器没有任何历史记录,你调用此方法,浏览器不会带你到任何历史记录。但是,即使在那种情况下,由于事件被触发,这两个方法也会被调用。

监听浏览器向前导航

此事件与浏览器后退导航非常相似,不同之处在于这是浏览器向前导航,因此它使用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 通过使用 JavaScript 访问window.performance对象的计时接口属性来访问。我们将每次导航到页面时都捕获页面加载时间。这可以通过在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时间,并计算它们之间的差异,这将给我们页面加载时间。

摘要

在本章中,您学习了EventFiringWebDriverEventListeners,以及它们如何协同工作,通过帮助开发者调试在测试用例执行过程中的每一步来简化开发者的工作。您还学习了如何使用 WebDriver 事件在页面上执行不同类型的测试,例如无障碍性和客户端性能检查。在下一章节中,您将学习更多关于 RemoteWebDriver 的内容,它用于在分布式和并行模式下在远程机器上运行测试,以进行跨浏览器测试。

问题

  1. 您可以使用WebDriverEventListener接口来监听 WebDriver 事件——对还是错?

  2. 您可以使用WebDriverEventListener在调用sendKeys方法之前自动清除输入字段吗?

  3. Selenium 支持无障碍测试——对还是错?

更多信息

您可以通过以下链接获取更多关于本章涵盖主题的信息:

第七章:探索 RemoteWebDriver

到目前为止,我们已经创建了测试用例,并尝试在各个浏览器上执行它们。所有这些测试都是针对安装在测试用例所在本地机器上的浏览器执行的。这并不总是可能的。有很大可能性,您可能正在使用 Mac 或 Linux,但想在 Windows 机器上的 IE 上执行测试。在本章中,我们将学习以下主题:

  • 使用 RemoteWebDriver 在远程机器上执行测试用例

  • JSON 线协议的详细解释

介绍 RemoteWebDriver

RemoteWebDriverWebDriver 接口的一个实现类,测试脚本开发者可以使用它通过远程机器上的 Selenium Standalone 服务器执行他们的测试脚本。RemoteWebDriver 有两个部分:服务器和客户端。在我们开始使用它们之前,让我们回顾一下我们之前做了什么。

以下图表解释了我们到目前为止所做的工作:

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

测试脚本位于本地机器上,而浏览器安装在远程机器上。在这种情况下,RemoteWebDriver 就派上用场了。如前所述,与 RemoteWebDriver 相关有两个组件:服务器和客户端。让我们从 Selenium Standalone 服务器 开始。

理解 Selenium Standalone 服务器

Selenium Standalone Server 是一个组件,它监听端口以接收来自 RemoteWebDriver 客户端的多种请求。一旦它收到请求,它将它们转发到以下任何一个:Chrome 驱动程序、IE 驱动程序或 Firefox 的 Gecko 驱动程序,具体取决于 RemoteWebDriver 客户端的请求。

下载 Selenium Standalone 服务器

让我们下载 Selenium Standalone Server 并开始运行它。您可以从 www.seleniumhq.org/download/ 下载它,但出于我们的目的,让我们下载特定版本,因为我们正在使用 WebDriver 版本 3.12.0。这个服务器 JAR 应该下载到包含浏览器的远程机器上。同时,请确保远程机器上已安装 Java 运行时。

运行服务器

在远程机器上打开您的命令行工具,导航到您下载 JAR 文件的目录。现在,要启动 Selenium Standalone 服务器,请执行以下命令:

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/hubRemoteWebDriver实例,其中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 参数来更改默认端口。

它将显示服务器当前正在处理的会话的整个列表。以下是这个截图:

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

您可以通过悬停在 Capabilities 链接上查看弹出窗口,如下面的截图所示:

这些是服务器为该会话隐式设置的默认期望能力。现在我们已经成功地在我们的测试脚本(在一台机器上使用 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 ServerRemoteWebDriver客户端在另一台机器上远程执行测试脚本。这使得 Selenium WebDriver 测试可以在具有不同浏览器和操作系统组合的远程机器上执行。我们还探讨了 JSON 线协议以及客户端库如何在幕后发送和接收请求以及响应。

在下一章中,我们将扩展 Selenium Standalone Server 和 RemoteWebDriver 的使用,以创建一个用于跨浏览器和分布式测试的 Selenium Grid。

问题

  1. 使用 Selenium,我们可以在远程机器上执行测试——对还是错?

  2. 用于在远程机器上运行测试的驱动程序类是哪个?

  3. 解释期望能力。

  4. Selenium 测试与 Selenium Standalone Server 之间使用什么协议?

  5. Selenium Standalone Server 默认使用哪个端口?

更多信息

您可以通过以下链接获取本章涵盖主题的更多信息:

第八章:设置 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 的工作原理,接下来让我们看看它中的中心节点和节点是如何工作的。幸运的是,因为我们正在处理 Selenium Grid,所以我们可以使用与上一章相同的 Remote WebDriver 服务器来作为 Selenium Grid 使用。如果你还记得,我们使用 seleniumserver-standalone-3.12.0.jar 来启动 Selenium Standalone Server。我们可以使用相同的 JAR 文件在中心节点机器上以中心节点模式启动,并在节点机器上启动 JAR 文件的副本以节点模式运行。尝试在你的 JAR 文件上执行以下命令:

java –jar selenium-server-standalone-3.12.0.jar –help

以下输出显示了如何在网格环境中使用服务器:

您将看到两个选项:将其用作独立服务器,它作为远程 WebDriver 运行,以及将其用于网格环境,这描述了 Selenium Grid。在本章中,我们将使用此 JAR 文件作为 Selenium Grid。

理解中心节点

中心节点是 Selenium Grid 的中心点。它有一个所有已连接节点和特定网格的注册表。中心节点是一个以中心节点模式运行的 Selenium Standalone 服务器,默认情况下监听机器的 4444 端口。测试脚本将尝试连接到该端口的中心节点,就像任何 Remote 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 端口上启动的 Grid 中心节点的控制台输出:

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

您在浏览器上应该看到的是以下截图所示:

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

如您所见,页面讨论了许多配置参数。我们将在 配置 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();
    }
} 

现在尝试执行前面的测试脚本并观察中心节点和节点的日志输出。中心节点的输出日志如下:

在中心节点发生的步骤顺序如下:

  1. 中心节点收到创建新会话的请求,platform=MAC, browserName=chrome

  2. 它验证与capabilities请求匹配的可用节点。

  3. 如果可用,它将使用节点主机创建一个新的会话;如果没有,它将拒绝测试脚本的请求,表示期望的能力不匹配任何已注册的节点。

  4. 如果在前一步中与节点主机创建了会话,则创建一个新的测试槽会话并将测试脚本交给节点。同样,你应在中心节点的控制台日志中看到以下输出:

在节点上执行的步骤顺序如下:

  1. 节点主机使用请求的期望能力创建一个新的会话。这将启动浏览器。

  2. 它在启动的浏览器上执行测试脚本的步骤。

  3. 它结束会话并将结果转发到中心节点,中心节点随后将其发送到测试脚本。

请求非注册的能力

当测试脚本请求中心节点未注册的能力时,中心节点将拒绝请求。让我们修改前面的测试脚本,请求 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 的 hub 和节点。第一种就是我们一直看到的方法;即通过命令行指定配置参数。第二种方法是提供一个包含所有这些配置参数的 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"
  }
}

一旦这些文件配置完成,可以使用以下命令将它们提供给节点和 hub:

java -jar selenium-server-standalone-3.12.0.jar -role node -nodeConfig nodeconfig.json 

这样,你可以使用 JSON 文件指定你的 hub 和节点的配置。

使用基于云的网格进行跨浏览器测试

要设置一个用于跨浏览器测试的 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 Grid,如下面的代码示例所示:

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 的 hub 并请求所需的操作系统和浏览器配置。Sauce Labs 的云管理软件会自动分配一个虚拟机,以便在我们的测试配置上运行。我们可以在以下截图所示的仪表板上监控这个运行:

截图

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

图片

Selenium 详情窗口

您还可以通过使用 Sauce Connect 实用程序来测试安全托管在内部服务器上的应用程序。Sauce Connect 在您的机器和 Sauce 云之间创建一个安全隧道。

摘要

在本章中,我们学习了 Selenium Grid,了解了 hub 和 node 的工作方式,更重要的是,学习了如何配置您的 Selenium Grid 以更好地控制环境和基础设施。Selenium Grid 将通过覆盖操作系统和浏览器的组合来为应用程序提供跨浏览器测试。我们还看到了如何使用云服务,如 Sauce Labs,在远程云环境中执行测试。

在下一章节中,我们将学习如何使用页面对象模式创建可重用和模块化的测试。

问题

  1. 哪个参数可以用来指定节点可以支持多少浏览器实例?

  2. 解释如何使用 Selenium Grid 来支持跨浏览器测试。

  3. 使用 RemoteWebDriver 运行 Selenium Grid 测试需要指定哪个 URL?

  4. Selenium Grid Hub 充当负载均衡器——对还是错?

更多信息

您可以查看以下链接以获取有关本章涵盖主题的更多信息:

第九章:页面对象模式

到目前为止,我们已经看到了 WebDriver 的各种 API,并学习了如何使用它们来完成我们在测试的 Web 应用程序上的各种操作。我们创建了许多使用这些 API 的测试,并且它们会持续执行以验证应用程序。然而,随着你的测试套件的增长,你的测试和代码的复杂性也会增加。这成为了一个挑战,特别是关于你脚本和代码的可维护性。你需要设计一个可维护的、模块化的和可重用的测试代码,这样随着你添加更多的测试覆盖率,它也能扩展。在本章中,我们将探讨页面对象模式来构建一个高度可维护的测试套件。我们将涵盖以下主题:

  • 页面对象模式设计是什么?

  • 设计 PageObjects 的良好实践

  • 页面对象模式的扩展

  • 一个端到端示例

一个写得不错的测试脚本只要目标 Web 应用程序不改变就能正常工作。但是一旦你的 Web 应用程序中的一个或多个页面发生变化,作为一个测试脚本开发者,你不应该处于不得不在数百个不同地方重构你的测试脚本的位置。让我们通过一个例子更好地理解这个陈述。我们将通过在一个 WordPress 博客上工作来尝试通过这一章。在我们开始之前,我希望你创建一个 WordPress 博客(wordpress.com/about)或者使用你现有的一个。

为我们的 WordPress 博客创建测试案例

在这里,我们使用了一个 WordPress 博客:demo-blog.seleniumacademy.com/wp/。在我们开始讨论页面对象模式之前,让我们为它创建三个测试案例。

测试案例 1 – 向我们的 WordPress 博客添加一篇新文章

以下测试脚本将登录我们 WordPress 博客的Admin门户并添加一篇新博客文章:

@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();
}

以下是在前一段代码中执行的步骤序列:

  1. 登录 WordPress Admin门户。

  2. 前往所有文章页面。

  3. 点击添加新文章按钮。

  4. 通过提供标题和描述添加一篇新文章。

  5. 发布文章。

测试案例 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();
}

以下是在前一个测试脚本中遵循的步骤序列,以删除文章:

  1. 登录 WordPress Admin门户。

  2. 前往所有文章页面。

  3. 点击要删除的文章。

  4. 删除文章。

测试案例 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);
}

以下是在前一个测试脚本中遵循的步骤序列,以计算我们博客上当前可用的文章数量:

  1. 登录Admin门户。

  2. 前往所有文章页面。

  3. 计算可用的文章数量。

在前面的三个测试脚本中,我们登录 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()的方法,用于管理这些元素的填充并提交登录表单。因此,这个AdminLoginPageobject类将代表 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 在页面上定位该元素。它接受定位机制(即通过IdNameClass 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 枚举中支持的枚举常量如下:

  • CLASS_NAME

  • CSS

  • ID

  • ID_OR_NAME

  • LINK_TEXT

  • NAME

  • PARTIAL_LINK_TEXT

  • TAG_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声明的元素,即emailpasswordsubmit,都将由 WebDriver 使用在FindBy注解中指定的定位机制初始化。

理解 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 的测试框架的良好实践了。

将网页视为服务提供商

从高层次来看,当你查看一个 Web 应用程序中的页面时,你会发现它是由各种用户服务聚合而成的。例如,如果你查看我们 WordPress 管理控制台中的所有帖子页面,它包含许多部分:

在前面的屏幕截图中,在“所有帖子”页面,用户可以执行以下五个操作:

  • 添加一篇新帖子。

  • 编辑选定的帖子。

  • 删除选定的帖子。

  • 通过分类过滤帖子。

  • 在所有帖子中搜索文本。

前面的活动是所有帖子页面提供给用户的服务。因此,你的页面对象也应该为测试用例提供这些服务,这是页面对象的用户。所有帖子页面对象的代码应如下所示:

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 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(){
 }
}

现在您的测试用例可以使用相同的页面对象来使用与所有帖子页面相关的隐含服务。

在页面对象中使用页面对象

将会有很多需要你在页面对象中使用页面对象的情况。让我们通过所有帖子页面上的一个场景来分析这一点。当你点击“添加新帖子”来添加新帖子时,浏览器实际上会导航到另一个页面。因此,你必须创建两个页面对象,一个用于所有帖子页面,另一个用于“添加新帖子”页面。将你的页面对象设计成模拟我们目标应用的精确行为将使事情非常清晰,并且相互独立。你可能可以通过几种不同的方式导航到“添加新帖子”页面。为“添加新帖子”页面创建一个自己的页面对象并在需要的地方使用它,将使你的测试框架遵循良好的面向对象原则,并使测试框架的维护变得容易。让我们看看在页面对象中使用页面对象会是什么样子。

AddNewPost 页面对象

AddNewPost页面对象添加新帖子,如下面的代码所示:

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 页面对象

AllPostsPage页面对象处理所有帖子页面,如下面的代码所示:

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页面对象中看到的那样,我们在createNewPost()方法中实例化了AddNewPage页面对象。因此,我们正在使用一个页面对象来调用另一个页面对象,并尽可能使行为接近目标应用。

将页面对象中的方法视为服务而不是用户操作

有时可能会对哪些方法构成页面对象产生混淆。我们之前看到每个页面对象都应该包含用户服务作为其方法。但很常见的是,我们在几个测试框架中看到一些页面对象的实现,它们将用户操作作为方法。那么用户服务用户操作之间的区别是什么?正如我们之前看到的,WordPress 管理控制台上的用户服务的一些例子如下:

  • 创建新帖子

  • 删除帖子

  • 编辑帖子

  • 在帖子中搜索

  • 过滤帖子

  • 计算所有现有帖子

所有的上述服务都讨论了目标应用程序的各种功能。现在,让我们看看一些用户操作的例子:

  • 鼠标点击

  • 在文本框中输入文本

  • 导航到页面

  • 点击复选框

  • 从下拉列表中选择一个选项

之前的列表展示了页面上的用户操作的一些示例。这些操作在许多应用程序中都很常见。你的页面对象不是为了向测试用例提供用户操作,而是提供用户服务。因此,你的页面对象中的每个方法都应该映射到目标页面提供给用户的某个服务。为了完成一个用户服务,页面对象的方法应该包含许多用户操作

几个用户操作组合在一起以完成一个用户服务。

如果你的页面对象使用用户操作而不是用户服务来提供方法,它将看起来如下;让我们看看AddNewPage页面对象将是什么样子:

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页面对象的代码中,我们有三个不同的方法来完成三个不同的用户操作。调用对象,现在应该调用以下方法,而不是仅仅调用addNewPage(String title, String description)方法:

typeTextinTitle(String title)
typeTextinContent(String description)
clickPublishButton()

上述用户操作是三个不同的用户操作,用于完成添加新帖子用户服务。这些方法的调用者也应该记住这些用户操作需要调用的顺序;也就是说,clickPublishButton()方法应该始终放在最后。这给测试用例和其他试图向系统中添加新帖子的页面对象引入了不必要的复杂性。因此,用户服务将隐藏页面对象用户的大部分实现细节,并减少维护测试用例的成本。

动态识别一些 Web 元素

在所有的页面对象中,我们使用@FindBy注解初始化了我们将在对象实例化期间使用的元素。总是很好,能够识别出完成一个用户服务所需的页面上的所有元素,并将它们分配给页面对象中的成员变量。然而,并不总是能够做到这一点。例如,如果你想编辑所有帖子页面上的一个特定帖子,在页面对象初始化期间,并不强制要求将页面上的每个帖子映射到页面对象的成员变量。当你有大量帖子时,页面对象的初始化将会花费不必要的时间将帖子映射到成员变量,即使我们并不使用它们。此外,我们甚至不知道需要映射多少成员变量才能将所有帖子映射到所有帖子页面。所有帖子页面的 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 被识别;包含所有帖子的“所有帖子”页面中的元素被映射到一个成员变量,在 AllPostsPage 页面对象中命名为 pageContainer。目标帖子仅在 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 的端到端示例上工作

现在我们已经了解了页面对象是什么,是时候看看一个端到端示例,该示例交互并测试 WordPress 管理控制台了。首先,我们将看到所有页面对象,然后是使用它们的测试用例。

查看所有页面对象

让我们先看看所有参与测试 WordPress 管理控制台的页面对象。

AdminLoginPage 页面对象

AdminLoginPage 页面对象处理登录页面。如果目标应用中的页面有任何更改,这个对象必须进行重构,使用以下代码:

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 页面对象的构造函数接受 WebDriver 实例。这将允许测试框架在整个测试脚本执行过程中以及页面对象中使用相同的驱动实例;因此,浏览器和 Web 应用的状态得以保留。您将看到所有页面对象都有类似的构造函数。除了构造函数之外,AdminLoginPage 页面对象还提供了 login(String username, String pwd) 服务。这个服务允许测试脚本登录到 WordPress 博客,并返回 AllPostsPage 页面对象。在返回 AllPostsPage 页面对象的实例之前,PageFactory 页面对象将初始化 AllPostsPage 页面对象的所有 Web 元素。因此,登录服务的所有实现细节都隐藏在测试脚本中,它可以与 AllPostsPage 页面对象一起工作。

AllPostsPage 页面对象

AllPostsPage 页面对象处理所有帖子页面,使用以下代码:

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 页面对象提供了六个服务:

  • 创建帖子

  • 编辑帖子

  • 删除帖子

  • 通过分类过滤帖子

  • 在帖子中搜索文本

  • 计算可用的帖子数量

一旦测试脚本通过 AdminLoginPage 页面对象的登录服务获取了这个页面对象的实例,它就可以使用这个页面对象的六个服务之一进行测试。如果实现细节有任何变化,例如导航到特定帖子或页面上 WebElement 的 ID,测试脚本实际上不必担心。修改这个页面对象将应用到 WordPress 博客上。

AddNewPostPage 页面对象

AddNewPostPage 页面对象处理向博客添加新帖子,使用以下代码:

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 页面对象在 AllPostsPage 页面对象的 createANewPost 服务中被实例化。这个页面对象提供了一个名为 addNewPost 的服务,它接受帖子的 titledescription 输入,并使用这些信息在博客中发布一篇新帖子。

EditPostPage 页面对象

EditPostPage 页面对象处理编辑现有帖子,使用以下代码:

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 的服务,用于编辑现有帖子。新的 titledescription 作为输入参数传递给此服务。

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 页面对象类似于 AddNewPostPageEditPostPage 页面对象,并在 AllPostsPage 页面对象的 deleteAPost 服务中实例化。这提供了一个名为 delete 的服务,用于删除现有帖子。正如您所看到的,AddNewPostPageEditPostPageDeletePostPage 页面对象都会带您到同一个页面。因此,将这三个页面对象合并为一个提供添加、编辑和删除帖子服务的页面对象是有意义的。

查看测试用例

现在是时候查看使用页面对象与 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 博客中:

  1. 测试脚本创建一个 ChromeDriver 实例,因为它打算测试在 Chrome 浏览器上添加新帖子到博客的场景。

  2. 它创建了一个与之前步骤中创建的相同驱动实例的 AdminLoginPage 页面对象实例。

  3. 一旦它获取到 AdminLoginPage 页面对象的实例,它就使用 login 服务登录到 WordPress 管理控制台。login 服务作为回报,向测试脚本提供一个 AllPostsPage 页面对象的实例。

  4. 测试脚本使用在先前的步骤中获得的 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 博客中:

  1. 它创建了一个与之前步骤中创建的相同驱动实例的 AdminLoginPage 页面对象实例。

  2. 一旦它获取到 AdminLoginPage 页面对象的实例,它就使用 login 服务登录到 WordPress 管理控制台。login 服务作为回报,向测试脚本提供一个 AllPostsPage 页面对象的实例。

  3. 测试脚本使用在先前的步骤中获得的 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 博客中删除帖子步骤的顺序:

  1. 它创建了一个使用之前步骤中创建的相同驱动实例的AdminLoginPage PageObject 实例。

  2. 一旦它获得了AdminLoginPage PageObject 的实例,它就使用login服务登录 WordPress 管理控制台。login服务作为回报,向测试脚本提供一个AllPostsPage PageObject 实例。

  3. 测试脚本使用在上一步骤中获得的AllPostsPage PageObject 的实例来使用所有帖子页面提供的许多服务之一。在这种情况下,它使用了deleteAPost服务。

帖子计数

此测试用例处理博客中当前可用的帖子数量的计数,使用以下代码:

@Test 
public void testCountPost() {
    AdminLoginPage loginPage =
            PageFactory.initElements(driver, AdminLoginPage.class);
    AllPostsPage allPostsPage = loginPage.login(username, password);
    Assert.assertEquals(allPostsPage.getAllPostsCount(), 1);
}

以下是在先前的测试脚本中执行以测试 WordPress 博客中帖子计数步骤的顺序:

  1. 它创建了一个使用之前步骤中创建的驱动实例的AdminLoginPage PageObject 实例。

  2. 一旦它获得了AdminLoginPage PageObject 的实例,它就使用login服务登录 WordPress 管理控制台。login服务作为回报,向测试脚本提供一个AllPostsPage PageObject 实例。

  3. 测试脚本使用在上一步骤中获得的AllPostsPage PageObject 的实例来使用所有帖子页面提供的许多服务之一。在这种情况下,它使用了getAllPostsCount服务。

摘要

在本章中,我们学习了 PageObject 模式以及我们如何使用 PageObjects 实现测试框架。它具有许多优点。PageObject 模式和LoadableComponents类提供了一个易于适应目标应用程序更改的测试框架,而无需更改任何测试用例。我们应该始终记住,一个设计良好的测试框架总是能够适应目标应用程序的更改。在下一章中,我们将探讨使用Appium测试 iOS 和 Android 移动应用程序。

问题

  1. 你如何初始化使用 PageFactory 实现的 PageObject?

  2. 我们可以使用哪个类来实现验证页面是否加载的方法?

  3. @FindBy 支持哪些By class方法?

  4. 当使用 PageFactory 时,如果你给 WebElement 变量命名与 ID 或 name 属性相同,那么你不需要使用@FindBy 注解——对还是错?

更多信息

你可以查看以下链接以获取有关本章涵盖主题的更多信息:

第十章:使用 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.comm.facebook.com的 URL 重定向发生。Facebook 应用程序服务器意识到请求是从移动设备发起的,并开始提供移动网站而不是桌面网站。这些 m.site 使用 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 及以下版本开发的测试应用程序,您需要使用 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,我们将执行以下步骤:

  1. 您可以从developer.apple.com/xcode/下载最新的 Xcode。

  2. 下载后,安装并打开它。

  3. 导航到“首选项”|“组件”,以下载和安装命令行工具和 iOS 模拟器,如下面的截图所示:

图片

如果您使用的是真实设备,您需要在设备上安装配置文件,并启用 USB 调试。

尝试启动 iPhone 模拟器并验证它是否正常工作。您可以通过导航到 Xcode | 打开开发者工具 | iOS 模拟器来启动模拟器。模拟器应该看起来与以下截图所示类似:

图片

设置 Android SDK

您需要从developer.android.com/studio/安装 Android SDK。下载并安装 Android Studio。

启动已安装的 Android Studio。现在下载任何 API 级别为 27 的 Android,并安装它。您可以通过导航到“工具 | SDK 管理器”来完成此操作。您应该会看到以下截图所示的内容:

在这里,我们正在安装 Android 8.1,其 API 级别为 27。

创建 Android 模拟器

如果您想在 Android 模拟器上执行测试脚本,您必须创建一个。要创建一个,我们将执行以下步骤:

  1. 在 Android Studio 中,通过导航到“工具 | AVD 管理器”打开 AVD 管理器。它将启动 AVD 管理器,如下面的截图所示:

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

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

安装 Appium

您可以从appium.io/下载 Appium。点击“下载 Appium”按钮以下载适用于您工作站平台的 Appium。在这里,我使用的是 Mac,因此它将下载 Appium DMG 文件。

将 Appium 复制到“应用程序”文件夹中,并尝试启动它。第一次启动时,它会要求您授权运行 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能力。在本文例中,我们使用了 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 浏览器并开始执行测试脚本命令。

使用设备云在真实设备上运行测试

Appium 支持在移动模拟器、仿真器和真实设备上进行测试。要设置一个使用真实设备的移动测试实验室,需要资本投资以及设备和基础设施的维护。手机制造商几乎每天都会发布新的手机型号和操作系统更新,而您的应用程序必须与新发布的产品兼容。

为了更快地应对这些变化并将投资降至最低,我们可以使用基于云的移动测试实验室。有许多供应商,如亚马逊网络服务、BrowserStack 和 Sauce Labs,提供基于云的实时移动设备实验室来执行测试,而不需要在前端设备上进行任何投资。您只需为测试使用的时间付费。这些供应商还允许您在他们的设备云中使用 Appium 运行自动化测试。

在本节中,我们将探索 BrowserStack,以在其实时设备云上运行测试:

  1. 您需要一个带有Automate功能订阅的 BrowserStack 账户。您可以在www.browserstack.com/上注册一个免费试用账户。

  2. 我们需要根据设备组合从 BrowserStack 获取所需的配置能力。BrowserStack 根据所选的设备和平台组合提供能力建议。请访问www.browserstack.com/automate/java并选择一个操作系统和设备:

图片

  1. 根据您的选择,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创建参数化和数据驱动的测试。这将帮助我们重用测试并提高测试覆盖率。

问题

  1. 移动应用有哪些不同类型?

  2. Appium Java 客户端库为测试 iOS 和 Android 应用程序提供了哪些类?

  3. 列出通过 USB 端口连接到计算机的 Android 设备的命令是什么?

  4. Appium 服务器默认使用哪个端口?

更多信息

您可以查看以下链接,获取有关本章涵盖主题的更多信息:

第十一章:使用 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 套件文件中添加参数。我们必须为参数提供namevalue属性。在这个例子中,我们创建了两个参数:searchWorditems。这些参数存储搜索词和应用程序为该搜索词返回的预期项目数量。

现在,让我们修改测试以使用参数而不是硬编码的值。首先,我们需要在测试方法之前的@Test注解中使用@Parameters注解。在@Parameters注解中,我们需要提供套件文件中声明的参数的确切名称和顺序。在这种情况下,我们将提供searchWorditems。我们还需要向测试方法添加参数以及所需的数据类型,以映射 XML 参数。在这种情况下,向searchProduct()测试方法添加了String searchWordint 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。现在,我们将使用由搜索返回的searchWords和预期的items计数的三个组合,而不是单个搜索词。我们将在SearchTest类中添加一个名为 provider()的新方法,如下面的代码所示,在@BeforeMethod注解之前:

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。这是通过以下步骤完成的:

  1. @Test注解中提供data provider的名称

  2. searchProduct方法中添加两个参数String searchWordint items

  3. 使用方法参数替换硬编码的值:

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 文件,并复制以下searchWordsitems的组合:

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();
    }
}

在提供的provider方法中,将使用 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 POI API,由 Apache 基金会开发,来操作 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 格式读取数据的方法。

问题

  1. 解释数据驱动测试是什么。

  2. Selenium 支持数据驱动测试——对或错?

  3. 在 TestNG 中,有哪些两种方法可以创建数据驱动测试?

  4. 解释 TestNG 中的DataProvider方法。

更多信息

您可以查看以下链接,获取本章涵盖主题的更多信息:

第十二章:评估

第一章

  1. 真或假:Selenium 是一个浏览器自动化库。

True.

  1. Selenium 提供了哪些不同类型的定位机制?

不同的定位机制类型是 ID、Name、ClassName、TagName、Link、LinkText、CSS Selector 和 XPATH。

  1. 真或假:使用 getAttribute() 方法,我们也可以读取 CSS 属性吗?

False. getCssValue() 方法用于读取 CSS 属性。

  1. 我们可以在 WebElement 上执行哪些操作?

执行的操作是点击、输入(sendKeys)和提交。

  1. 我们如何确定复选框是被勾选还是未勾选?

通过使用 isSelected() 方法。

第二章

  1. WebDriver 成为 W3C 规范的意义是什么?

WebDriver 现在是一个 W3C 规范。这意味着浏览器必须完全遵守世界万维网联盟(简称 W3C)设定的 WebDriver 规范,并且将由浏览器厂商原生支持。HTML5 和 CSS 是其他突出的 W3C 规范。

  1. 真或假:WebDriver 是一个接口。

True.

  1. 哪些浏览器支持无头测试?

Google Chrome 和 Mozilla Firefox。

  1. 我们如何使用 Chrome 测试移动网站?

通过使用移动仿真功能。

第三章

  1. 哪个版本的 Java Streams API 被引入?

Java 8。

  1. 解释 Streams API 的过滤函数。

Java Stream API 提供了一个 filter() 方法,可以根据给定的谓词过滤流元素。假设我们想获取页面上所有可见的链接元素,我们可以使用 filter() 方法以以下方式返回列表:

List<WebElement> visibleLinks = links.stream()
   .filter(item -> item.isDisplayed())
    .collect(Collectors.toList());
  1. Streams API 中的哪种方法会从 filter() 函数返回匹配元素的数量?

count()

  1. 我们可以使用 map() 函数通过属性值:True 或 false 过滤 WebElements 列表:?

False.

第四章

  1. 我们可以使用哪些不同的格式来输出截图?

OutputType 接口支持在 BASE64BYTESFILE 格式中的截图类型。

  1. 我们如何使用 Selenium 切换到另一个浏览器标签?

我们可以使用 driver.switchTo().window() 方法切换到另一个浏览器标签。

  1. 真或假:defaultContent() 方法将切换到之前选定的框架。

False. defaultContent() 方法将切换到页面。

  1. Selenium 提供了哪些导航方法?

Navigate 接口提供了 to()back()forward()refresh() 方法。

  1. 我们如何使用 Selenium 添加 cookie?

我们可以使用 driver.manage().addCookie(Cookie cookie) 方法添加 cookie。

  1. 解释隐式等待和显式等待之间的区别。

一旦设置隐式等待,它将在 WebDriver 实例的整个生命周期中可用。当调用 findElement 时,它将在设定的持续时间内等待元素。如果元素在设定时间内没有出现在 DOM 中,它将抛出 NoSuchElementFound 异常。

另一方面,显式等待用于等待特定条件发生(例如,元素的可见性或不可见性、标题的变化、元素属性的变化、元素变为可编辑或自定义条件)。与隐式等待不同,显式等待将轮询 DOM 以满足条件,而不是等待固定的时间。如果条件在定义的超时之前得到满足,它将退出,否则将抛出异常。我们可以使用ExpectedConditions类中的各种预定义条件与显式等待一起使用。

第五章

  1. 对还是错——拖放操作需要源元素和目标元素。

对。

  1. 列出我们可以使用 actions API 执行的键盘方法。

sendKeys()keyUp()keyDown()

  1. 哪个 actions API 方法可以帮助执行双击操作?

doubleClick(WebElement target).

  1. 使用 actions API,我们如何执行保存选项(即,Ctrl + S)?

new Actions(driver) .sendKeys(Keys.chord(Keys.CONTROL, "s")) .perform();.

  1. 我们如何使用 actions API 打开上下文菜单?

通过调用contextClick()方法。

第六章

  1. 你可以使用 WebDriverEventListener 接口来监听 WebDriver 事件:对还是错?

对。

  1. 你可以使用 WebDriverEventListener 接口在调用sendKeys方法之前自动清除输入字段吗?

我们可以在beforeChangeValueOf()事件处理程序中调用WebElement.clear()方法。

  1. Selenium 支持可访问性测试:对还是错?

错。Selenium 不支持可访问性测试

第七章

  1. 对还是错:使用 Selenium,我们可以在远程机器上执行测试吗?

对。

  1. 用于在远程机器上运行测试的哪个驱动类?

RemoteWebDriver类。

  1. 解释DesiredCapabilities类。

DesiredCapabilities类用于指定测试脚本从 RemoteWebDriver 需要的浏览器能力。例如,我们可以在DesiredCapabilities中指定浏览器名称、操作系统和版本,并将其传递给RemoteWebDriver。Selenium Standalone Server 将匹配配置的能力与可用的节点,并在匹配的节点上运行测试。

  1. Selenium 测试和 Selenium Standalone Server 之间使用什么协议?

JSON-Wire。

  1. Selenium Standalone Server 使用的默认端口是什么?

端口4444

第八章

  1. 哪个参数可以用来指定节点可以支持的浏览器实例数量?

maxInstances

  1. 解释如何使用 Selenium Grid 来支持跨浏览器测试。

使用 Selenium Grid,我们可以为各种浏览器和操作系统组合设置节点,并在分布式架构中运行测试。根据测试提供的功能,Selenium Grid 选择适当的节点,并在所选节点上执行测试。我们可以根据所需的跨浏览器测试矩阵组合添加所需数量的节点。

  1. 使用RemoteWebDriver运行 Selenium Grid 测试时,需要指定哪个 URL?

http://gridHostnameOrIp:4444/wd/hub

  1. Selenium Grid Hub 充当负载均衡器:对或错?

对。Selenium Grid Hub 根据节点的可用性在多个节点上分发测试。

第九章

  1. 如何初始化使用 PageFactory 实现的 PageObject?

PageFactory.initElements(driver, pageObjectClass)

  1. 使用哪个类可以实现验证页面是否加载的方法?

LoadableComponent

  1. @FindBy 支持哪些By class方法?

ID、Name、ClassName、TagName、Link、PartialLinkText、CSS Selector 和 XPATH。

  1. 在使用 PageFactory 时,如果你通过相同的 ID 或 name 属性命名 WebElement 变量,那么你不需要使用@FindBy注解:对或错?

对。你可以声明与 id 或 name 属性值相同的 WebElement 变量,PageFactory 将解析它而无需使用@FindBy注解。

第十章

  1. 有哪些不同类型的移动应用?

原生、混合和移动 Web 应用。

  1. Appium Java 客户端库为测试 iOS 和 Android 应用提供了哪些类?

AndroidDriverIOSDriver

  1. 列出通过 USB 端口连接到计算机的 Android 设备的命令是什么?

adb devices

  1. Appium 服务器默认使用哪个端口?

端口4723

第十一章

  1. 解释数据驱动测试。

数据驱动是一种测试自动化框架方法,其中输入测试数据以表格格式或电子表格格式存储,单个测试脚本读取数据中的每一行,这可以是一个独特的测试用例,并执行步骤。这使测试脚本可重用,并通过不同的测试数据组合增加了测试覆盖率。

  1. 对或错:Selenium 支持数据驱动测试。

错误。

  1. TestNG 中有哪两种方法可以创建数据驱动测试?

TestNG 提供了两种数据驱动测试方法:套件参数和数据提供者。

  1. 解释 TestNG 中的 DataProvider 方法。

TestNG 中的 DataProvider 方法是一个被@DataProvider注解特殊标记的方法。它返回一个对象数组。我们可以通过从任何格式(如 CSV 或 Excel)读取表格数据来返回,以使用数据提供者测试测试用例。

posted @ 2025-09-12 13:56  绝不原创的飞龙  阅读(119)  评论(0)    收藏  举报