精通Selenium WebDriver 3.0 (第2版)

978-7-115-51547-6
作者: [印度]马克• 柯林(Mark Collin)
译者: 赵卓穆晓梅
编辑: 谢晓芳

图书目录:

详情

本书是使用Selenium WebDriver 2.0实现Web自动化测试的指南,展示了大量测试原理、方法和技巧。主要内容包括:如何构建测试框架,如何处理失败的测试,Selenuim生成的各种异常的含义,自动化测试失败的原因,页面对象的使用方法,高级用户交互API的使用方法,JavaScriptExcutor类的使用方法,Selenuim的缺点,如何在Selenuim中使用Docker等。

图书摘要

版权信息

书名:精通Selenium WebDriver 3.0 (第2版)

ISBN:978-7-115-51547-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [印度]马克•柯林(Mark Collin)

译    赵 卓 穆晓梅

责任编辑 谢晓芳

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright ©2018 Packt Publishing. First published in the English language under the title Mastering Selenium WebDriver 3.0, Second Edition.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


内容提要

本书通过大量测试代码、界面截图和操作步骤,介绍了如何使用Selenium WebDriver 3.0实现Web自动化测试。主要内容包括如何构建测试框架、如何处理失败的测试、Selenium生成的各种异常的含义、自动化测试失败的原因、页面对象的使用方法、高级用户交互API的使用方法、JavascriptExecutor类的使用方法、Selenium的缺点、如何在Selenium中使用Docker等。

本书有助于读者快速掌握并在实际工作中使用Selenium WebDriver 3.0,适合测试人员、开发人员以及相关专业人士阅读。


赵卓,新蛋科技有限公司电子商务研发团队项目经理,从事过多年测试工作和开发工作,精通各类开发和测试技术。编写过的图书有《Selenium自动化测试指南》,翻译过的图书有《Visual Studio 2010软件测试指南》和《快速编码:高效使用Microsoft Visual Studio》。

穆晓梅,新蛋科技有限公司产品服务团队高级测试工程师,负责美国新蛋网与tt海购网的测试与移动应用程序的质量把控工作,具有丰富的网站及移动端应用测试经验。


记得我最早接触Selenium是在2011年,那时QTP(Quick Test Professional)仍然是业界主流工具,我用QTP来进行Web自动化测试,遇到了诸多阻力。

QTP不但价格不菲,而且只能安装在Windows系统上,其安装包很大,安装过程也非常麻烦,支持的编程语言只有VBS(Microsoft Visual Basic Script Edition),支持的浏览器寥寥无几。这无疑使Web自动化的推进充满艰辛,人们只好另寻他法。

