作者:吕泽廷
用上了奇技淫巧的这种字眼,本应该以一种狂拽酷炫吊炸天的开场才能名副其实,然而想到其实我们是一篇务实的技术总结性文章,那么我们就不再以一种博人眼球的开场来提升逼格了。本文主要通过总结多年使用WebDriver的经验和心得来跟大家一起交流学习。那么我们就不再闲扯题外话,言归正传,娓娓道来了。
在WebDriver时代,Wait不得不说是其精华功能的存在。在官方文档中我们可以看到等待分为显性等待(Explicit Waits)和隐性等待(Implicit Waits)。但是使用场景其实差异很大,正因为官方文档把这两个放在一起描述,导致我们大部分人对两者的差异感知不深,同时对Explicit Waits不太感冒,然而他却有四两拨千斤之巧劲。
既然把他们捧得如此之高了,那么我们来详细说一下这两等待的使用方法和场景。
这个隐式等待,之所以叫做隐式,是因为他在这里定义后,会影响后续所有的findElement()和findElements()。在后续进行findElement(By)和findElements(By)方法时,webdriver会每隔500ms根据元素定位器(By)进行查找元素,如果查到,则返回元素,如果没有查到,休息500ms再进行查找,如果超过10s,则报元素不存在。
那么我们可以明确知道,Implicit Waits的作用于是在查找元素。当然到了这里,我们大部分人可能主观的认为,ImplicitWait在手,世界我有,基本上不用考虑Explicit Waits了哈。
当然Selenium的设计人员肯定不会做无用功的,我们再来分析一下显式等待。从上面的写法我们可以看到他的作用是:检查定位器(By)的元素状态是可以被点击状态时,结束等待,否则每个500ms检查一次,当到达10s仍然处于不可点击状态,则抛出异常,结束后续操作。
这么解读,其实大家应该了解了,这个等待是为了等待页面到达某种条件,当条件满足后,再做后续的其他一系列事情。如上图代码中,当这个元素可以点击了,那么我们接下去对他进行点击操作。
那我们日常使用中,会不会出现这种情况多不多呢?我们先来说一个比较直接例子1:
比如我们在有些网站的注册上会看到,请求验证码在倒计时时,是不能点击验证码发送按钮的,所以这个等待也就是等这个元素能被点击的时候,我们再一次点击它。
我们在有些网站的注册上会看到如上图所示的登录注册,请求验证码在倒计数时,是不能点击验证码发送按钮的,所以这个等待也就是等这个元素能被点击的时候,我们再一次点击它,否则就会出现。
看到这里我们是不是让我们想到了一个词AJAX,在以前的静态网页时代,一个Implicit Waits我们就可以打遍天下无敌手,但是随着互联网技术的发展,用户体验的提升,AJAX技术随处可见,正因为这些,我们会看到页面越来越炫丽,同时交互流程也越来越严禁。这时候,我们不得不靠Explicit Wait来帮助我们解决AJAX动态页面上自动化操作。既然给予了如此厚望,Explicit Waits也负厚望,基本实现了绝大部分动态页面的响应监控。这里列举了一下ExpectedConditions的一些常用方法:
ExpectedConditions的方法一瞥 |
titleContains(String) |
等待页面title包含String |
urlMatches(String) |
等待页面url匹配正则string |
elementToBeClickable(WebElement) |
等待元素可以被点击 |
attributeContains(WebElement, String, String) |
等待元素包含某个属性 |
visibilityOf(WebElement) |
等待页面元素可见 |
textToBePresentInElement(WebElement, String) |
等待元素显示内容为String |
frameToBeAvailableAndSwitchToIt(WebElement) |
等待frame可以切换 |
numberOfElementsToBe(By, Integer) |
等待相同元素有指定个数 |
看到这里,其实我们应该了解显式和隐式等待作用其实是基本没有任何重合的,有的只是相互依赖关系,只有找到了这个元素(使用Implicit Waits),才能基于这个元素进行各种条件等待(Explicit Waits)。当然我们在实际使用过程中,也只有更好的使用两种等待,才能使我们的用例更加健壮稳定,同时提升我们的效率(PS:目前大部分应对加载慢的问题,都是强制增加等待时间,其实是不够优雅的)。
作为一名工程师,在使用工具的时候,应该尽量做到知其然知其所以然,这样我们才不会变为搬砖码农。那么如何才能久病成医(深入了解和使用WebDriver)?不得不说,学习WebDriver的各类Exception是一条捷径。因为看懂一些常用的Exception,有利于我们迅速定位排查问题,提升我们的调试和维护自动化的效率。这里我们就列举一些常见的WebDriver病灶(Exception)供大家诊断。
我们先来看一下查找元素的异常,直白的翻译这个异常,我们可以得到无效的选择器异常。那么我们什么地方会用到选择器呢?回忆一下,WebDriver提供的定位方式有id,name,className,css,tag,linkText,xpath等,其中只有css和xpath存在着语法(其他都是唯一的String)。何为无效呢?也就是我们拼成的css和xpath不符合css和xpath的语法,导致程序无法识别。当我们看到这个异常的时候,我们就可以瞬间反应过来,可能这个FindElement方法的定位器写的css和xpath语法错误了。
在写对了元素定位器,同样会出现异常,如同这个异常一样,翻一下:没有这样的元素异常。也就是说根据我们所写的合法元素定位器,无法找到符合要求的元素。试想一下,在哪些情况下会出现这样的情况呢?因为我们的选择器写的很正确不是吗?如果在id,name,className,css,tag,linkText中出现该错误,十之八九是因为自己手打String的时候,出现了错误。当然遇到xpath出现的话,也许会相对复杂一些,相对路径写错了或是元素属性值打错了等。
这个看着就更浅显易懂了,没有这样的Frame,出现这个异常无外乎两种情况,第一种,该Frame真的不存在当前的
DOM
结果中;第二种,我们写错了元素定位,导致根据该Frame定位无法在当前的
DOM
中找不到这个元素。
这个是因为我们将页面切换到了不存在的窗口导致出错。只需要填写正确的页面句柄或者title即可。
- StaleElementReferenceException
这个异常不好理解,我们先翻译一些这个异常:陈旧的元素引用异常。那什么样的元素会变成陈旧的呢?我们这里假设一种常见:我们在页面上找到了一个元素并把这个元素赋值给myElement,即WebElement myElement = driver.findByElement(by);在接下去的操作中,myElement这个页面元素在一系列操作后,彻底的从页面上删除了,那么接下去我们执行myElement.click(),此时页面已经没有这个元素了,那么他的引用关系是不是很陈旧了?
上面讲了一些我们在编写用例的时候会出现的“硬”异常,为什么我叫他做“硬”异常呢?因为他们是在我们用例写成后,第一次执行报异常后,如果不进行任何修改,每次都会出现该异常。是一盘不回锅就永远嚼不下去的硬菜。当然有硬菜就会有软菜,也就是一些非必现的偶发性异常。
这个异常是WebDriver的总异常类,大部分没有独立异常catch的类,都会通过这个Exception抛出。对于这个类,可能我们就不是单纯的看异常名字了,需要详细分析的Exception Message了。当然我们也不一一介绍,只举例我们的“软”异常。
这条Message,应该是我们在写自动化点击操作中遇见较为频繁的一条。我们翻译一下,元素不能在坐标(669,358)上进行点击,原因是其他元素接收到了这个点击事件。那为什么会出现这种情况呢?首先我们来说一下WebDriver的实现机制:它是通过调用浏览器接口,实际进行浏览器操作的。比如这里的点击,其实是我们找到这个元素,计算这个元素的中心点在浏览器中的位置,然后对浏览器的这个位置进行模拟点击,所以我们看到会有一个明确的坐标点。那为啥会点击的时候没有点到,同时被其他元素接收了呢?这里我们就又需要重申一次AJAX了。我们举个详细的例子:
- InvalidElementStateException
这个异常在我们日常AJAX页面上也是较为常见的一种。我们来翻译一下,无效的元素状态异常。那什么情况下会出现无效的元素状态。我们再看一个例子:
如图,在大部分网站的注册功能上,会在输入正确手机号后,获取验证码按钮变成可以点击,当然这个操作是JS来控制,但是有可能出现我如图的情况,我已经输入了手机号,但是由于JS慢了半拍,导致获取验证码处于不能点击的状态,这时点击获取验证码即会出现我们说的异常了。
通过了解Exception后,我们可以很快的定位出程序存在的问题,从而实现效率的提升。在上面举例的这些异常的例子,大多数是这个异常出现的一个场景之一,但是通过我们深入分析原因,很容易将我们遇到的问题基本可以归类到这几种异常。所以大家后续编写用例过程中,如果遇到如上这些异常,可以适当的分析和定位原因。当然我们可以看出来上面的“硬”异常,其实是可以通过我们的细心可以慢慢规避,当我们遇到“软”异常的时候,我们可能需要分析具体问题了。然而,我们业内会有很多框架二次封装,有部分就是在解决那些“软”异常,来提升测试的稳定性。
在Wait和Exception中,我们从定位问题做到了一定程度的效率提升,不过WebDriver提供给我们的秘技远不止这些。这里我们要讲另一个大杀器---Annotation。Annotation(注解)可能我们学习SSH和MVC的时候可能遇到比较多,用处我们就不用赘述了,最基本可以减少我们自己初始化对象的工作,同样WebDriver为了提供了这一便利,可以将我们找元素操作托管出去。接下去,我们来看下如何通过注解来提升自动化效率的。
我们先从FindBy注解开始入门。通过这张图我们可以轻松的知道,查找className中有cover的元素就是我们的WebElement cover。这样的查找元素,是不是看起来比之前的方式简洁不少。当然如果匹配到元素时多个的时候,我们只需要把WebElement替换成List<WebElement>,其中到底是一个还是一个list,这种粗俗简单的事情交由WebDriver自己帮我们实现即可,我们只需要申明className="cover"的元素就是我们需要找的cover元素。
- FindBys({FindBy(id = "foo"), FindBy(className = "bar")})
有时候,我们在定位元素的时候,发现需要上下文中多个定位器帮助我们定位(多个并且[&&]查询条件),当然WebDriver也为我们提供了这样的遍历,通过FindBys,会帮助我们查找className=brand的元素及子元素中含有className=info的元素。是不是有种不再需要复杂而又冗长的Xpath也能清晰的查找不易定位的元素了?不过由于使用了多个定位器,查询的性能上比起相对路径的Xpath还是有一定的差距的,所以在日常使用中,不要太多的组合使用,避免影响效率。
- FindAll({FindBy(id = "foo"), FindBy(className = "bar")})
从FindBys到FindAll,我们不难猜出FindAll是一个或者[||]的查询条件了,所有包含id="foo"和className="bar"的元素都是我们查询结果,当然大部分条件下会返回给我们一个list。不过目前,用这种方式获取元素的场景还是较少的,可能在获取点赞或者收藏这两中元素选中和未选中的集合时会用到。
随着互联网和移动互联网的不断发展,其实我们绝大多数的互联网产品,都会有Web端,Wap端,Android端,iOS端,面对这么多端,除却本身操作系统的差异,产品从设计开始,都是希望各端页面和交互尽量保持一致,来降低用户的使用成本和开发成本。既然各端相似度极高,那么对于测试人员,自然而然会冒出一个Idea:单一一个功能既然在各个端的逻辑和交互基本一致,那么在精益化的测试过程中,是否可以将这些功能交由1个人来完成?这里我们现在的客户端产品就是这么来分配我们的测试工作的,经实践分析,将iOS和Android合并测试后,可以做到效率的1.5-1.8倍提升。不得不说是一个喜大普奔的好结果。既然功能测试可以这么做,我们的自动化测试为啥不能也效仿这种做法呢?我们能想到的,WebDriver的大神们当然也不会落后,为了支持多端共用元素,也提供了高效可用的攻略:
从这里,我们可以看到同样一个元素SELECT_TOPIC元素,我们在Android,Selendroid和iOS中有着不一样的元素定位方式,当时这个完全不打紧,因为让用户感知到的就是这个元素,在自动化过程中,当我们启用不同的driver时,就会根据driver的device来确定是用@AndroidFindBy,@SelendroidFindBy或是@iOSFindBy了,是不是“居家旅行必备良药”?
这里我们介绍了一系列查找元素的注解方式,从表象上,我们就能发现,通过一个简单的注解,就可以将driver.findElement(By)操作变得简洁明了,当然我们可以进一步联想到,其实就你可以用一个单独的文件来维护所有的元素,我们是否还记得Selenium 1.0时代比较流行的UIMAP设计模式?隐隐感觉在这里做到了又一次的重生,具体我们会在下一模块讲到。
有了注解方式,确实大大提升了我们的编写效率,同时降低了维护成本。不过WebDriver向来是个送佛送到西的活雷锋,有了注解,还为我们提供了一套与Page Object设计模式相结合的使用方式,让我们的代码编写更加清晰,同时具有更低的维护成本。对于什么是Page Object设计模式我们就不在费唇舌了,网上解说太多了,那么就来说说这两者怎么结合吧。
总体结构上如上图所示,将整个Case划分成页面元素对象,页面对象,及用例对象。接下来我们至底而上来层层分析。
主要对页面上的元素进行声明和定位,方便后续在页面上使用。在这里我们就可以把注解用得如鱼得水了。具体示例如下:
我们为一个页面,建立一个页面元素的对象,在整个页面元素对象中,我们对这个页面的所有元素用注解进行声明。再使用注解的同时,我们可以发现整个对象基本上实现了UIMAP的作用,即将一个页面的元素维护在一个统一的文件了,方便后续因为重构和功能迭代进行调整元素定位。
Page对象主要是对该页面上元素的操作的封装,将特定的逻辑操作封装在一起,以组成特定的业务单元。如下图所示:
在Page对象中,基本上都是和业务相关的基本操作,比如做一个点击,做一次输入,进行一次检查等(如图中checkCover,checkBrandLogo,checkBrandName等),同时也会有对整个业务的封装,比如对这个页面静态数据的检查(如图中apiCheck)。在这里,我们发现在页面中操作页面元素是相当简单的,直接取对象的属性即可。
看到这里我们可以知道结构相对清晰,那么我们如何在PageObject和Page之间建立关系呢?WebDriver同样为我们提供了便利。如下图,我们可以通过PageFactory类轻松的在Page类中实例化PageObject中的所有元素。
在Page对象的构造函数中,通过PageFactory对PageObject进行初始化。这样看起来是否就很简洁清晰了呢?完成初始化后,就直接调用对象属性的方式来使用所有的页面元素。
通过使用注解,将原先的页面对象拆分成为PageObject对象和Page对象,不仅简化了元素定位操作,同时做到元素定位和页面业务的完全分离,结构清晰,维护简单。
在讲解Wait和Exception中,隐隐体现出来使用WebDriver还是蕴含一些小窍门和注意事项,既然知道了这些,我们自然而然地想是不是应该将这些窍门和技巧运用到我们日常的编写用例上,这时候顺水推舟的会出现一个词“封装”。当然,“封装”也是我们程序员最喜欢干的事情。当然开发WebDriver的程序员们在设计了这么多秘技的同时,也为我们提供了一些封装思想,方便我们在日常编写用例过程中更加轻松,高效,易用,可维护和可拓展。
这里我们就引入“Bot Style Test”,它的核心思想:他是对Selenium API进行行为导向的抽象操作封装,以便提供一种命令行式的用例编写方法,同时便于后期解决一些Selenium存在的问题及提供一些Selenium不支持功能。
从我们前面讲Wait和Exception的时候,已经指出,在没有适当使用Wait和Exception时,可能我们一个简单的点击或者输入操作,都有可能出现一定概率的失败情况。这里我们可以粗浅的任务,WebDriver基础的点击和输入操作是存在缺陷的,那么为了解决这些存在的问题,我们可以同Bot Style Test的思想进行封装,让我们在使用的时候更加简单方便。
接下来我们以点击和输入操作作为一个例子分析一下:
前面我们分析过,如果一个元素是在不能点击的状态下进行点击操作,会报InvalidElementStateException异常,那么我们是不是可以在点击操作执行之前,进行条件为检查元素是否可以被点击的Wait操作。等元素可以被点击的时候,再进行操作。这样我们是否就可以避免因为在不可点击的状态下进行元素点击而产生用例不必要的执行失败了?
我们前面,介绍过了,如果被点击元素被其他元素遮挡的情况下,我们会发生WebDriverException,原因可能是JAVA执行速度远快于JAVAScript,那么我们为何不在这里增加重试来保证点击成功呢?当然重试还是会失败,我们需要捕捉处理。如上图,我们通过捕捉WebDriverException,当其中的message中包含"Other element would reccive the click"来判定是否被其他元素遮挡。如果遮挡,则进行重试,重试次数到达后,如果还是失败,则我们主动抛出异常。
以上2个点击例子只是冰山一角的缩影,实际过程中可能为了提高我们的点击操作的稳定性,易用性等会有其他一系列符合“Bot Style Test”思想等处理。在保证操作是简单的单行命令的同时,高度集成各种异常捕获,同时只能处理,以便达到用例操作本身的稳定性,可以让我们把精力核心放在业务逻辑的思考,及BUG的跟踪定位,而不是在排查WebDriver不当使用而导致出现各类偶发问题。
本文来自网易实践者社区,经作者吕泽廷授权发布