当时在Web功能测试领域,一款名为Selenium的测试工具已经开始崭露头角,它可以对多个浏览器(如Safari、Chrome、手机浏览器等)进行测试,支持各种语言(如Java、C#、Python、Ruby等),跨平台(如Windows系统、Linux系统等),开源并且免费。我一接触到这款惊艳的工具,便毫不犹豫地将其推广到公司中使用,效果非常显著。Selenium的优势显而易见,后续发展必定势不可挡。

时至今日,Selenium WebDriver 3.0(简称Selenium)已然发展成Web功能测试领域最强大的工具之一,而业界也对自动化提出了更高的要求,如何使用Selenium已不再是人们关注的重点,重点在于,如何更好地用Selenium来实施自动化测试,如何真正让自动化变得越来越有成效。

本书恰好能完美地解答这些问题。本书讲解了Selenium高级层面的许多应用,其中所包含的理念并不局限于Selenium工具本身,甚至适用于所有的自动化测试。作者不仅讲述如何用好Selenium这款工具,还揭示了如何正确地推进自动化测试,如何更好地保证软件质量。相信所有的读者都能从中受益匪浅。

本书在介绍Selenium的同时,还介绍了一些非常热门的技术或实践(如持续集成/持续部署、Docker等),并将它们应用到Selenium上,最大限度地发挥Selenium的作用。另外,本书还对Selenium的未来进行了剖析,讲述了机器学习和人工智能的应用,这些内容着实令人叹为观止。

在翻译过程中,作者深厚的技术功底和丰富的经验让我由衷折服,使我受益良多,他的思维总是灵活的,不被常识束缚,总是持有怀疑精神。我明显感觉到了他实事求是的态度:对于任何事物,只有适合我们的才是正确的,如果找不到更合适的,我们也要想办法来扬长避短。

由衷感谢本书的作者Mark Collin,正是由于他敢于探索、乐于分享的精神才造就了如此精彩的图书。

我的同事穆晓梅也参与了本书的翻译,这样才顺利完成了本书的翻译工作。

最后,感谢谢晓芳编辑在本书翻译过程中给予我的信任、支持和鼓励。

由于水平所限,本书翻译中的疏漏或不当之处在所难免,敬请广大读者及同行批评指正。

赵卓


Pinakin Chaubal是PMP认证专家,并获得过ISTQB基础级认证,同时还获得过HP0-M47 QTP 11认证。他在IT行业拥有17年以上的工作经验,曾在Patni、Accenture和L&T Infotech等公司任职。他是YouTube上Automation Geek频道的创始人,该频道教授PMP、ISTQB、Selenium WebDriver(与Jenkins集成)、使用Cucumber的页面对象模型,以及JavaScript(包括ES6)。

Nilesh Kulkarni是一名软件工程师,目前就职于PayPal。他拥有相当丰富的Selenium经验,曾用不同的编程语言在WebDriver上开发过多套框架,同时他还是一名开源贡献者。 他一直在积极推进PayPal的开源UI自动化框架nemo.js。他不仅对质量充满热情,还致力于研发各种开发人员生产力工具。在Stack Overflow上经常可见他的身影。


本书将着重介绍Selenium高级层面的一些应用。它将帮助你更好地理解Selenium这款测试工具,同时会提供一系列策略来帮助你创建可靠且可扩展的测试框架。

在自动化测试领域,行之有效的实现方式并非只有一种。本书将介绍一系列极简的实现方式,它们非常灵活,可根据你的特定需求进行自定义。

本书不介绍如何编写那些厚重的测试框架,以隐藏Selenium的细节。相反,本书将展示如何通过实用的附加功能来扩展Selenium,这些附加功能可以融入Selenium提供的丰富且精心设计的API中。

如果你是一名软件测试工程师或开发工程师,同时使用过Selenium、了解Java并且对自动化测试感兴趣,还希望提升测试技能,那么本书非常适合你。

为了让你能够快速入门,第1章先讲解如何建立一个基本的测试框架。之后会重点介绍如何通过Maven来设置项目,以下载依赖项。然后,使用TestNG在同一个浏览器中运行多个实例,展示并行执行测试的优势。接下来,讨论如何使用Maven插件自动下载驱动程序文件,使测试代码变得可移植,以及如何在后台模式下不间断地执行测试。

第2章探讨当测试执行失败时的应对方案。该章会深入分析测试可靠性为何十分重要,如何在Maven配置文件中设置测试的执行方式。你将了解持续集成、持续交付和持续部署的相关概念,并在持续集成服务器中设置测试构建。你还将学习如何连接到Selenium-Grid,如何在测试失败时截屏,以及如何通过读取栈追踪信息来分析测试失败的原因。

对于自动化测试出错的案例,第3章提出大量见解。该章探索Selenium能够产生的各类异常,介绍它们的意义。此外,你会更好地理解WebElement引用DOM元素的原理,了解Selenium的基本体系结构,并了解它如何向浏览器发送命令。

第4章讲述自动化测试失败的常见原因,以及各种等待解决方案。你将学习Selenium中等待策略的运作方式,了解如何使用等待策略来确保测试的稳定性和可靠性。

第5章探讨页面对象的定义,讲解如何有效使用这些对象避免失效。同时,该章还将介绍如何复用页面对象,精简代码以减少冗余,增强自动化测试的可读性。最后,该章展示如何创建流式页面对象。

第6章讲解如何使用高级用户交互API。你将学习如何挑战一些高难度的自动化场景,如悬停菜单和可拖曳控件。同时,该章还将探讨使用高级用户交互API可能遇到的一些问题。

第7章介绍JavascriptExecutor类的用法。该章将探讨如何使用JavaScript来解决复杂的自动化问题,还将讨论如何执行异步脚本,在执行完成时使用回调函数来通知Selenium。

第8章展示Selenium本身的局限性。接下来将探讨在各种场景下如何使用外部库和应用程序来扩展Selenium,以便你可以使用合适的工具和手段来完成任务。

第9章展示如何将Docker和Selenium结合在一起。你会发现在Docker中启动Selenium-Grid是多么容易的一件事,还将了解如何将Docker集成到构建过程中。

第10章首先讲述机器学习和人工智能,然后讨论如何通过Applitools Eyes使用人工智能技术。

附录A讲述有助于改善Selenium项目的各种途径。

附录B探讨从TestNG切换到JUnit所需的转变。

附录C讲述如何创建基于Appium的测试框架。

本书所使用的软件如下:

一般情况下,安装的浏览器种类越多越好。如果要完成本书中的所有练习,则至少需要安装Mozilla Firefox和Google Chrome。

IntelliJ IDEA社区版是免费的,要使用其完整功能,需要购买旗舰版。可以根据自己的喜好,使用旧版本的IntelliJ IDEA或者其他IDE。本书的代码都是用IntelliJ IDEA 2018编写的。

你可以用自己的账号从packtpub网站下载本书的示例代码文件。如果你通过其他途径购买本书,可以访问packtpub网站并注册,文件将直接通过电子邮件发送给你。

可以通过以下几个步骤下载代码。

(1)在packtpub网站上注册或登录。

(2)选择SUPPORT选项卡。

(3)单击Code Downloads & Errata按钮。

(4)在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。

下载文件后,请确保使用下列工具的最新版本来解压文件:

本书所涉及的代码包也在GitHub上进行托管,请访问GitHub官方网站,在搜索栏输入“Mastering Selenium WebDriver 3.0,Second Edition”进行搜索。如果有代码变更,将直接更新到已有的GitHub库中。

我们还提供了额外的代码包,这些代码包源于各式各样的图书和视频。如果你已访问GitHub官方网站并进入了本书的相关页面,只要单击左上角的PacktPublishing链接就可以访问这些图书和视频。

本书采用了一些版式约定。

代码段的格式如下。

public class BasicTest {

    private ExpectedCondition<Boolean> pageTitleStartsWith(final
    String searchString) {        
        return driver -> driver.getTitle().toLowerCase().
        startsWith(searchString.toLowerCase());    
    }

命令行的输入/输出会按如下格式书写。

mvn clean verify -Dwebdriver.gecko.driver=

 

表示警告或重要说明。

 

 

表示提醒信息或小提示。

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

常规反馈:发送电子邮件至feedback@packtpub.com,邮件标题中请带上书名。如果对于本书有任何疑问,请发电子邮件至questions@packtpub.com

勘误表:虽然我们已经全力确保内容的正确性,但错误仍旧难以完全避免。如果你在本书中发现任何错误,我们将非常感谢你的反馈。请访问packtpub网站,选择图书名称,单击Errata Submission Form链接,并录入详细信息。

盗版:如果你在网上发现盗版的packt图书,无论它们是以什么形式在互联网上传播的,请把地址或网站名称提供给我们,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供相关链接。

投稿:如果你具备相关专业知识,有意愿主导或参与图书撰写,请访问packtpub网站

请不吝点评。在你阅读本书后,为什么不在下单的网站评论一番呢?你的客观意见将为那些正在观望的读者提供参考,帮助他们决定是否购买本书,也可以帮助我们了解你对本书的看法,同时有利于作者了解你对本书的反响。非常感谢!

有关Packt的更多信息,请访问packtpub网站。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书配套资源包括源代码。

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意,为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


你或许有所耳闻,Selenium的主要问题之一便是运行完所有测试要花多久的时间。据我所闻,测试时间短则几小时,长则几天。本章将介绍如何提升运行速度,使你写出的测试能快速、定期地运行。

你可能会遇到的另一个问题是其他人如何运行你的测试。问题的原因通常在于,要在其他计算机上配置项目并使其可以运行简直是一种痛苦,对他们来说,这太费劲了。除了要提升测试速度外,还要让其他人很容易获取你的代码并自己运行。

如何通过创建快速反馈循环来做到这一点?

首先,解释一下快速反馈循环的含义。当开发人员更改或重构代码时,他们可能会出现失误,改错某些东西。一旦他们提交了代码,反馈循环就会启动,并在结束时告知这些代码变更是对还是错的。我们希望反馈循环尽可能快,在理想情况下,开发人员在签入代码之前就可以运行所有可用的测试。如此一来,在测试代码之前,就能够知道对代码的修改是否有错。

最终,我们想要达到的目的是找出开发人员做的哪些更新会导致测试失败,毕竟功能发生了变化。这些最终版本的代码会使测试转变为实时文档,第2章将会讨论这方面的更多内容。

本章将从创建一个基本的测试框架开始讲解。需要哪些软件呢?在编写本章代码时,所使用的软件和浏览器版本如下。

请确保你的软件至少已更新至上述版本,以确保一切能正常运作。

理想情况下,每当有人向中央代码库推送代码时,我们希望测试能够运行。这样做的一部分原因是可以让测试的运行变得十分容易。如果只签出代码库然后执行命令就可以让所有测试运行起来,这就意味着开发人员更可能运行测试。

我们将使用Apache Maven来简化这个过程。引用Maven文档里的一句说明:

“Maven试图将模式应用到项目构建的基础设施中,提供明确的途径来使用最佳实践,以提高理解力和生产力。”

Maven是一种用于构建和管理Java项目的工具(包括下载所需的任何依赖项),在许多公司中都作为标准企业基础设施的一部分。Maven并不是解决这个问题的唯一方法(例如,Gradle是一个非常强大的替代方案,在许多领域它与Maven的功能一样强大,甚至在一些方面超过了它),但Maven是最有可能在周围看到的一个工具。大多数Java开发人员将在其职业生涯的某个阶段使用这个工具。

Maven的一个主要优点是它鼓励开发人员使用标准化的项目结构,使熟悉Maven的人可以轻松浏览源代码。Maven还易于接入CI系统(如Jenkins或TeamCity),因为所有主要的系统都能理解Maven POM文件。

Maven是如何让开发人员轻松运行测试的呢?当我们使用Maven设置项目时,它可以签出测试代码,只需要在终端窗口中键入mvn clean verify,即可自动下载所有的依赖项,设置类的路径,并运行所有的测试。

没有比这更容易的事了。

全面讲解Maven的安装和运行并不属于本书探讨的范围。Apache软件基金会已经提供了一个Maven设置指南,只需要5min便可以完成设置。请访问Maven官方网站,单击左侧菜单中的Users Centre选项,页面跳转后Users Centre会展开,再单击其下的子选项Maven in 5 Minutes。

如果你正在使用的是Debian版的Linux,可以轻松使用如下命令。

sudo apt-get install maven

如果你在使用Homebrew运行Mac,则只需要以下代码。

brew install maven

一旦完成安装并运行Maven后,就可以使用基本的POM文件来启动Selenium项目了。首先创建一个基本的Maven目录结构,然后在里面新建一个名为pom.xml的文件。查看下方的截图。

在Java环境中,你可能会接触到两种主流测试框架——JUnit 和TestNG。TestNG更易于上手,而且能够开箱即用,但是JUnit 的可扩展性更好。在Selenium的邮件列表中,许多人都在询问关于TestNG 的问题,TestNG 无疑更受大众欢迎,而关于JUnit的问题却不常见。

这里不建议将其中任何一个作为正确之选,因为它们都是业界里极其出色的框架。由于TestNG似乎更受大众欢迎,因此本章重点介绍TestNG的实现方式。

如果你更喜欢JUnit,那么可以参考附录B。在那里将实现相同的基础项目,但使用的是JUnit而不是TestNG。这意味着你可以同时参阅TestNG的实现方式和JUnit的实现方式,而不是纠结用哪个最好。你也可以在之后选择自己喜欢的那个,并阅读相关章节。

首先看一下基于TestNG的Maven项目,它的基础POM代码如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <groupId>com.masteringselenium.demo</groupId>
    <artifactId>mastering-selenium-testng</artifactId>
    <version>DEV-SNAPSHOT</version>
    <modelVersion>4.0.0</modelVersion>

    <name>Mastering Selenium TestNG</name>
    <description>A basic Selenium POM file</description>
    <url>http://www.epubit.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-
        8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-
        8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!-- Dependency versions -->
        <selenium.version>3.12.0</selenium.version>
        <testng.version>6.14.3</testng.version>
        <!-- Plugin versions -->
        <maven-compiler-plugin.version>3.7.0</maven-
        compilerplugin.version>
        <maven-failsafe-plugin.version>2.21.0</maven-
        failsafeplugin.version>
        <!-- Configurable variables -->
        <threads>1</threads>
    </properties>

    <build>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <configuration>
                   <source>${java.version}</source>
                   <target>${java.version}</target>
               </configuration>
               <version>${maven-compiler-plugin.version}</version>
           </plugin>
       </plugins>
    </build>

    <dependencies>
       <dependency>
           <groupId>org.seleniumhq.selenium</groupId>
           <artifactId>selenium-java</artifactId>
           <version>${selenium.version}</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.seleniumhq.selenium</groupId>
           <artifactId>selenium-remote-driver</artifactId>
           <version>${selenium.version}</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.testng</groupId>
           <artifactId>testng</artifactId>
           <version>${testng.version}</version>
           <scope>test</scope>
       </dependency>
    </dependencies>

</project>

这里看到的主要是Maven模板代码。其中groupIdartifactIdversion都遵循如下标准的命名规范。

这里添加了Maven编译器插件,以便可以指定编译代码所需的Java版本。这里选择的是Java 8,因为这是Selenium目前支持的最低Java版本。

接下来引用代码所依赖的库,它们保存在依赖项区块中。首先为Selenium和TestNG分别添加了一个依赖项。请注意,只给它们指定测试所需的范围,这将确保这些依赖项只加载到测试运行时的类路径中,而绝不会打包在作为构建过程的一部分而生成的任何工件中。

 

使用Maven属性来设置依赖项版本。这不是必需的,却是一种常见的Maven规范。要点在于,如果它们都在一个地方声明,那么后续更新POM中各项内容的版本会更加容易。XML可能非常冗长,而且要通过POM查找将要更新的各个依赖项或插件的版本非常艰难,要花很长的时间,尤其是在你刚开始使用Maven配置文件时。

现在可以用自己的IDE打开POM文件。(在本书中,假设你使用的是IntelliJ IDEA,不过任何主流的IDE应该都能够打开POM文件并从中创建项目。)

现在我们有了Selenium项目的基础。下一步便是创建一个基本的测试,使其可以通过Maven运行。首先,创建一个src/test/java目录。你的IDE应该能自动将该目录识别为测试源码目录。然后,在该目录中创建一个名为com.masteringselenium的包。最后,在这个包中,创建一个名为BasicTest.java的文件,在这个文件中将编写如下代码。

package com.masteringselenium;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.Test;

public class BasicTest {

    private ExpectedCondition<Boolean> pageTitleStartsWith(final
    String searchString) {
        return driver -> driver.getTitle().toLowerCase().
        startsWith(searchString.toLowerCase());
    }

     private void googleExampleThatSearchesFor(final
    String searchString) {

        WebDriver driver = new FirefoxDriver();

        driver.get("http://www.baidu.com");

        WebElement searchField = driver.findElement(By.name("q"));

        searchField.clear();
        searchField.sendKeys(searchString);

        System.out.println("Page title is: " + driver.getTitle());

        searchField.submit();

        WebDriverWait wait = new WebDriverWait(driver, 10, 100);
        wait.until(pageTitleStartsWith(searchString));

        System.out.println("Page title is: " + driver.getTitle());

        driver.quit();
    }

    @Test
    public void googleCheeseExample() {
        googleExampleThatSearchesFor("Cheese!");
    }

    @Test
    public void googleMilkExample() {
        googleExampleThatSearchesFor("Milk!");
    }
}

相信读者都非常熟悉这两个测试,它们都是基本的Google搜索场景,所有复杂的操作都被抽象到了一个方法中,可以传入不同的搜索关键字来多次调用该方法。万事俱备,只欠运行测试。要启动它们,只需要在终端窗口中输入如下命令。

mvn clean verify

Maven会从Maven Central下载所有Java依赖项。这一步完成后,就会构建项目,接着运行测试。

 

如果你在下载依赖项时遇到问题,请尝试在命令末尾添加-U,这将强制Maven检查Maven中央仓库以获取更新后的库。

接下来将看到Firefox成功启动,不过测试无法正常运行。原因在于Selenium 3中所有的驱动文件都不再默认与Selenium绑定,现在必须单独下载相关的驱动文件才能运行测试。

先下载该文件,然后将一个环境变量传给JVM,以便能顺利运行首个测试。稍后还将介绍一个稍微简化的方法,用来自动下载所需的驱动文件。

由于正在对Firefox运行测试,因此需要下载geckodriver文件。要获取最新版本,可以访问GitHub官方网站,在搜索栏中输入geckodriver进行搜索,在搜索结果中选择mozilla/geckodriver后进入项目页面,然后单击releases选项卡。

既然有了一个可用的驱动文件,就需要告诉Selenium在哪里找到它。幸运的是,Selenium团队已经提供了一种方法。当Selenium启动并尝试实例化驱动对象时,它将查找一个系统属性,该属性用于保存测试所需的可执行文件路径。这些系统属性的格式为WebDriver.<DRIVER_TYPE>.driver。为了让测试顺利运转,需要在命令行上传入这个系统属性。

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

这次,Firefox应该能正确加载,顺利运行测试且不带任何错误,最终的测试状态为通过。

 

如果仍有问题,请检查你正在使用的Firefox版本。本章中的代码是针对Firefox 60编写的。如果你使用的是早期版本,geckodriver可能无法完全支持,也许会遇到一些错误。

现在我们已拥有一个非常基础的项目,可以用Maven来运行几个非常基础的测试。目前来说,一切都运行得非常快,但当你开始向项目中添加更多的测试时,速度就会开始放缓。为了解决该问题,我们将并行执行测试,充分利用计算机的所有性能。

“并行执行测试”对不同的人来说会有不同的理解,因为它可能表示以下任意一种情况。

我们应该把并行执行测试作为提高测试覆盖范围的手段吗?

当然,在你编写自动化测试时,为了确保当前正在测试的网站能够正常运作,最初都会告知你网站必须要支持所有的浏览器。实际上,这是不现实的,浏览器种类太多了,不可能全都支持。例如,对于拥有许多奇异对象的AJAX密集型站点,它难道能在Lynx浏览器上运行吗?

 

Lynx是一个基于文本的Web浏览器,可以在Linux终端窗口中使用,并且在2014年还在积极开发中。

接下来你可能会听到:“好吧,那我们将支持Selenium所支持的每个浏览器。”这些初衷都是好的,但还会遇到麻烦。大多数人没有意识到的是,Selenium核心团队官方支持的浏览器是当前版本的浏览器,以及发布Selenium版本时先前版本的浏览器。实际上,它也可能适用于较旧的浏览器,核心团队会做很多工作来确保他们不会破坏对旧浏览器的支持。然而,如果要在Internet Explorer 6、Internet Explorer 7或Internet Explorer 8上运行一系列测试,这些运行测试的浏览器并未得到官方支持。

我们讨论下一组问题。Internet Explorer仅能在Windows计算机上运行,而且在Windows计算机上同时只能安装一个版本的Internet Explorer。

 

要在同一台计算机上安装多个版本的Internet Explorer,可以使用一些高科技,不过这样就无法获得准确的测试情况。最好预装并运行多个操作系统,但每个系统只安装某一版本的Internet Explorer。

Safari仅支持OS X计算机,且同时只能安装一个版本。

 

原来一个旧版本的Safari是支持Windows系统的,但现在Windows系统不再支持该版本,请勿使用该版本。

显而易见,即使我们希望针对Selenium支持的每个浏览器都执行全部测试,也无法在同一台计算机上实现此操作。

目前,人们更倾向于修改测试框架,使其可以接收要运行的浏览器列表。开发人员编写了一些代码,用来检测或指定计算机上可用的浏览器。一旦完成这项工作,他们就能够开始在少数几台计算机上并行执行所有测试,最后得到一个类似于下图的矩阵。

这个办法非常好,但它并没有彻底解决一直存在的难题:总有一两个浏览器无法在本地计算机上运行,因此你永远无法得到完全跨浏览器的测试覆盖率。使用多种不同的驱动程序实例(可能在多个线程中)在不同的浏览器上运行,只略微提高了覆盖率。我们仍然没有实现完全跨浏览器的覆盖率。

这样做也会有一些副作用。不同浏览器的运行速度不尽相同,因为各个浏览器中的JavaScript引擎不一样。在将代码推送到源码库之前,检查代码是否正常工作的过程可能会因此非常缓慢。

最终,这样做只会使我们更难诊断问题。一旦测试失败,要逐个排查是哪一个浏览器出的问题并定位失败的原因。这看上去只占用你1min的时间,但所有这些时间加起来就很多了。

为什么我们不暂且只在某一种浏览器上运行测试呢?让它在一种浏览器上运行得又稳又快,然后再考虑跨浏览器的兼容性。

 

在开发用的计算机上先选择某一种浏览器来运行测试可能是个好办法。然后可以使用CI服务器来弥补这个不足,将浏览器的覆盖率纳入考虑范围,作为构建流水线的一部分。先为本地计算机挑选一个带有快速JavaScript引擎的浏览器也许同样是一个好办法。

本章中的TestNG示例使用的TestNG版本为6.14.3,Maven Failsafe插件版本为2.21.0。如果你用的是这些组件更旧的版本,部分功能可能会无法使用。

首先,更改POM文件,添加一个threads属性,该属性用于决定运行测试的并行线程的数量。然后,使用Maven Failsafe插件来配置TestNG。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <!-- Dependency versions -->
    <selenium.version>3.12.0</selenium.version>
    <testng.version>6.14.3</testng.version>
    <!-- Plugin versions -->
    <maven-compiler-plugin.version>3.7.0
    </maven-compiler-plugin.version>
    <maven-failsafe-plugin.version>2.21.0
    </maven-failsafe-plugin.version>
    <!-- Configurable variables -->
    <threads>1</threads>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                 <source>${java.version}</source>
                 <target>${java.version}</target>
            </configuration>
            <version>${maven-compiler-plugin.version}</version>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${maven-failsafe-plugin.version}</version>
            <configuration>
                 <parallel>methods</parallel>
                 <threadCount>${threads}</threadCount>
            </configuration>
            <executions>
                 <execution>
                      <goals>
                          <goal>integration-test</goal>
                          <goal>verify</goal>
                      </goals>
                 </execution>
            </executions>
       </plugin>
    </plugins>
</build>

 

当使用Maven Failsafe插件时,设置为integration- test的goal节点将确保测试在集成测试阶段执行。设置为verify的goal节点将确保Failsafe插件可验证在integration-test阶段执行检查的结果,如果某些内容未通过则无法进行构建。如果没有设置verify的goal节点,则构建不会失败。

TestNG支持开箱即用的并行线程,这里只需要告诉它如何使用这些线程。这就是Maven Failsafe插件发挥作用的地方。我们将使用它为测试配置并行执行的环境。如果你将TestNG作为依赖项,则此配置将应用于TestNG,无须做任何特别的事情。

本例所关注的是parallelthreadCount这两项配置。我们已经将parallel节点设置为methods。这将在项目中搜索有@Test注释的方法,并将它们全部收集到一个巨大的测试池中。然后,Failsafe插件会从这个池中取出测试并运行它们。并发运行的测试数量取决于有多少线程可用。我们将使用threadCount属性来控制线程数。

需要注意的是,该方法不能控制测试运行的先后顺序。

我们配置threadCount来控制并行运行的测试数量,你可能注意到,这里没有指定一个数字。相反,这里使用的是Maven变量${threads},这将获取在properties块中定义的Maven属性threads的值,并将其传递给threadCount

因为threads是Maven属性,所以可以使用-D开关在命令行上重写它的值。如果不重写它的值,它将使用在POM中设置的值作为默认值。

因此,如果运行以下命令,它将使用在POM文件中的默认值1。

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

然而,如果使用这个命令,它将覆盖存储在POM文件中的值1,并使用值2替代。

mvn clean verify -Dthreads=2 -
Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

如你所见,这使我们能够调整用于运行测试的线程的数量,而无须对代码进行任何修改。

我们已经使用Maven和Maven Failsafe插件的强大功能来设置并行运行测试时使用的线程数量,但是还有更多工作要做。

如果你现在运行测试,将会发现,即使我们为代码提供了多个线程,所有的测试也仍然只在一个线程中运行。由于Selenium不具备线程安全,因此还需要编写一些额外代码,以确保各个Selenium实例在其独立的线程中运行,而不会在其他线程中运行。

以前在每个测试中都会创建一个FirefoxDirver实例。我们要将它从测试中提取出来,并将实例化浏览器部分放到其归属类DriverFactory中。然后,添加一个名为DriverBase的类,它将处理线程的封送。

现在,我们要构建的项目具有下图所示结构。

首先,创建DriverFactory类,使用的代码如下。

package com.masteringselenium;

import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.RemoteWebDriver;

public class DriverFactory {

    private RemoteWebDriver webDriver;

    private final String operatingSystem =
    System.getProperty("os.name").toUpperCase();
    private final String systemArchitecture =
    System.getProperty("os.arch");

    RemoteWebDriver getDriver() {
        if (null == webDriver) {
            System.out.println(" ");
            System.out.println("Current Operating System: " +
            operatingSystem);
            System.out.println("Current Architecture: " +
            systemArchitecture);
            System.out.println("Current Browser Selection:
            Firefox");
            System.out.println(" ");
            webDriver = new FirefoxDriver();
        }
        return webDriver;
    }

    void quitDriver() {
        if (null != webDriver) {
            webDriver.quit();
            webDriver = null;
         }
    }
}

这个类持有对WebDriver对象的引用,并确保每次调用getDriver()时都返回一个有效的WebDriver实例。如果某个实例已经启动,那么它将会获取现有的那个。如果需要的实例还没启动,则会先启动一个。

它还提供了一个quitDriver()方法,用于执行WebDriver对象的quit()方法,并将类中定义的WebDriver对象设置为null。这可以防止与已关闭的WebDriver对象进行交互,从而产生错误。

 

我们使用的是driver.quit()而不是driver.close()。按照一般经验,不应该使用driver.close()来进行清理。如果在测试期间出现的一些情况导致WebDriver实例提前关闭,则将会抛出错误。WebDriver API中的“关闭并清理”命令是driver.quit()。如果在测试时打开了多个窗口,而你只想关闭其中一些窗口,那么通常使用driver.close()

接下来,使用如下命令创建DriverBase类。

package com.masteringselenium;

import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DriverBase {

    private static List<DriverFactory> webDriverThreadPool =
    Collections.synchronizedList(new ArrayList<DriverFactory>());
    private static ThreadLocal<DriverFactory> driverThread;

    @BeforeSuite(alwaysRun = true)
    public static void instantiateDriverObject() {
        driverThread = new ThreadLocal<DriverFactory>() {
            @Override
            protected DriverFactory initialValue() {
                DriverFactory webDriverThread = new DriverFactory();
                webDriverThreadPool.add(webDriverThread);
                return webDriverThread;
            }
        };
    }

    public static RemoteWebDriver getDriver() {
        return driverThread.get().getDriver();
    }

    @AfterMethod(alwaysRun = true)
    public static void clearCookies() {
        getDriver().manage().deleteAllCookies();
    }

    @AfterSuite(alwaysRun = true)
    public static void closeDriverObjects() {
        for (DriverFactory webDriverThread : webDriverThreadPool) {
            webDriverThread.quitDriver();
        }
    }
}

这是一个微型的类,用于封装一个驱动对象池。可以使用ThreadLocal对象在各个独立的线程中实例化WebDriverThread对象。我们还创建了一个getDriver()方法,该方法使用DriverFactory对象上的getDriver()方法来将各个测试传递给可用的WebDriver实例。

这样做是为了隔离WebDriver的各个实例,以确保测试之间不会交叉污染。当开始并行运行测试时,我们不想看到不同的测试都开始向同一个浏览器窗口发送命令。现在,WebDriver中的每个实例都已安全地锁定在自己的线程中。

因为使用工厂类启动所有的浏览器实例,所以还要确保能正确关闭它们。为此,创建了一个带有@AfterMethod注释的方法,用于在测试运行后销毁驱动程序。如果测试在运行中没有正常触发代码行driver.quit(),则这种清理方式将发挥作用,例如,测试在运行时出错了,导致测试失败并提前结束测试。

注意,@AfterMethod@BeforeSuite注释有一个参数alwaysRun = true,其作用是确保标记的方法总会运行。本例中使用@AfterMethod注释可以确保无论如何都会调用到driver.quit()方法,即使测试失败。这样能保证正确关闭驱动实例,而驱动实例又会关闭浏览器。如果部分测试失败了,那么运行结束时常会有一些正打开的浏览器窗口残留在那里,这种措施将有助于减少这种现象。

现在剩下的就是清理basicTest类的代码,并将其更名为BasicIT。为什么要更改测试的名称?因为我们将使用maven-failsafe-pluginintegration-test阶段运行测试,而该插件在默认情况下会选取以IT结尾的文件。如果类名以TEST结尾,它将会被另一个插件maven-surefire-plugin选取。我们不希望maven-surefire-plugin选到这些测试,实际上该插件是用来进行单元测试的,我们想使用的是maven-failsafe-plugin,因此将使用以下代码。

package com.masteringselenium;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.Test;

public class BasicIT extends DriverBase {

    private ExpectedCondition<Boolean> pageTitleStartsWith(final
    String searchString) {
        return driver -> driver.getTitle().toLowerCase()
        .startsWith(searchString.toLowerCase());
    }
    private void googleExampleThatSearchesFor(final String
    searchString) {

        WebDriver driver = DriverBase.getDriver();
        driver.get("http://www.baidu.com");

        WebElement searchField = driver.findElement(By.name("q"));

        searchField.clear();
        searchField.sendKeys(searchString);

        System.out.println("Page title is: " + driver.getTitle());

        searchField.submit();

        WebDriverWait wait = new WebDriverWait(driver, 10, 100);
        wait.until(pageTitleStartsWith(searchString));

        System.out.println("Page title is: " + driver.getTitle());
    }

    @Test
    public void googleCheeseExample() {
        googleExampleThatSearchesFor("Cheese!");
    }

    @Test
    public void googleMilkExample() {
        googleExampleThatSearchesFor("Milk!");
    }
}

我们编辑了之前的那个基础测试,使其继承于DriverBase。在测试中,我们并没有实例化一个新的FirefoxDriver,而是调用DriverBase.getDriver()来获得一个有效的WebDriver实例。最后,我们从公共方法中删除了driver.quit(),因为这些操作已在基类DriverBase中实现。

如果用以下代码重新启动测试,则会发现这和之前没有任何区别。

mvn clean verify -Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

然而,如果你运行接下来的代码,并指定一些线程,将会看到这一次打开了两个Firefox浏览器,有两个测试在并行运行,最后两个浏览器都会关闭。

mvn clean verify -Dthreads=2 -
Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

 

如果你想证实每个测试都是在各自独立的线程中运行的,那么可以在DriverFactory类的getDriver()方法中添加System.out.println("Current thread: "+ Thread. currentThread().getId());

这将会显示当前线程ID,这样你就能看到Firefox- Driver的各实例是在不同的线程中运行的。

 

只看到一个浏览器启动?根据maven-failsafe- plugin的配置,会默认搜索出所有以IT.java结尾的文件。如果文件名是以Test开头或结尾的,则它们将被maven-surefire插件优先选取,因此线程配置将被忽略。请仔细检查,确保你的failsafe配置是正确的。

你也许已经注意到,只运行两个规模非常小的测试,根本感觉不到运行整个套件的时间缩短了多少。这是因为大部分时间都花在编译代码和加载浏览器上了。但是随着越来越多的测试添加进来,运行测试的时间将明显缩短。

 

这可能是调整BasicIT.java的好时机。可以尝试添加更多的测试,用不同的关键字来搜索,试试不同数量的线程,看看可以同时启动和运行多少个并发浏览器。请确保你已记录了执行时间,看看实际速度获得了多少增长(这在本章后面会有作用)。到某一点时,它将会达到计算机硬件的极限,此时添加更多线程会拖慢速度,起不到任何加速作用。对测试进行调优,使其能合理利用硬件环境,这是多线程运行测试的一个重要部分。

怎样才能加快速度呢?因为启动Web浏览器是一项计算量很大的任务,所以可以选择在每次测试之后暂不关闭浏览器。这显然有一些副作用,例如,当前页面并不是该应用程序的常规入口页,而且还可能带有一些多余的会话信息。

如果这种办法存在风险,并会有副作用,为什么我们还是要考虑这种办法?很简单,原因就是速度。假设有一套数量为50的测试,如果每次运行测试都要花费10s加载和关闭浏览器,那么合理重用这些浏览器将极大地缩短所花费的时间。如果对于这50个测试,我们总共只花10s来启动和关闭浏览器,那么总测试时间就缩短了490s。

我们看看它是如何运作的。首先尝试处理会话问题。因为WebDriver提供了一个命令来清除Cookie,所以在每次测试结束时触发它即可。接下来将添加一个新的@AfterSuite注释,以便在所有测试都执行完成后关闭浏览器。查看以下代码。

package com.masteringselenium;

import com.masteringselenium.config.DriverFactory;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DriverBase {

    private static List<DriverFactory> webDriverThreadPool =
    Collections.synchronizedList(new ArrayList<DriverFactory>());
    private static ThreadLocal<DriverFactory> driverThread;

    @BeforeSuite(alwaysRun = true)
    public static void instantiateDriverObject() {
        driverThread = new ThreadLocal<DriverFactory>() {
            @Override
            protected DriverFactory initialValue() {
                DriverFactory webDriverThread = new DriverFactory();
                webDriverThreadPool.add(webDriverThread);
                return webDriverThread;
            }
        };
    }

    public static RemoteWebDriver getDriver() {
        return driverThread.get().getDriver();
    }
    @AfterMethod(alwaysRun = true)
    public static void clearCookies() {
        try {
            getDriver().manage().deleteAllCookies();
        } catch (Exception ex) {
            System.err.println("Unable to delete cookies: " + ex);
        }
    }

    @AfterSuite(alwaysRun = true)
    public static void closeDriverObjects() {
        for (DriverFactory webDriverThread : webDriverThreadPool) {
            webDriverThread.quitDriver();
        }
    }
}

在代码中,首先添加了一个同步列表,用于存储WebDriverThread的所有实例。然后,修改了initialValue()方法,将创建的每个WebDriverThread实例添加到这个新的同步列表中。这样做的目的是跟踪线程。

接下来,重命名了带@AfterSuite的方法,以确保方法名尽可能一目了然。修改后的名称为closeDriverObjects(),这个方法并不像之前一样仅关闭当前使用的WebDriver实例。相反,它对webDriverThreadPool列表进行了遍历,关闭了所跟踪的每个线程的实例。

实际上,我们并不清楚运行了多少个线程,因为这是由Maven控制的。但这不是问题,毕竟编写这段代码就是为了解决这个问题。我们所能知道的是,当测试结束后,每个WebDriver实例将彻底关闭,不会有任何错误。这都归功于webDriverThreadPool列表的运用。

最后,给名为clearCookies()的方法添加了@AfterMethod,用于在每次测试之后清除浏览器的Cookie。这样就可以在不关闭浏览器的情况下将浏览器重置为中性状态,接下来就能放心地开始另一个测试了。

 

可以尝试调整BasicIT.java,添加更多测试来搜索不同的关键词。基于之前的实验,你可能已经对当前硬件的最佳平衡点有了粗略的了解。当所有测试都结束运行且浏览器全部关闭时,再次执行这些测试,并记下耗费的时长,算算执行时间是否有所减少。

就像所有事情一样,在运行测试期间,始终保持浏览器窗口打开的这一做法并不是十全十美的,不会每次都有效。

有时候,有些站点设置了Selenium无法识别的服务器端Cookie。在这种情况下,清除本地Cookie可能没有任何效果,你可能会发现关闭浏览器才是确保各个测试都有一个纯净环境的唯一方法。

如果你用的是InternetExplorerDriver,则可能会发现当使用稍旧版本的Internet Explorer(如Internet Explorer 8和Internet Explorer 9)时,测试会运行得越来越慢,最终停止运行。很遗憾,旧版本的IE并不完美,它们确实存在一些内存泄露问题。

使用InternetExplorerDriver会加剧这些问题,因为它确实给浏览器带来了压力。因此,它遭到了很多不公正的评价。其实它拥有很出色的代码,修复了大量的瑕疵。

这并不是说不能用这种方法了,也许你正在测试的应用程序中就看不到任何问题。当然,可以使用多种策略,将测试分为多个阶段;可以将能够重用浏览器的这部分测试放在第一阶段,而将那些需要重启浏览器的测试放到第二阶段。

对于在各个测试之间关闭和重启浏览器所耗费的时间,如果能将其移除,则会对测试运行的速度产生巨大的影响。根据个人经验,在任何可能的情况下,建议尽量让浏览器保持打开状态,以减少测试的耗时。

总体来说,要验证这些办法是否行之有效,唯一的途径就是结合实验和客观数据。记住,要先调查研究,然后根据各个浏览器/计算机的组合情况,调整线程的使用;或者,你应该设置一个基线,让它适用于当前环境中的所有情况。

到目前为止,我们已经将测试并行化,以便能同时运行多个浏览器实例。然而,目前我们仍然只用过一种驱动,即FirefoxDriver。前一节提到过Internet Explorer浏览器的问题,但是现在还没明确讲述使用Internet Explorer运行测试的方法。我们看看如何解决这个问题。

首先,在Failsafe插件的配置文件中添加一个名为systemPropertyVariables的新配置,并在其中建立一个名为browser的Maven属性。这和文档的描述差不多,在systemPropertyValues中定义的所有内容都将成为Selenium测试可用的系统属性。我们将使用Maven变量来引用Maven属性,这样就可以在命令行上动态修改该值。

需要对POM文件进行的修改已包含在以下代码中。

<properties>
    <project.build.sourceEncoding>UTF-
    8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <!-- Dependency versions -->
    <selenium.version>3.12.0</selenium.version>
    <testng.version>6.14.3</testng.version>
    <!-- Plugin versions -->
    <maven-compiler-plugin.version>3.7.0
    </maven-compiler-plugin.version>
    <maven-failsafe-plugin.version>2.21.0
    </maven-failsafe-plugin.version>
    <!-- Configurable variables -->
    <threads>1</threads>
    <browser>firefox</browser>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
            </configuration>
            <version>${maven-compiler-plugin.version}</version>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${maven-failsafe-plugin.version}</version>
            <configuration>
                <parallel>methods</parallel>
                <threadCount>${threads}</threadCount>
                <systemPropertyVariables>
                     <browser>${browser}</browser>
                </systemPropertyVariables>
            </configuration>
            <executions>
                <execution>
                    <goals>
                         <goal>integration-test</goal>
                         <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

现在需要创建一个包来放置驱动配置代码。在这个包中,将添加一个新的接口和一个新的枚举。我们还会将DriverFactory类转移到这个包中,以保持规范整洁(参见下面的截图)。

DriverSetup是一个非常简单的接口,后续将由DriverType类来实现,代码如下。

package com.masteringselenium.config;

import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

public interface DriverSetup {
    RemoteWebDriver getWebDriverObject(DesiredCapabilities capabilities);
}

DriverType类是完成所有工作的地方,代码如下。

package com.masteringselenium.config;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.opera.OperaOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.util.HashMap;

public enum DriverType implements DriverSetup {

    FIREFOX {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            FirefoxOptions options = new FirefoxOptions();
            options.merge(capabilities);

            return new FirefoxDriver(options);
         }
    },
    CHROME {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            HashMap<String, Object> chromePreferences = new
            HashMap<>
            ();
            chromePreferences.put("profile.password_manager_enabled"
            ,false);

            ChromeOptions options = new ChromeOptions();
            options.merge(capabilities);
            options.addArguments("--no-default-browser-check");
            options.setExperimentalOption("prefs",
            chromePreferences);

            return new ChromeDriver(options);
         }
    },
    IE {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            InternetExplorerOptions options = new
            InternetExplorerOptions();
            options.merge(capabilities);
            options.setCapability(CapabilityType.ForSeleniumServer.
            ENSURING_CLEAN_SESSION, true);
            options.setCapability(InternetExplorerDriver.
            ENABLE_PERSISTENT_HOVERING, true);
            options.setCapability(InternetExplorerDriver.
            REQUIRE_WINDOW_FOCUS, true);

            return new InternetExplorerDriver(options);
        }
    },
    EDGE {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            EdgeOptions options = new EdgeOptions();
            options.merge(capabilities);

            return new EdgeDriver(options);
         }
    },
    SAFARI {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            SafariOptions options = new SafariOptions();
            options.merge(capabilities);

            return new SafariDriver(options);
         }
    },
    OPERA {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            OperaOptions options = new OperaOptions();
            options.merge(capabilities);

            return new OperaDriver(options);
        }
    }
}

如你所见,这个基本的枚举允许选择某个Selenium支持的默认浏览器。每个枚举都实现了getWebDriverObject()方法,这使我们可以传入一个DesiredCapabilities对象,然后将其合并到相关驱动程序的Options对象中。接着,实例化WebDriver对象并返回。

 

DesiredCapabilities对象实例化<DriverType> Driver对象的做法目前已经废弃,新的做法是使用<DriverType>Options对象。只是DesiredCapabilities目前仍可在许多地方使用(例如,实例化RemoteWebDriver对象以连接Selenium-Grid仍受支持),因此还没有完全删除它。

我们看看刚才设置的默认选项,了解它们是如何使各个驱动顺利运行的。

不必使用前面提到的那些选项,它们曾在过去有效。如果你不想使用那些选项,只需要删除你不感兴趣的选项,并参照FirefoxDriver部分设置每个getWebDriverObject()方法。记住,这里只是测试框架的初始点。可以在其中添加任何对测试有帮助的选项。由于这里是实例化驱动对象的地方,因此添加在此处最合适。

既然一切都已就位,就需要重写DriverFactory方法(参见如下代码)。

package com.masteringselenium.config;

import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import static com.masteringselenium.config.DriverType.FIREFOX;
import static com.masteringselenium.config.DriverType.valueOf;

public class DriverFactory {

    private RemoteWebDriver webDriver;
    private DriverType selectedDriverType;

    private final String operatingSystem =
    System.getProperty("os.name").toUpperCase();
    private final String systemArchitecture =
    System.getProperty("os.arch");

    public DriverFactory() {
        DriverType driverType = FIREFOX;
        String browser = System.getProperty("browser",
        driverType.toString()).toUpperCase();
        try {
            driverType = valueOf(browser);
        } catch (IllegalArgumentException ignored) {
            System.err.println("Unknown driver specified,
            defaulting to '" + driverType + "'...");
        } catch (NullPointerException ignored) {
            System.err.println("No driver specified,
            defaulting to '" + driverType + "'...");
        }
        selectedDriverType = driverType;
    }

    public RemoteWebDriver getDriver() {
        if (null == webDriver) {
            instantiateWebDriver(selectedDriverType);
        }

        return webDriver;
    }

    public void quitDriver() {
        if (null != webDriver) {
            webDriver.quit();
            webDriver = null;
        }
    }

    private void instantiateWebDriver(DriverType driverType) {
        System.out.println(" ");
        System.out.println("Local Operating System: " +
        operatingSystem);
        System.out.println("Local Architecture: " +
        systemArchitecture);
        System.out.println("Selected Browser: " +
        selectedDriverType);
        System.out.println(" ");
        DesiredCapabilities desiredCapabilities = new
        DesiredCapabilities();
        webDriver =
        driverType.getWebDriverObject(desiredCapabilities);
    }
}

在代码里会执行许多操作。首先,添加一个名为selectedDriverType的新变量,它用来存储旨在运行测试的驱动类型。然后,添加一个构造函数,用于指定在实例化类时使用的selectedDriverType。构造函数会查找名为browser的系统属性,计算出所需的DriverType类型。另外还有一些异常处理代码。如果程序无法识别所需的驱动类型,则始终返回默认值,在本例中默认为FirefoxDriver。如果你的期望是每当传入无效驱动字符串时抛出异常,则可以删除此异常处理。

接下来,添加一个名为instantiateWebDriver()的新方法,它与之前在getDriver()中的代码非常相似。唯一真正的区别在于,现在我们能传入一个DriverType对象来指定所需的WebDriver对象。在这个新方法中还创建一个DesiredCapabilities对象,并将其传递给getWebDriverObject()方法。

最后,对getDriver()方法进行调整以调用新的instantiateDriver()方法。另外需要注意的是,我们不会传递WebDriver对象,而是传递RemoteWebDriver对象,因为默认情况下所有驱动都继承自RemoteWebDriver

现在进行试验。首先,执行以下代码,观察一切是否正常运作。

mvn clean verify -Dthreads=2 -
Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

这次的运行结果和上次的运行结果没有任何区别。接下来,观测异常处理的运行情况。

mvn clean verify -Dthreads=2 -Dbrowser=iJustMadeThisUp -
Dwebdriver.gecko.driver=<PATH_TO_GECKODRIVER_BINARY>

再次运行的情况应该与之前运行的情况完全相同。因为找不到名为IJUSTMADETHISUP的枚举值,所以默认使用FirefoxDriver

最后,试试另一个浏览器。

mvn clean verify -Dthreads=2 -Dbrowser=chrome

运行该浏览器可能成功也可能失败,它会尝试启动ChromeDriver,但如果你的系统上没有安装Chrome驱动程序可执行文件(默认值为$PATH),则可能会抛出异常,这说明它找不到Chrome驱动程序可执行文件。

要解决此问题,首先要下载Chrome驱动程序文件,然后使用-Dwebdriver.chrome. driver = <PATH_TO_CHROMEDRIVER_BINARY>来设定驱动程序文件的路径,就像之前对geckodriver所操作的那样。对于开发人员来说,这并没有使测试简易到开箱即用的程度。看起来我们还要更努力。

前些年我曾遇到过这个问题,但在当时还没有使用Maven轻松获取驱动程序二进制文件的方法。对于这个问题,我没有找到一个完美的解决方案,所以当时我做了任何一个开源软件爱好者都会做的事情:写一个插件来解决这个问题。

该插件允许你指定一系列驱动程序二进制文件,这些文件将自动下载,同时不需要手动进行设置。这还意味着你可以强制指定要使用的驱动程序二进制文件的版本,从而消除因驱动程序二进制文件的版本差异而导致的各种偶发性问题,毕竟不同版的驱动程序二进制文件在不同计算机上的行为可能有所不同。

现在将使用该插件来完善当前项目,新的项目结构如下图所示。

先从调整POM文件开始,我们将使用以下代码创建一些新属性,其中一个属性名为overwrite.binaries,另一个属性用于设置插件的版本。

<properties>
    <project.build.sourceEncoding>UTF-
    8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <!-- Dependency versions -->
    <selenium.version>3.12.0</selenium.version>
    <testng.version>6.14.3</testng.version>
    <!-- Plugin versions -->
    <driver-binary-downloader-maven-plugin.version>1.0.17
    </driver-binary-downloader-maven-plugin.version>
    <maven-compiler-plugin.version>3.7.0
    </maven-compiler-plugin.version>
    <maven-failsafe-plugin.version>2.21.0
    </maven-failsafe-plugin.version>
    <!-- Configurable variables -->
    <threads>1</threads>
    <browser>firefox</browser>
    <overwrite.binaries>false</overwrite.binaries>
</properties>

然后,使用以下代码添加driver-binary-downloader插件。

<plugin>
    <groupId>com.lazerycode.selenium</groupId>
    <artifactId>driver-binary-downloader-maven-plugin</artifactId>
    <version>${driver-binary-downloader-maven-plugin.version}
    </version>
    <configuration>
        <rootStandaloneServerDirectory>${project.basedir}
        /src/test/resources/selenium_standalone_binaries
       </rootStandaloneServerDirectory>
        <downloadedZipFileDirectory>${project.basedir}
        /src/test/resources/selenium_standalone_zips
        </downloadedZipFileDirectory>
        <customRepositoryMap>${project.basedir}
         /src/test/resources/RepositoryMap.xml
        </customRepositoryMap>
        <overwriteFilesThatExist>${overwrite.binaries}
        </overwriteFilesThatExist>
    </configuration>
    <executions>
        <execution>
             <goals>
                 <goal>selenium</goal>
             </goals>
        </execution>
    </executions>
</plugin>

最后,使用以下代码在maven-failsafe-plugin配置中添加一些新的系统属性。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <parallel>methods</parallel>
        <threadCount>${threads}</threadCount>
        <systemPropertyVariables>
            <browser>${browser}</browser>
             <!--Set properties passed in by the driver binary 
             downloader-->
             <webdriver.chrome.driver>${webdriver.chrome.driver}
             </webdriver.chrome.driver>
             <webdriver.ie.driver>${webdriver.ie.driver}
             </webdriver.ie.driver>
             <webdriver.opera.driver>${webdriver.opera.driver}
             </webdriver.opera.driver>
             <webdriver.gecko.driver>${webdriver.gecko.driver}
             </webdriver.gecko.driver>
             <webdriver.edge.driver>${webdriver.edge.driver}
             </webdriver.edge.driver>
        </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

默认情况下,插件在TEST_COMPILE阶段运行。它在pom.xml文件中的顺序无关紧要,因为在此阶段不会有任何实际运行的测试。新增的overwrite.binaries属性用来设置driver-binary-downloadermaven-plugin配置下的overwriteFilesThatExist选项的值。默认情况下,它不会覆盖已存在的文件。如果我们一定要下载新的文件版本或刷新现有的文件,它为我们提供了另一个选择,可以让插件强制覆盖现有文件。

另外两项配置用于指定文件路径。downloadedZipFileDirectory用于指定将驱动程序二进制文件的Zip包下载到何处,rootStandaloneServerDirectory用于指定将驱动程序二进制文件解压到何处。

接下来,我们将使用customRepositoryMap配置,令其指向customRepositoryMap. xml文件。customRepositoryMap.xml文件用于存放所有驱动程序二进制文件的下载路径。

最后,向maven-failsafe-plugin中添加了一些系统属性变量,以便在下载文件后提供这些文件的路径。driver-binarydownloader-maven-plugin将设置一个Maven变量,以指向所下载文件的路径。即使用来设置系统属性的变量并不存在,也是可行的。

这就是一个比较精妙的地方,我们已经设置过系统属性,Selenium将自动使用这些属性来查找驱动程序二进制文件的路径。这意味着我们无须再添加任何额外代码来使其生效。

现在需要创建RepositoryMap.xml文件来设定驱动文件的下载路径,同时还需要创建一个在之前未曾使用的文件夹“src/test/resources”。下方的代码创建了一个基本的RepositoryMap.xml文件,并指定了驱动文件的默认下载路径。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<root>
    <windows>
        <driver id="internetexplorer">
            <version id="3.9.0">
                 <bitrate sixtyfourbit="true">
                      <filelocation>http://selenium-
                      release.storage.googleapis.com/3.9/
                      IEDriverServer_x64_3.9.0.zip</filelocation>
                      <hash>c9f885b6a339f3f0039d670a23f998868f539e65
                      </hash>
                      <hashtype>sha1</hashtype>
                 </bitrate>
                 <bitrate thirtytwobit="true">
                      <filelocation>http://selenium-
                      release.storage.googleapis.com/3.9/
                      IEDriverServer_Win32_3.9.0.zip</filelocation>
                      <hash>dab42d7419599dd311d4fba424398fba2f20e883
                      </hash>
                      <hashtype>sha1</hashtype>
                 </bitrate>
              </version>
         </driver>
         <driver id="edge">
             <version id="5.16299">
                 <bitrate sixtyfourbit="true" thirtytwobit="true">
                      <filelocation>https://download.microsoft.com/
                      download/D/4/1/D417998A-58EE-4EFE-A7CC-
                      39EF9E020768/MicrosoftWebDriver.exe
                      </filelocation>
                      <hash>60c4b6d859ee868ba5aa29c1e5bfa892358e3f96
                      </hash>
                      <hashtype>sha1</hashtype>
                  </bitrate>
             </version>
         </driver>
         <driver id="googlechrome">
             <version id="2.37">
                 <bitrate thirtytwobit="true" sixtyfourbit="true">
                      <filelocation>
                      https://chromedriver.storage.googleapis.com/
                      2.37/chromedriver_win32.zip</filelocation>
                      <hash>fe708aac4eeb919a4ce26cf4aa52a2dacc666a2f
                      </hash>
                      <hashtype>sha1</hashtype>
                  </bitrate>
             </version>
         </driver>
         <driver id="operachromium">
             <version id="2.35">
                 <bitrate sixtyfourbit="true">
                     <filelocation>https://github.com/operasoftware
                     /operachromiumdriver/releases/download/v.2.35
                     /operadriver_win64.zip</filelocation>
                     <hash>180a876f40dbc9734ebb81a3b6f2be35cadaf0cc
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
                 <bitrate thirtytwobit="true">
                     <filelocation>https://github.com/operasoftware/
                     operachromiumdriver/releases/download/v.2.35/
                     operadriver_win32.zip</filelocation>
                     <hash>55d43156716d7d1021733c2825e99896fea73815
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
             </version>
         </driver>
         <driver id="marionette">
             <version id="0.20.0">
                 <bitrate sixtyfourbit="true">
                     <filelocation>
                     https://github.com/mozilla/geckodriver/
                     releases/download/v0.20.0/
                     geckodriver-v0.20.0-win64.zip</filelocation>
                     <hash>e96a24cf4147d6571449bdd279be65a5e773ba4c
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
                 <bitrate thirtytwobit="true">
                     <filelocation>
                     https://github.com/mozilla/geckodriver/
                     releases/download/v0.20.0/geckodriver-v0.20.0-
                     win32.zip</filelocation>
                     <hash>9aa5bbdc68acc93c244a7ba5111a3858d8cbc41d
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
             </version>
         </driver>
    </windows>
    <linux>
        <driver id="googlechrome">
            <version id="2.37">
                <bitrate sixtyfourbit="true">
                     <filelocation>https://chromedriver.storage.googl
                     eapis.com/2.37/
                     chromedriver_linux64.zip</filelocation>
                     <hash>b8515d09bb2d533ca3b85174c85cac1e062d04c6
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
            </version>
        </driver>
        <driver id="operachromium">
            <version id="2.35">
                <bitrate sixtyfourbit="true">
                    <filelocation>
                    https://github.com/operasoftware/
                    operachromiumdriver/releases/download/
                    v.2.35/operadriver_linux64.zip</filelocation>
                    <hash>
                    f75845a7e37e4c1a58c61677a2d6766477a4ced2
                    </hash>
                    <hashtype>sha1</hashtype>
                </bitrate>
             </version>
         </driver>
         <driver id="marionette">
             <version id="0.20.0">
                 <bitrate sixtyfourbit="true">
                     <filelocation>
                     https://github.com/mozilla/geckodriver/
                     releases/download/v0.20.0/geckodriver-v0.20.0-
                     linux64.tar.gz</filelocation>
                     <hash>
                     e23a6ae18bec896afe00e445e0152fba9ed92007
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
                 <bitrate thirtytwobit="true">
                     <filelocation>
                     https://github.com/mozilla/geckodriver/
                     releases/download/v0.20.0/geckodriver-v0.20.0-
                     linux32.tar.gz</filelocation>
                     <hash>
                     c80eb7a07ae3fe6eef2f52855007939c4b655a4c
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
                 <bitrate arm="true">
                     <filelocation>
                     https://github.com/mozilla/geckodriver/
                     releases/download/v0.20.0/geckodriver-v0.20.0-
                     arm7hf.tar.gz</filelocation>
                     <hash>
                     2776db97a330c38bb426034d414a01c7bf19cc94
                     </hash>
                     <hashtype>sha1</hashtype>
                 </bitrate>
             </version>
         </driver>
    </linux>
    <osx>
        <driver id="googlechrome">
            <version id="2.37">
                <bitrate sixtyfourbit="true">
                    <filelocation>
                    https://chromedriver.storage.googleapis.com/
                    2.37/chromedriver_mac64.zip</filelocation>
                    <hash>
                    714e7abb1a7aeea9a8997b64a356a44fb48f5ef4
                    </hash>
                    <hashtype>sha1</hashtype>
                </bitrate>
            </version>
        </driver>
        <driver id="operachromium">
            <version id="2.35">
                <bitrate sixtyfourbit="true">
                    <filelocation>
                    https://github.com/operasoftware/
                    operachromiumdriver/releases/download/v.2.35/
                    operadriver_mac64.zip</filelocation>
                    <hash>
                    66a88c856b55f6c89ff5d125760d920e0d4db6ff
                    </hash>
                    <hashtype>sha1</hashtype>
                </bitrate>
            </version>
        </driver>
        <driver id="marionette">
            <version id="0.20.0">
                <bitrate thirtytwobit="true" sixtyfourbit="true">
                    <filelocation>
                    https://github.com/mozilla/geckodriver/
                    releases/download/v0.20.0/geckodriver-v0.20.0-
                    macos.tar.gz</filelocation>
                    <hash>
                    87a63f8adc2767332f2eadb24dedff982ac4f902
                  </hash>
                    <hashtype>sha1</hashtype>
                </bitrate>
            </version>
        </driver>
    </osx>
</root>

 

这个文件比较大,通过键盘录入这些内容比较麻烦,更简便的办法是从GitHub官网上查看最新版本的driver-binary-downloader的README.md文件,复制并粘贴其内容。请访问GitHub官方网站,在搜索栏输入selenium-standalone-server-plugin进行搜索,在搜索结果中选择Ardesco/selenium-standalone-server-plugin进入项目页面,然后单击README.md并查阅该文件。

如果你所在的公司网络禁止访问外网,那么可以先下载文件,然后将它们放在本地文件服务器上。接下来只要更新RepositoryMap.xml,使其指向本地文件服务器即可,而不用通过Internet下载。这为你提供了极大的灵活性。

再次运行项目,检查一切是否正常运作。首先,执行以下代码。

mvn clean verify -Dthreads=2

你会发现一切都能正常运行,即使我们并没有在命令行上对webdriver.gecko. driver系统属性进行实际的设置。接下来看看选择chrome时是否依然能正确运行,请执行以下代码。

mvn clean verify -Dthreads=2 -Dbrowser=chrome

这一次打开的不再是Firefox浏览器,而会同时打开两个Chrome浏览器。你也许还会注意到,在上一次运行时,由于要下载一系列驱动文件,因此其运行速度可能会因此减缓。因为这一次这些文件已经下载过了,所以仅验证了它们是否存在,测试的运行速度将会更快。我们也无须设置任何系统属性,先前已经在POM文件中进行了插件配置,一切将自动完成。

现在,我们可以让任何人都访问这些代码,只需要签出并运行代码,测试就会正常运行。

因为后台模式似乎已经火热了很长一段时间了,所以我们看看如何在这个不断完善的框架中添加对后台浏览器的支持。

实际上,这项改动相对比较容易。首先,修改POM文件。使用以下代码添加<headless>属性(这里会将其设置为true,假设你总是希望在后台模式下开始运行)。

<properties>
    <project.build.sourceEncoding>UTF-
    8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <!-- Dependency versions -->
    <selenium.version>3.12.0</selenium.version>
    <testng.version>6.14.3</testng.version>
    <!-- Plugin versions -->
    <driver-binary-downloader-maven-plugin.version>1.0.17
    </driver-binary-downloader-maven-plugin.version>
    <maven-compiler-plugin.version>3.7.0
    </maven-compiler-plugin.version>
    <maven-failsafe-plugin.version>2.21.0
    </maven-failsafe-plugin.version>
    <!-- Configurable variables -->
    <threads>1</threads>
    <browser>firefox</browser>
    <overwrite.binaries>false</overwrite.binaries>
    <headless>true</headless>
</properties>

然后,将其传入maven-failsafe-plugin

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <parallel>methods</parallel>
        <threadCount>${threads}</threadCount>
        <systemPropertyVariables>
            <browser>${browser}</browser>
            <headless>${headless}</headless>
            <!--Set properties passed in by the driver binary downloader-->
            <webdriver.chrome.driver>${webdriver.chrome.driver}
            </webdriver.chrome.driver>
            <webdriver.ie.driver>${webdriver.ie.driver}
            </webdriver.ie.driver>
            <webdriver.opera.driver>${webdriver.opera.driver}
            </webdriver.opera.driver>
            <webdriver.gecko.driver>${webdriver.gecko.driver}
            </webdriver.gecko.driver>
            <webdriver.edge.driver>${webdriver.edge.driver}
            </webdriver.edge.driver>
        </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最后,将更新DriverType枚举,以读取新系统属性“headless”,并将其应用于CHROMEFIREFOX中,代码如下。

package com.masteringselenium.config;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.opera.OperaOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.util.HashMap;

public enum DriverType implements DriverSetup {

    FIREFOX {
        public RemoteWebDriver getWebDriverObject
        (DesiredCapabilities capabilities) {
            FirefoxOptions options = new FirefoxOptions();
            options.merge(capabilities);
            options.setHeadless(HEADLESS);

            return new FirefoxDriver(options);
        }
    },
    CHROME {
        public RemoteWebDriver getWebDriverObject
        (DesiredCapabilities capabilities) {
            HashMap<String, Object> chromePreferences =
            new HashMap<>();
            chromePreferences.put("profile.password_manager_enabled"
            , false);

            ChromeOptions options = new ChromeOptions();
            options.merge(capabilities);
            options.setHeadless(HEADLESS);
            options.addArguments("--no-default-browser-check");
            options.setExperimentalOption("prefs",
            chromePreferences);

            return new ChromeDriver(options);
        }
    },
    IE {
        public RemoteWebDriver getWebDriverObject
        (DesiredCapabilities capabilities) {
            InternetExplorerOptions options = new
            InternetExplorerOptions();
            options.merge(capabilities);
            options.setCapability(CapabilityType.ForSeleniumServer.
            ENSURING_CLEAN_SESSION, true);
            options.setCapability(InternetExplorerDriver.
            ENABLE_PERSISTENT_HOVERING, true);
            options.setCapability(InternetExplorerDriver.
            REQUIRE_WINDOW_FOCUS, true);

            return new InternetExplorerDriver(options);
        }
    },
    EDGE {
        public RemoteWebDriver
        getWebDriverObject(DesiredCapabilities
        capabilities) {
            EdgeOptions options = new EdgeOptions();
            options.merge(capabilities);

            return new EdgeDriver(options);
        }
    },
    SAFARI {
        public RemoteWebDriver getWebDriverObject
        (DesiredCapabilities capabilities) {
            SafariOptions options = new SafariOptions();
            options.merge(capabilities);
            return new SafariDriver(options);
        }
    },
    OPERA {
        public RemoteWebDriver getWebDriverObject
        (DesiredCapabilities capabilities) {
            OperaOptions options = new OperaOptions();
            options.merge(capabilities);

            return new OperaDriver(options);
        }
    };

    public final static boolean HEADLESS =
    Boolean.getBoolean("headless");
}

现在用与之前完全相同的方式来运行项目。

mvn clean verify -Dthreads=2

测试再次启动,但这一次不会看到任何浏览器窗口弹出。一切都会像之前一样正常运行,测试如同预期的那样通过了。如果你想让浏览器窗口再次弹出,可以设置headless为false

mvn clean verify -Dthreads=2 -Dheadless=false

你可能已经发现,本节一直没有提及GhostDriver或PhantomJS,这是因为PhantomJS不再处于主动开发状态,而GhostDriver不再拥有核心维护人员。PhantomJS仍然可用,而且可能依然能让GhostDriver启动并运行。然而,用它们进行测试,会存在一些问题。

随着ChromeDriver和FirefoxDriver相继发布了支持后台模式的版本,继续使用PhantomJS就不再有意义了。在PhantomJS的鼎盛时期,它曾是非常强大的工具,但现在它已不再是使用Selenium的有效工具了。

本章讲解了如何使用Maven设置一个基本项目,下载依赖项,配置类路径以及构建代码。你将能够在TestNG中通过同一浏览器的多个实例并行运行测试,以及使用Maven插件自动下载驱动程序文件,使测试代码便于使用。你已了解如何在测试中使用合适的线程数,以及在必要时重写该数目。最后,你学会了如何在后台模式下运行Firefox和Chrome浏览器,这样就可以在本地不间断运行测试,而无须在CI服务器上使用桌面环境来运行。

下一章将讲述当测试出错时的处理方式,并展示如何在多个测试同时运行的情况下对问题进行跟踪。


为了使工作更加顺利,本章将介绍测试失败时的处理方式。本章探讨的主题如下。

许多公司仍设有分立的测试和开发团队,这显然不是一个理想的状况,因为测试团队通常无法完全了解开发团队正在构建的东西。如果该测试团队的任务是在Web前端编写自动化功能测试,则会面临更多额外的挑战。

问题在于,测试团队的进展通常落后于开发团队,落后程度取决于开发团队发布的频率。比开发团队究竟落后了多少并不是关键,而是一旦处于落后状态,就只能一直追赶。一旦玩起了追赶竞赛,就只能不断更新脚本,使它们能够与最新的软件版本一同工作。

 

有人可能会将“修复脚本使其支持新功能”这一行为称为重构。这是错误的。重构是指重新写代码,使其更简洁、更高效。而实际代码(或者测试脚本)的功能不会改变。如果对代码的功能进行了更改,那么就不能称为重构。

虽然保持脚本不断更新并不一定是件坏事,但每当有新版本代码发布时,都中断测试就非常糟糕了。如果这些测试总是无缘无故地罢工,测试人员就不会再信任它们。当他们看到构建失败时,会下意识地认为这是由测试导致的其他问题,而不是当前被测试的Web应用程序的问题。

我们需要找到一种方法来避免测试总是毫无原因地失败。从一些不太容易引发争议的事情开始:先确保测试代码始终与应用程序代码位于同一个代码库中。

这样有何帮助?

如果测试代码与应用程序代码位于同一个库中,那么所有开发人员都可以访问它。在上一章中,我们了解了如何使开发人员易于签出并运行测试。如果我们能确保测试代码与应用程序代码位于相同的代码库中,也就确保了该应用程序的开发人员将自动复制测试代码。这意味着,你现在只需要给开发人员提供一条可执行命令,他们就能利用这些本地代码运行测试,并查看是否有Bug。

将测试代码放在与应用程序代码相同的库中,还有另一个好处:开发人员可以完全访问这些测试代码。他们可以了解测试代码工作原理,并在更改应用程序功能的同时更改测试。理想的情况是,开发人员对系统所进行的每一次更改也会导致测试的更改,以保持它们的同步。这样,下一次发布应用程序时,测试就不会无缘无故地失败,测试也不再仅局限于进行自动回归检查了。它们将转变为实时文档,而这个实时文档属于整个团队,可以由整个团队更新,使其始终能描述当前版本的应用程序是如何工作的。

“实时文档”代表什么意思?随着应用程序的不断构建,需要不断编写自动化测试以确保它能满足特定的标准。而这些测试有许多不同的形式和大小,从单元测试到集成测试,以及端到端功能测试等。在某种程度上所有的测试都是在描述应用程序如何工作的。不得不承认,这听起来就像文档一样。

这个文档可能并不完美,但这无法反驳它成为文档的事实。假设某个应用程序拥有一些单元测试,可能还拥有一些写得很烂的端到端测试。这如同在外国买入的某个廉价电子产品的文档一样。它附带一本手册,里面多半会有一小部分写得很烂的英文说明,讲的内容也不够详细。这很可能因为它最初就是为国内市场生产的,从来没有打算要出口到国外。配套的许多文档都是用一种你根本看不懂的语言书写的,但这对说那种语言的人很实用。这并不意味着产品不好,只是在遇到问题时很难处理。大多数情况下,可以在不使用手册的情况下解决问题。但如果问题很复杂,你可能要找一个会说当地语言的人或者知道产品是如何工作的人,让他们解释给你听。

当将测试作为一种实时文档进行讨论时,在不同的测试阶段,它是适用于不同人群的文档。单元测试本质上是技术性很强的内容,详尽地解释了系统的各个部分是如何工作的。如果你把它和电子产品的说明书进行比较,单元测试就像是附录中的技术规格,提供了绝大部分消费者都不关心的许多深度信息。集成测试就像是说明如何将你的电子设备与其他电子设备相连的那部分手册。如果你需要将一个电子设备连接到另一个电子设备,这些说明将非常实用,但如果你没这个打算,则可能就不太关心这些。最后,功能性端到端测试也是文档的一部分,它们用于实际讲述如何使用该设备,这是普通用户阅读得最多的那部分手册(他们可不关心技术细节)。

我认为在编写自动化测试时,最重要的事情之一是确保它们是优质的文档。这意味着你应描述被测试应用程序的所有部分是如何工作的。或者,换句话说,它应拥有高级别的测试覆盖率。不过,最难的部分是让那些不懂技术的人也能理解这些测试。这就是领域特定语言(domain-specific language,DSL)的用武之地,你可以将测试的内部工作原理隐含在人们能够理解的语言中。好的测试就像优质的文档,如果它们确实很好,则会用清晰易懂的语言来描述事物,而读者也无须从任何地方寻求帮助;另一方面,糟糕的测试就像是从另一种语言翻译过来的指令,无论好坏,它们都有意义。

那么,为什么它是实时文档,而不只是普通文档呢?因为每当被测试的应用程序发生变化时,自动化测试也会随之改变。它们随着产品的发展而发展,并继续解释它在当前状态下是如何工作的。如果构建通过,这些文档将继续描述系统当前的工作方式。

不要仅把自动化测试看作回归测试,回归测试只用来检查功能是否有改变,应将自动化测试看作描述产品工作方式的实时文档。如果有人来问你某样东西是如何工作的,最好能给他们看一个能解答这些问题的测试。如果做不到这一点,那么这些文档是不完善的。

那么什么时候做回归测试?答案是不做回归测试。我们不需要回归测试阶段,这些测试文档会实时讲述产品是如何工作的。当产品的功能发生变化时,测试就会更新,告诉我们最新功能如何工作。而旧功能的现有文档不会改变,除非其功能发生了变化。

测试文档同时涵盖了回归部分和新功能部分。

对于自动化测试来说,测试的可靠性是极为关键的一部分。如果测试不可靠,那么它们将不受信任,这将产生深远的影响。我相信测试人员都曾在这样的环境中工作过,因为种种原因,导致测试可靠性一直很差。我们看看下面两种场景。

测试不可靠的一个常见原因是设立了专门的测试自动化团队,且该团队与应用程序开发团队是分开的。应尽量避免这种情况,因为自动化测试团队只能不断追赶。开发团队会不断推出需要进行自动化测试的新特性,但自动化测试团队根本不知道接下来会发生什么。通常,当测试中断时,他们才知道现有特性已经变了。除了补救性修复测试之外,他们还要搞清楚新功能是什么,了解新功能是否按照预期运行。

这通常会引发一种现象,测试管理者会意识到他们没有足够的时间来做完所有的事情,便会寻求减轻工作压力的方法,于是便进行了“仔细权衡”。

在这种情况下,通常会听到这种建议:是时候降低自动化测试的通过标准了。“只要95%的自动化测试通过,就应该没问题,我们已经达到了很高的覆盖率,那5%的失败可能是由于系统变更所导致的,我们暂时没有时间去处理它们。”一开始人们都很高兴,他们继续进行自动化的工作,确保95%的测试都通过,但是很快通过率就会降到95%以下。几周后,他们做出了一个务实的决定,将通过标准降低到90%,然后是85%,接着是80%。不知不觉中,失败已经遍布测试,你已经无法分辨应用程序中的哪些故障是合理的问题,哪些是预期的故障,哪些是间歇性故障。

当测试失败得一塌糊涂时,不会再有人关注它正确与否,他们只会讨论那个神奇的测试通过率——80%:这已经是一个很高的数字了,如果这么多测试已经通过,那说明产品还是合格的,不是吗?如果测试通过率低于80%,则要调整一些失败的测试,让它们通过。这通常是轻而易举的事情,毕竟这时已经腾不出时间去解决那些真正棘手的问题了。

我不想打击他们,但如果事情已经发展到这个程度,那么自动化测试肯定是失败的了,没有人会信任这些测试。不要只看80%这个数字,需要看到问题的另一面:有20%的网站功能没有按照预期工作,而你连为什么都不知道!难道要阻止开发人员编写新代码,才能解决当前所处的混乱局面吗?到底是如何变成这样的?正是因为他们觉得测试可靠性无关紧要,这项失误才会造成如此严重的后果。

不论是在孤立的自动化团队中,还是在团队成员一块工作的混合团队中,都可能出现这种场景。你可能曾经见过这种不完全可靠的自动化测试,它就是偶尔会无缘无故地失败、时好时坏的那种测试。因为有人可能检查过并且找不出失败的原因,所以测试人员都忽略它。现在每当它再度失败时,就有人说:“又是那个时好时坏的测试,别管它,它很快就会变绿的。”

 

时好时坏的测试是指在没有明显原因的情况下偶发性地失败并且之后再次运行又会通过的测试。这种测试有着五花八门的称呼,包括易碎测试、随机失败、不稳定测试,或者公司内部特有的其他名称。

现在真正的问题是:测试不会无缘无故地时好时坏。这个测试拼命地想告诉测试人员一些事情,而测试人员却选择忽略。它想表达什么?除非深入挖掘其失败原因,否则将无从得知,能看到的只是冰山的一角。引起问题的可能性很多,下面列举了一些。

当测试出现时好时坏的情况时,测试人员尚不清楚问题出在哪里,但是关键在于绝不能自欺欺人——它就是有问题。如果你不解决它,时机一到,会有更多麻烦。

假设你有一个正在测试的软件,其功能是交易股票,因为公司必须在竞争中处于优势地位,所以每天都要发布新版本。而从一开始就有一个时好时坏的测试,有人曾对其进行了检查,结果未发现任何代码问题,仅是测试不可靠而已。于是测试人员默许了这种情况,现在只在它标红时快速进行手工检查。现在又有一段新代码签入,那个本就不稳定的测试再次标红,不过人们已经对这条测试的时好时坏的情况习以为常,于是快速执行了手工测试:看起来一切都正常,所以再次忽略了它。发布继续进行,但很快出现了问题:交易软件突然开始在应该买进的时候卖出,在应该卖出的时候买进。未及时发现该问题,因为软件已经通过测试,所以不应该出现问题。一小时以后,一切都乱套了,这个软件误清了所有仓位,又买进了一堆垃圾股。短短一小时里,公司已经损失了一半的市值,怎么做都无力回天了。于是大家开始调查原因,这次发现这个时好时坏的测试并非无缘无故地失败,它确实出了问题,但该问题在执行快速手工检查时并不明显。接下来所有的目光都转向你:你是这些代码的检验人,理应阻止这轮发布,责任是推卸不掉的。要是那条邪恶的测试从一开始就没有时好时坏该多好。

之前的场景示例比较夸张,但希望你能明白这一点:时好时坏的测试是危险的,不应该置之不理。

理想情况下,我们应该处于这样一种状态:每当有测试失败时,都意味着对系统进行了未归档的更改。对于未归档的更改,我们应该怎么办?这取决于是不是有意要进行更改。如果不希望进行修改,则回滚;如果确实要进行更改,则更新文档(也就是自动化测试)来支持它。

如何才能加强测试可靠性,确保各项更改尽早发现?

我们可以要求开发人员在每次推送代码前运行测试,但有时他们会忘记这回事,也可能根本没忘,只是感觉当前更改的内容比较少,似乎不值得对如此小的更改进行完整的测试。(你是否曾听到有人说过“就只是几句CSS更改而已……”?)请确保在每次将代码推送到中央源码库之前,都必须运行测试且通过测试,纪律至上。

如果团队不守纪律怎么办?如果仍然提交了很容易发现的错误,甚至已经明文规定过在向中央库推送代码之前必须运行测试,结果还没起作用,又该怎么办呢?如果实在找不到其他可行的办法,我们还可以与开发人员商讨,强制执行下述规定。

这其实非常简单:大多数源码管理(Source Code Management,SCM)系统都支持钩子。当使用特定SCM方法时,将自动触发操作。我们看看如何在一些最常用的SCM系统中实现钩子。

首先,需要复制一个项目。如果你愿意,可以在GitHub上创建一个全新项目用于复制。将第1章中编写的代码存放到该项目库中来进行实验,不失为一个好主意。

一旦从GitHub上复制了某个项目,下一步就是切换到SCM根文件夹。Git创建了一个名为.git的隐藏文件夹,用于保存关于项目的所有信息,Git需要这些信息来工作。接下来切换到该文件夹,然后通过以下代码切换到hooks子文件夹。

cd .git/hooks

Git有一系列预定义的钩子名称。无论何时,一旦执行了Git命令,Git都将在hooks文件夹中查找是否有文件与预定义的钩子名匹配,这些钩子将作为该命令的结果触发。如果有匹配的文件,Git将运行它们。在将代码推送到Git之前,我们要确保构建项目并运行所有测试。为此,添加一个名为pre-push的文件。添加pre-push文件后,在该文件中填入以下代码。

#!/usr/bin/env bash 
mvn clean install

现在,每当使用git push命令时,都会触发这个钩子。

 

关于Git钩子,有一点需要注意:钩子对于每个用户都是独有的,它们不受推送或拉取的库的控制。如果你想给使用代码库的开发人员自动安装钩子,则需要想其他办法。例如,编写一个脚本,该脚本的执行将作为构建的一部分,用于将钩子文件复制到.git/hooks文件夹中。

理论上还可以添加一个pre-commit钩子,但我们并不关心代码是否能在开发人员的本地计算机上工作(他们可能正在进行某项更改,但只改到一半,仅仅提交代码防止丢失)。我们真正关心的是,当代码被推送到中央源码库时,它能正常运行。

 

如果你是Windows用户,可能会翻阅之前提到的代码,并认为它和*nix系统上的代码非常相似。不用担心——Windows版的Git会安装Git bash,用于解释这些脚本,所以它也可以在Windows系统上工作。

Subversion(SVN)钩子稍微复杂一些,这一定程度上取决于系统配置。钩子存储在svn库中一个名为hooks的子文件夹中。与Git一样,它们需要命名为特定的名称(SVN手册中提供了完整的名称列表)。基于我们的目标,我们只对pre-commit钩子感兴趣。先从一个基于*nix的环境开始。首先,需要创建一个名为pre-commit的文件。然后,在其中填入以下代码。

#!/usr/bin/env bash
mvn clean verify

如你所见,它看起来与Git钩子脚本基本相同,但是这可能有些问题。SVN钩子是在一个空环境中运行的,因此,如果你通过环境变量使mvn成为一个可识别的命令,那么它可能无法正常工作。如果/usr/bin/usr/local/bin/中带符号链接,则应该没问题;如果没有,则可能需要为mvn命令指定一个绝对文件路径。

现在,还需要让这个钩子适用于Windows用户。方法类似,但是由于在不同操作系统中SVN查找的文件也不相同,因此这次文件需要命名为pre-commit.bat

mvn clean verify

文件的内容和*nix的实现相似,只是不再需要bash的shebang符号(也就是#!)。由于Windows也存在同样的空环境问题,因此,你可能要再次提供安装Maven的绝对路径。希望用Windows系统进行开发的用户都把Maven安装在同一个地方。

 

请注意,这种钩子并非绝对可靠。如果你忘记在自己的计算机上提交本地更改,这些测试可能会通过,但是当你将代码推送到中央源码库时,部分更改将会丢失。如果此时其他人运行了最新版本的代码,则可能会构建失败。与所有办法一样,这不是一颗银弹,但肯定会有所帮助。

现在,我们已经能确保在推送代码至中央源码库之前运行测试,这样便可以捕获绝大多数错误,但事情仍不算完美,某个开发人员可能忘记提交代码更改。在这种情况下,测试将在其本地计算机上运行并通过,但是源码控制中将缺少某个能使更改正常工作的重要文件。这是“在我的计算机上测试正常运行”的问题的原因之一。

即便提交了所有文件并测试通过,但开发人员的机器环境与部署代码的生产环境可能完全不同。这是“在我的计算机上测试正常运行”的问题的主要原因。

尽管我们都已尽力确保一切正常,但如何才能进一步降低这些风险,并确保在出现问题时能够迅速发现问题的根源?

那些只在开发用的计算机上构建代码和执行测试时才可能遇到的问题,通过持续集成这种办法能得以有效解决。持续集成服务器会持续监视源码库,每当检测到有变化时,将会触发一系列操作。第一个操作是构建代码,在构建代码时运行所有可用的测试(通常是单元测试),然后创建一个可部署的工件。接着,该工件通常将部署到作为实时环境副本的服务器上。一旦代码部署到服务器,就会在服务器上执行剩余的测试,以确保一切正常。如果它没有按照预期工作,构建就会失败,开发团队将收到通知,接着便要修复问题。需要注意的是,我们只构建了一次工件。如果需要多次重新进行构建,则每次测试的工件可能是不同的(比如,可能是用不同版本的Java构建的,也可能是它应用了不同的属性,也可能是其他状况)。

通过持续集成,我们寻求的工作流程如下图所示。

大多数持续集成系统还具有大型的可视化仪表,以便人们随时了解构建的状态。如果屏幕标出红色,应暂停手上的事情,尽快修复问题。

我们看看在持续集成服务器上运行测试有多么容易。接下来讲述的并不是持续集成全部功能的设置,只讲一讲我们使用的那部分,用于运行目前所建立的测试。

我们要做的第一件事是设定Maven配置文件,以便把Selenium测试与构建的其他部分隔离开来,这样就可以将其放在持续集成服务器上独立的UI测试块中。对POM的更改非常简单,只需要用一个profile代码段把<build>和<dependencies>代码段包装起来。代码如下。

<profiles>
    <profile>
        <id>selenium</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>com.lazerycode.selenium</groupId>
                    <artifactId>driver-binary-downloader-maven-
                    plugin</artifactId>
                    <version>${driver-binary-downloader-maven-
                    plugin.version}</version>
                    <configuration>
                        <rootStandaloneServerDirectory>
                        ${project.basedir}/src/test/
                        resources/selenium_standalone_binaries
                        </rootStandaloneServerDirectory>
                        <downloadedZipFileDirectory>
                        ${project.basedir}/src/test/
                        resources/selenium_standalone_zips
                        </downloadedZipFileDirectory>
                        <customRepositoryMap>${project.basedir}
                        /src/test/resources/RepositoryMap.xml
                        </customRepositoryMap>
                        <overwriteFilesThatExist>
                        ${overwrite.binaries}
                        </overwriteFilesThatExist>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>selenium</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>${maven-failsafe-plugin.version}
                    </version>
                    <configuration>
                        <parallel>methods</parallel>
                        <threadCount>${threads}</threadCount>
                        <systemPropertyVariables>
                            <browser>${browser}</browser>
                            <headless>${headless}</headless>
                            <!--Set properties passed in by the 
                            driver binary downloader-->
                            <webdriver.chrome.driver>
                            ${webdriver.chrome.driver}
                            </webdriver.chrome.driver>
                            <webdriver.ie.driver>
                            ${webdriver.ie.driver}
                            </webdriver.ie.driver>
                            <webdriver.opera.driver>
                            ${webdriver.opera.driver}
                            </webdriver.opera.driver>
                            <webdriver.gecko.driver>
                            ${webdriver.gecko.driver}
                            </webdriver.gecko.driver>
                            <webdriver.edge.driver>
                            ${webdriver.edge.driver}
                            </webdriver.edge.driver>
                        </systemPropertyVariables>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

如你所见,已经创建了一个名为selenium的配置文件。这将使我们能进行开关控制,决定是否把Selenium测试作为构建的一部分来执行。如果你现在激活selenium配置文件,就会只执行Selenium测试。

mvn clean verify -Pselenium

还可以,通过以下命令专门阻止Selenium测试的运行。

mvn clean verify -P-selenium

请注意,添加了<activeByDefault>true</activeByDefault>,如果命令行上没有明确指定配置,则activeByDefault将确保该配置默认激活,因此可直接执行以下命令。

mvn clean verify

之前这条命令将Selenium测试作为正常构建的一部分来运行,之前设置的SCM钩子也会生效。

接下来看看两种较为流行的持续集成服务器——TeamCity和Jenkins。由于TeamCity在许多企业环境中较受欢迎,因此对它有一个基本的了解是很有用的。Jenkins则功能丰富,即便你现在还未使用Jenkins,在其后职业生涯的某个阶段可能仍会安装Jenkins。

为何要如此深入地探讨持续集成呢?持续集成与Selenium有什么关系?

首先,在CI服务器上设置Maven项目非常容易,使用Maven等构建/依赖管理工具的益处很多。

其次,有一两种CI服务器的设置经验总是好的,日后总有使用它们的可能。

最后,CI服务器有一些开箱即用的限制。我们将了解如何以最小代价解决这些问题。

TeamCity是一个企业级持续集成服务器。它支持许多开箱即用的技术,非常可靠和强大。我最喜欢的功能之一是能够启动Amazon Web ServiceAWS)云构建代理。你需要创建构建代理Amazon Machines ImageAMI),一旦完成此操作,TeamCity服务器就可以启动任意数量的构建代理,然后在构建结束后关闭它们。

需要在本地计算机上完成TeamCity的基本安装,才能进行本节的操作。可以下载WAR文件,并在Tomcat等应用服务器上运行它。如果你的计算机上安装了Docker,也可以运行以下命令(需要在本地计算机上创建~/teamcity/data~/teamcity/logs目录)。

docker run -it --name teamcity-server-instance \
 -v ~/teamcity/data:/data/teamcity_server/datadir \
 --v ~/teamcity/logs:/opt/teamcity/logs \
 --p 8111:8111 \
 -jetbrains/teamcity-server

要访问TeamCity实例,请在浏览器地址栏中输入以下URL。

http://localhost:8111

首次启动TeamCity时,将有标准的设置过程。只需要单击Proceed,进入创建管理员账户的界面。生成一个管理员账户(例如,临时将用户名设为admin,密码设为admin),然后单击左上角的Projects按钮,会看到以下界面。

(1)单击Create project按钮。

(2)命名项目,也可以添加一些描述,以说明项目的用途(见下图)。请记住,我们正在创建的项目并不是实际构建版本,它将用来保留该项目的所有构建信息。Selenium Tests可能不是一个好的项目名称,但这就是我们目前所拥有的东西。

(3)单击Create按钮,之后将看到已创建的项目。

(4)向下滚动到Build Configurations区域(见下图)。

(5)进入此区域后,单击Create build configuration按钮。

这里是创建生成配置的地方。这里将其简单命名为Webdriver,因为它会用来运行WebDriver测试(当然,可以给生成配置起一个更好的名字),参见下面的截图。

(6)当设置完配置的名称时,请单击Create按钮。

(7)配置源码控制系统,使TeamCity可以监视源码控制的变更。如下图所示,这里选择的是Git,并填入了一些参考值,但显然你需要填入与自己的源码控制系统相关的值。

(8)一旦填写完详细信息并单击Create按钮后,就要添加构建步骤,如下图所示。单击Add build step按钮开始下一步。

(9)弹出进行构建的主界面(见下图)。

(10)选择构建类型。在本例中,因为用的是Maven项目,所以这里选Maven(见下图)。为Maven构建填入详细信息。这里简单填了“clean verify”,由于之前已经确保Selenium配置文件默认运行,因此现在无须查看Advanced options

(11)向下滚动,单击Save按钮,TeamCity构建就已准备就绪。现在只需要确保每次将代码签入源码库时都会触发它。

(12)单击Triggers。在下图中,可以设置一个操作列表,它们将引发构建的执行。请单击Add new trigger按钮并选择VCS Trigger

(13)如果单击Save按钮(见下图),将设置一个触发器。每次将代码推送到中央源码库时都会触发构建。

Jenkins是持续集成(Continue Integration,CI)领域最受欢迎的公司之一,也是一些云服务的基础(如cloudbees)。它的应用非常广泛,有关持续集成的内容几乎都会提及它。

需在本地计算机上完成Jenkins的基本安装,才能进行本节的操作。请按照以下步骤进行。

(1)如果你已经安装了Tomcat等应用服务器,那么接下来只需要下载WAR文件,并将其放到应用服务器的webapps目录下即可。如果你决定使用Docker路由,则可以运行以下命令(需要在本地计算机上创建~/jenkins目录)。

docker run -it --name jenkins-instance \
 -p 8080:8080 \
 -p 50000:50000 \
 -v ~/jenkins:/var/jenkins_home \
 Jenkins

(2)要访问Jenkins 实例,请在浏览器地址栏中输入以下URL。

http://localhost:8080

(3)首先将看到一个界面(见下图),它要求你解锁Jenkins。

(4)查看终端窗口(假设你用的是Docker),将看到如下界面。

(5)从终端复制密码,并将其输入解锁界面,然后单击Continue按钮。它将询问你打算安装哪些插件,暂时先使用Jenkins的默认建议。Jenkins将下载所需的插件,并为首次使用做好准备。最后一个设置步骤是创建一个管理员账号(可以再次临时将用户名设为admin,密码设为admin)。

(6)现在Jenkins已经准备就绪,你将看到Welcome to Jenkins界面。

我们看看如何创建Jenkins构建,以便能运行测试。

(1)单击上一个界面中的create new jobs链接,弹出以下界面。

(2)填入构建名称,然后选择Freestyle project选项。

(3)单击OK按钮进入构建配置界面,再单击Source Code Management选项卡,选择Git并填入详细信息(见下图)。

 

当然,你可以使用自己喜欢的源码管理系统。Git非常流行,但是Jenkins也支持许多其他工具。

(4)在以下界面中设置构建触发器和构建环境。最好设置Git钩子,以便每次向源码控制提交更改时都触发构建。

(5)设置Maven job。单击Add build step按钮,然后选择Invoke top-level Maven targets(见下图)。

(6)只需要填入默认的Maven目标,即可完成设置(参见下图)。

(7)单击上一个界面中的Save按钮,会返回刚创建的项目。现在总算大功告成了,截图如下。

现在应该可以运行Jenkins构建了,它将下载所有的依赖项,并运行所有相关内容。

到目前为止,我们已经了解了如何创建一个非常简单的持续集成服务,但这仅是冰山的一角。我们已经使用持续集成来提供快速反馈循环,这样便可在问题出现时,迅速收到通知,并及时做出响应。如果扩展该服务,使它不仅能告知我们是否存在问题,还能告知我们是否准备将某些内容部署到生产环境,将会怎样?这是持续交付的目标(参见下图)。

持续交付的下一步是什么?答案是持续部署。它是如何实现的?只要各持续交付阶段被标记为已通过,代码就会自动部署到现场。想象一下,每当有新功能即将完成时,短短几小时内我们就能对该功能进行充分测试,随后就能将其自动发布到现场。

从写完代码到呈递至用户手中,一天之内即可完成。流程参见下图。

目前还未实现上述目标。我们目前已经有一个基本的CI设置,以便在CI上运行测试,但是这仍然有很大的差距。到目前为止,这项基本的CI设置还只运行在一个操作系统上,无法在所有浏览器/操作系统的组合上运行测试。可以通过设置各种构建代理来处理这个问题,这些构建代理连接到CI服务器并运行不同版本的操作系统/浏览器。然而,这需要更多时间来进行配置,而且还相当复杂。可以通过设置Selenium-Grid来扩展CI服务器的功能,CI服务器可以连接到Selenium-Grid并运行各种Selenium测试。其功能非常强大,但也有设置成本。在这里可以使用第三方服务,如SauceLabs。大多数第三方Grid服务都有免费版,但这在刚入门或在刚接触其功能时非常有用。请记住,使用第三方服务并不会将你绑定在这些服务中。由于每款Selenium-Grid几乎都一样,因此即便开始使用第三方服务,也无法阻碍你构建自己的网格,也可以配置自己的构建代理,准备以后脱离第三方服务。

之前我们已经有了一个可用的Maven实现,接下来将对其扩展,以便它能连接到Selenium-Grid。这些扩展的功能将使你能连接到任何Selenium-Grid,不过我们先关注如何连接到SauceLabs提供的第三方服务上,毕竟它提供了免费版。现在展示需要对TestNG代码进行哪些修改。

先从修改POM文件开始。首先,要添加一些属性,可使用以下代码在命令行上进行配置。

<properties>
     <project.build.sourceEncoding>UTF-
     8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-
     8</project.reporting.outputEncoding>
     <!-- Dependency versions -->
     <phantomjsdriver.version>1.4.3</phantomjsdriver.version>
     <selenium.version>3.5.3</selenium.version>
     <testng.version>6.11</testng.version>
     <!-- Plugin versions -->
     <driver-binary-downloader-maven-plugin.version>1.0.14</driver-
     binary-downloader-maven-plugin.version>
     <maven-failsafe-plugin.version>2.20</maven-failsafe-
     plugin.version>
     <!-- Configurable variables -->
     <threads>1</threads>
     <browser>firefox</browser>
     <overwrite.binaries>false</overwrite.binaries>
     <remote>false</remote>
     <seleniumGridURL/>
     <platform/>
     <browserVersion/>
</properties>

因为每个人的Selenium-Grid URL不同,所以seleniumGridURL未填入内容,你可以给它赋一个默认值,同样还有platformbrowserVersion属性。接下来,需要确保这些属性由maven-failsafe-plugin设置(作为测试JVM中的系统属性)。为此,需要使用以下代码修改maven-failsafe-plugin配置。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
      <parallel>methods</parallel>
      <threadCount>${threads}</threadCount>
      <systemProperties>
           <browser>${browser}</browser>
           <remoteDriver>${remote}</remoteDriver>
           <gridURL>${seleniumGridURL}</gridURL>
           <desiredPlatform>${platform}</desiredPlatform>
           <desiredBrowserVersion>
            ${browserVersion}
           </desiredBrowserVersion>
           <!--Set properties passed in by the driver binary downloader-->
           <phantomjs.binary.path>${phantomjs.binary.path}
           </phantomjs.binary.path>
           <webdriver.chrome.driver>${webdriver.chrome.driver}
           </webdriver.chrome.driver>
           <webdriver.ie.driver>${webdriver.ie.driver}
           </webdriver.ie.driver>
           <webdriver.opera.driver>${webdriver.opera.driver}
           </webdriver.opera.driver>
           <webdriver.gecko.driver>${webdriver.gecko.driver}
           </webdriver.gecko.driver>
           <webdriver.edge.driver>${webdriver.edge.driver}
           </webdriver.edge.driver>
      </systemProperties>
    </configuration>
    <executions>
      <execution>
           <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
           </goals>
      </execution>
    </executions>
</plugin>

现在可以使用System.getProperty()将这些属性提供给测试代码。现在,需要对DriverFactory类进行一些修改。首先,使用以下代码添加一个新的类变量useRemoteWebdriver

private static final DriverType DEFAULT_DRIVER_TYPE = FIREFOX;
 private final String browser = System.getProperty("browser",
 DEFAULT_DRIVER_TYPE.name()).toUpperCase();
 private final String operatingSystem = 
 System.getProperty("os.name").toUpperCase();
 private final String systemArchitecture = 
 System.getProperty("os.arch");
 private final boolean useRemoteWebDriver = 
 Boolean.getBoolean("remoteDriver");

该变量将读取在POM中设置的系统属性,并决定是否要使用RemoteWebDriver实例。无论是否需要RemoteWebDriver实例,都要先使用以下代码来更新instantiateWebDriver方法,以便在需要时创建RemoteWebDriver实例。

private void instantiateWebDriver(DesiredCapabilities desiredCapabilities)
throws MalformedURLException {
     System.out.println(" ");
     System.out.println("Current Operating System: " +
     operatingSystem);
     System.out.println("Current Architecture: " +
     systemArchitecture);
     System.out.println("Current Browser Selection: " +
     selectedDriverType);
     System.out.println(" ");
     if (useRemoteWebDriver) {
         URL seleniumGridURL = new
         URL(System.getProperty("gridURL"));
         String desiredBrowserVersion =
         System.getProperty("desiredBrowserVersion");
         String desiredPlatform =
         System.getProperty("desiredPlatform");

         if (null != desiredPlatform && !desiredPlatform.isEmpty())
         {
             desiredCapabilities.setPlatform
              (Platform.valueOf(desiredPlatform.toUpperCase()));
         }

         if (null != desiredBrowserVersion && 
         !desiredBrowserVersion.isEmpty()) {
              desiredCapabilities.setVersion(desiredBrowserVersion);
         }

         webdriver = new RemoteWebDriver(seleniumGridURL, 
         desiredCapabilities);
     } else {
         webdriver = selectedDriverType.getWebDriverObject
          (desiredCapabilities);
     }
}

所有重要工作到此完成。我们使用useRemoteWebDriver变量来决定是要实例化普通的WebDriver对象还是RemoteWebDriver对象。如果要实例化一个RemoteWebDriver对象,则首先读取在POM中设置的系统属性。最重要的信息是seleniumGridURL,如果缺少它,则无法连接Grid。我们要读取系统属性并试图从中生成一个URL。如果URL无效,将抛出InvalidURLException异常。这并无不妥,毕竟此时无法连接到Grid,所以也可以在此结束测试运行,并抛出一个有用的异常。另外两个信息是可选的。如果提供了desiredPlatformdesiredBrowserVersion,则Selenium-Grid将使用满足这些条件的代理;如果不提供这些信息,Selenium-Grid将抓取任意免费的代理,并在其上运行测试。

当阅读这段代码时,难以立刻看出请求的是哪种浏览器,不过别担心,后面会讨论相关内容。每个DesiredCapabilities对象都会默认设置浏览器类型,因此如果创建的是DesiredCapabilities.firefox(),则将请求Selenium-Grid在Firefox浏览器上运行测试。这也是最初将getDesiredCapabilities()方法与instantiateWebDriver()方法分开的原因之一。

既然已经修改完代码,就需要测试看看它是否可以运行。最简单的方法是通过Selenium-Grid提供商(如SauceLabs)建立一个免费账户,然后用其运行测试。要运行测试,请在命令行中输入以下内容(显然,需要你提供自己的SauceLabs用户名和密码才能使代码正常运行)。

mvn clean install \
 -Dremote=true \
 -DseleniumGridURL=http://{username}:
 {accessKey}@ondemand.saucelabs.com:80/wd/hub \
 -Dplatform=win10 \
 -Dbrowser=firefox \
 -DbrowserVersion=55

不必进行各种复杂的设置,就能顺利连接到第三方Grid并见证各个测试的运行,这种感觉棒极了。通过这种方式,现在我们已经能在CI上远程运行测试了。

然而,这也给我们带来了一些全新的挑战。在远程运行测试时一旦出现问题,要弄清楚原因就会很棘手,尤其是在自己的计算机上代码貌似正常运行的时候。现在我们需要找到一种在远程运行测试时更容易诊断问题的方法。

即使你已经让测试完全可靠,失败也无可避免。这种情况下,通常很难只用文字就把问题描述清楚。如果其中一个测试失败了,并且能获得一张浏览器中发生错误时的截图,那么要解释出错的原因不就更加轻松了吗?当Selenium测试失败时,我首先想知道的就是失败时屏幕上显示的内容。如果能知晓失败时屏幕上的内容,就能够诊断绝大多数问题,而无须通过堆栈追踪来定位到代码行号,再查看相关代码,再分析哪里出的错。每次测试失败时都能获取屏幕所显示内容的截图,岂不是很好?我们以第1章中构建的项目为例,对其进行一些扩展,使其在每次测试失败时都进行截图。下面展示如何通过TestNG实现此功能。

(1)创建一个名为listeners的包(参见以下截图)。

(2)为TestNG实现一个自定义监听器,用于监听测试失败的情况,然后使用以下代码抓取屏幕截图。

package com.masteringselenium.listeners;

import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.Augmenter;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import static com.masteringselenium.DriverBase.getDriver;

public class ScreenshotListener extends TestListenerAdapter {

    private boolean createFile(File screenshot) {
        boolean fileCreated = false;

        if (screenshot.exists()) {
            fileCreated = true;
        } else {
            File parentDirectory = new
            File(screenshot.getParent());
            if (parentDirectory.exists() ||
            parentDirectory.mkdirs()) {
                try {
                    fileCreated = screenshot.createNewFile();
                } catch (IOException errorCreatingScreenshot) {
                    errorCreatingScreenshot.printStackTrace();
                }
            }
       }

       return fileCreated;
    }

    private void writeScreenshotToFile(WebDriver driver,
    File screenshot) {
       try {
           FileOutputStream screenshotStream = new
           FileOutputStream(screenshot);
           screenshotStream.write(((TakesScreenshot)
           driver).getScreenshotAs(OutputType.BYTES));
           screenshotStream.close();
       } catch (IOException unableToWriteScreenshot) {
           System.err.println("Unable to write " +
           screenshot.getAbsolutePath());
           unableToWriteScreenshot.printStackTrace();
       }
    }
    @Override
    public void onTestFailure(ITestResult failingTest) {
       try {
           WebDriver driver = getDriver();
           String screenshotDirectory =
           System.getProperty("screenshotDirectory",
           "target/screenshots");
           String screenshotAbsolutePath =
           screenshotDirectory +
           File.separator + System.currentTimeMillis() + "_" +
           failingTest.getName() + ".png";
           File screenshot = new File(screenshotAbsolutePath);
           if (createFile(screenshot)) {
               try {
                    writeScreenshotToFile(driver, screenshot);
               } catch (ClassCastException
                    weNeedToAugmentOurDriverObject) {
                    writeScreenshotToFile(new
                    Augmenter().augment(driver), screenshot);
               }
               System.out.println("Written screenshot to " +
               screenshotAbsolutePath);
           } else {
               System.err.println("Unable to create " +
               screenshotAbsolutePath);
           }
      } catch (Exception ex) {
          System.err.println("Unable to capture
          screenshot...");
          ex.printStackTrace();
      }
    }
}

首先,创建一个极具想象力的方法createFile,用于创建文件。然后,创建一个同样具有想象力的方法writeScreenShotToFile,用于将屏幕截图写入文件中。注意,在这些方法中没有捕捉任何测试异常,因为这是由监听器截获的。

 

如果在监听器中抛出测试异常,则TestNG可能会陷入困境。通常,TestNG会捕获这些异常,于是测试会继续运行,但这样做,测试就未必会失败。如果测试通过了,但是能获取失败和栈追踪信息,请检查监听器实现方式是否有误。

最后一段代码才是实际的监听器。你可能一眼就注意到,该方法的全部内容都放在了trycatch里,乍一看还以为写错了。虽然我们的确需要截图来展示出错的位置,但是如果由于某种原因无法成功截取或无法将截图写入磁盘中,我们可不希望测试终止运行。为了确保不中断测试的运行,我们会捕获因截图产生的错误,并将其记录到控制台以备后续参考,然后进行之前的工作。

并不是Selenium中的所有驱动实例都可以转换为TakesScreenshot对象。因此,对于那些不能转换成TakesScreenshot对象的驱动实例,我们捕获了它产生的ClassCastException异常,并增强它们。不是所有对象都能增强的,对于那些无须增强的驱动对象,如果尝试增强会抛出错误。通常需要增强的是RemoteWebDriver实例。除了在必要时增强驱动对象之外,该函数的主要作用是为截图生成文件名。我们希望确保文件名是唯一的,这样就不会意外覆盖其他截图。为此,使用的是当前时间戳和当前测试的名称。也可以使用随机生成的全局唯一标识符(Globally Unique Identifier,GUID)来命名,但是时间戳更易于跟踪在某时刻发生的事情。

最后,希望将截图的绝对路径记录到控制台中,这样便能更容易地查找已经创建的截图。

在之前的代码中,你可能会注意到,我们使用的是一个系统属性来获取保存截图的目录。你可以将出错时的截图保存到任何位置。这里已设置的默认位置为target/screenshots,如果要重写该值,则需要在POM中设置该系统属性。

为了做到这一点,需要修改maven-failsafe-plugin部分,通过以下代码新增一个额外属性。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
         <parallel>methods</parallel>
         <threadCount>${threads}</threadCount>
         <systemProperties>
              <browser>${browser}</browser>
              <screenshotDirectory>${screenshotDirectory}
              </screenshotDirectory>
              <remoteDriver>${remote}</remoteDriver>
              <gridURL>${seleniumGridURL}</gridURL>
              <desiredPlatform>${platform}</desiredPlatform>
              <desiredBrowserVersion>${browserVersion}
              </desiredBrowserVersion>
              <!--Set properties passed in by the driver binary
              downloader-->
              <phantomjs.binary.path>${phantomjs.binary.path}
              </phantomjs.binary.path>
              <webdriver.chrome.driver>${webdriver.chrome.driver}
              </webdriver.chrome.driver>
              <webdriver.ie.driver>${webdriver.ie.driver}
              </webdriver.ie.driver>
              <webdriver.opera.driver>${webdriver.opera.driver}
              </webdriver.opera.driver>
              <webdriver.gecko.driver>${webdriver.gecko.driver}
              </webdriver.gecko.driver>
              <webdriver.edge.driver>${webdriver.edge.driver}
              </webdriver.edge.driver>
         </systemProperties>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

由于我们通过Maven变量来使该属性变得可配置,因此还需要在POM的properties部分进行设置。

<properties>
     <project.build.sourceEncoding>UTF-
    8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
     <!-- Dependency versions -->
     <phantomjsdriver.version>1.4.3</phantomjsdriver.version>
     <selenium.version>3.5.3</selenium.version>
     <testng.version>6.11</testng.version>
     <!-- Plugin versions -->
     <driver-binary-downloader-maven-plugin.version>1.0.14</driver-
    binary-downloader-maven-plugin.version>
     <maven-failsafe-plugin.version>2.20</maven-failsafeplugin.version>
     <!-- Configurable variables -->
     <threads>1</threads>
     <browser>firefox</browser>
     <overwrite.binaries>false</overwrite.binaries>
     <remote>false</remote>
     <seleniumGridURL/>
     <platform/>
     <browserVersion/>
     <screenshotDirectory>${project.build.directory}
    /screenshots</screenshotDirectory>
</properties>

在Maven变量的定义中,你可以看到这里使用了之前没有定义过的一个Maven变量。Maven有一系列可以直接使用的预定义变量,如${project.build.directory},它用于提供目标目录的路径。每当Maven构建项目时,它都会将所有文件编译到一个名为target的临时目录中,然后,它运行所有测试,并将结果保存到该目录中。这个目录基本上是一个用于执行Maven任务的小沙箱。

当执行Maven构建时,使用clean命令通常是比较好的做法。

mvn clean verify

clean命令将删除目标目录,以确保在构建项目时,不受前一个构建中遗留内容的干扰,以避免可能会产生的问题。这意味着,在另一轮测试开始之前,如果你还没有将之前运行时的截图复制到其他地方,那么它们都将被删除。

一般来说,我们在运行测试时只会对当前测试的运行结果感兴趣(理应归档之前的结果,以备后续参考),所以将旧截图删除也并无不妥。为了保持规范和整洁,并使其便于查找,我们创建了一个截图子目录,用于把截图存储在此目录中。

既然截图监听器已准备就绪,就只需要通知测试去使用它。这非常简单,因为所有测试都继承自DriverBase,所以只需要使用如下代码,给DriverBase添加一个@listener注释。

import com.masteringselenium.listeners.ScreenshotListener;
import org.testng.annotations.Listeners;

@Listeners(ScreenshotListener.class)
public class DriverBase

从现在开始,一旦有测试失败,截图将会自动保存。

 

为何不试它一试?试着修改测试,有意使其失败,以生成截图。当测试运行时,试着在浏览器前面放一些Windows或OS对话框,再触发截图,看看这是否会对屏幕上可见的内容产生影响。

在诊断测试问题时,截图是强有力的助手,但有时会有这样一些错误,这类错误从页面上看起来完全正常。我们应如何诊断这类问题?

令人惊讶的是,很多人都对栈追踪信息心存畏惧。当栈追踪信息出现在屏幕上时,我常见到他们惊慌失措的样子。

“天哪!怎么又出问题了!又是几百行压根不认识的破代码,简直受不了了!到底要怎么办呀?”

首先请淡定。栈追踪固然包含很多信息,但实际上它们是非常友好且实用的内容。我们修改项目使其生成栈追踪信息并完成分析。对DriverFactory中的getDriver()方法进行一个小改动,通过以下代码迫使其一直返回null

public static WebDriver getDriver() {
    return null;
}

如此一来便永远无法返回驱动对象了,这将引发预期的错误。再次运行测试,但是要确保已带上-e开关,它可使Maven显示栈追踪信息。

 mvn clean verify -e

这次应该会看到一组栈追踪信息输出到终端,第一条信息如下图所示。

它还不算庞大,所以我们仔细瞧一瞧。第 1 行讲述了问题的根源:有一个NullPointerException异常。你以前可能见过这些内容。这里的代码就像在抱怨它期望在某个时刻获得某种对象,而我们没有给它。接下来的一连串文本行告诉我们问题发生在应用程序的哪个位置。

在这条栈追踪信息中,引用了相当多的代码行,它们中的绝大多数都是未知的,毕竟我们没有编写这些代码。从最底层开始,一条条往上看。首先为测试失败时正在运行的代码行,它位于Thread.java的第748行。该线程正在调用一个run方法(位于ThreadPoolExecutor.java的第624行),方法内部又调用了一个runWorker方法(位于ThreadPoolExecutor.java的第1149行)。持续向上分析栈追踪信息,会发现我们所看到的其实是一个代码层次结构,它包含所有调用的方法,并指出各方法中出现问题的那行代码。

我们特别关注的是我们所写代码的相关行,在本例中是栈追踪信息的第2行和第3行。可以看出,它提供了两个非常有用的信息,告诉我们代码在哪里出了问题。如果查看代码,就可以看到在发生错误时,它在试着做什么,这样便能尝试解决问题。从第2行开始分析。首先,它告诉我们哪个方法引起了问题。在本例中,它是com.masteringselenium. listeners.ScreenshotListener.onTestFailure。然后,它会告诉我们该方法中的哪一行引发了问题,在本例中是ScreenshotListener.java的第58行。在这里,onTestFailure()方法试图将一个WebDriver实例传递给writeScreenshotToFile()方法。如果查看栈追踪信息的上一行,将看到writeScreenshotToFile()在第41行出错,它试着用驱动实例给屏幕截图,却抛出了一个空指针错误。

也许现在你已经想起来了,我们修改过getDriver(),让它返回null值而不是返回有效的驱动对象。显然,我们不能对null值调用.getScreenshotAs(),否则会出现空指针错误。

那么为什么没有在WebDriverThread中出错呢?毕竟那里看上去才是问题的根源。其实传递null是一种合法行为,只有试图用null做一些事才会导致问题,这就是它在DriverBase的第34行也不会出错的原因。getDriver()方法只负责传递变量,并没有用它做任何事。在ScreenshotListener类的第41行,第一次尝试用null执行操作时,才会引发失败。

现在,如果你仔细观察,会注意到虽然得到了栈信息,它标识出了在ScreenshotListener类中出现的错误,但实际上并没有真正报错。这是因为截屏代码放在try...catch块中,它只显出栈追踪信息,以便你了解截屏失败的原因,但它实际上没有造成测试失败。实际产生报错的地方是在这堆信息中更靠后的位置。

如果再往下看一点,就会看到在BasicIT.java的第16行上触发了一个错误。这也是一个空指针错误,而这行代码才是第一次用驱动对象进行操作的地方。这样便能说得通了。

最后,我们还得到了另一个空指针错误。它位于clearCookies()方法上,但是由于没有行号,因此尚不清楚哪里出了问题。这是因为我们犯了一个错误:之前写过一些代码,用于在各个测试之间清除Cookie,以避免停止和重启浏览器,但没有考虑出错的可能性,例如,此时可能没有可用的驱动对象。该错误最终也引起了这样的结果,虽然并没有运行任何其他测试,但还是得到了一条奇怪的消息,指出运行了3个测试。

请使用以下代码来解决这个问题,以防止后续发生这种情况时又引起错误。

AfterMethod(alwaysRun = true)
public static void clearCookies() throws Exception {
    try {
        getDriver().manage().deleteAllCookies();
    } catch (Exception ex) {
        System.err.println("Unable to clear cookies: "
      + ex.getCause());
    }
}

现在修改了clearCookies()方法,将内容故在try...catch里。这意味着,它会捕获错误,但不再中断测试,剩余测试将继续执行。我们会将一些信息输出到控制台,以便解决可能发生的问题,但是这次信息已不再冗长。现在只输出原因,而不会输出整个栈追踪信息。通过这种方式仍然可以找出错误所在,但庞大的栈追踪信息不会使注意力分散到其他地方,而非实际导致错误的位置。 我们调整ScreenshotListener以便使栈追踪信息更简洁,代码如下。

@Override
public void onTestFailure(ITestResult failingTest) {
    try {
        WebDriver driver = getDriver();
        String screenshotDirectory =
        System.getProperty("screenshotDirectory",
        "target/screenshots");
        String screenshotAbsolutePath = screenshotDirectory +
        File.separator + System.currentTimeMillis() + "_" +
        failingTest.getName() + ".png";
        File screenshot = new File(screenshotAbsolutePath);
        if (createFile(screenshot)) {
             try {
                  writeScreenshotToFile(driver, screenshot);
             } catch (ClassCastException
                  weNeedToAugmentOurDriverObject) {
                  writeScreenshotToFile(new Augmenter()
                  .augment(driver), screenshot);
             }
             System.out.println("Written screenshot to " +
             screenshotAbsolutePath);
        } else {
             System.err.println("Unable to create " +
             screenshotAbsolutePath);
        }
      } catch (Exception ex) {
        System.err.println("Unable to capture screenshot: "
        + ex.getCause());
    }
}

和上一次相同,现在只记录原因。我们重新运行测试,看看这次输出的内容,参见如下截图。

这一次仍出现相同的错误,但是由于清理过代码,因此内容会更便于理解。这一次能明显看出问题在BasicIT.java的第16行。如果再查阅该行的实际代码,就会发现:通过getDriver()方法获取驱动对象后,这里首次对该对象进行了操作。抛出了NullPointerException异常,这非常合理,由于我们先前更改的代码导致了此异常。

栈追踪信息也许令人望而生畏,但一旦学会解读它们,就能将问题看得一清二楚。不过需要一些时间来习惯阅读栈追踪信息,并运用它所提供的信息来解决核心问题。使用栈追踪信息时,请记住一个重点,即完完整整地进行阅读。不要心存畏惧,或者在走马观花后便胡乱猜测问题所在。栈追踪信息提供了许多有用的信息来帮助你诊断问题,虽然它不会直接指出有问题的代码,但是它提供了一个很好的起点。

 

可以试着故意在代码中引入更多错误,然后再运行测试。看看是否可以通过阅读栈追踪信息来解决代码中存在的问题。

阅读本章后,希望你不再把自动化测试仅看作回归测试,相反,应该将它们看作一种实时文档,随着所验证代码的变化而不断发展壮大。当出现问题时,可以通过截屏来辅助诊断,同时能流利阅读栈追踪信息。你将了解如何把测试连接到Selenium-Grid以提供额外的灵活性。最后,你还将深入理解可靠性为何如此重要,以及如何为成功的持续集成、持续交付和持续部署提供支持。

下一章将研究Selenium所产生的异常,讨论如何解决可能遇到的各类异常,并探讨它们的意义。


相关图书

现代软件测试技术之美
现代软件测试技术之美
渗透测试技术
渗透测试技术
金融软件测试从入门到实践
金融软件测试从入门到实践
深入理解软件性能——一种动态视角
深入理解软件性能——一种动态视角
Android自动化测试实战:Python+Appium +unittest
Android自动化测试实战:Python+Appium +unittest
云原生测试实战
云原生测试实战

相关文章

相关课程