Web 自动化:一种基于 Page Object 的实现及常见异常处理

作者:mekhidu

团队:腾讯移动品质中心TMQ

前言

Selenium的使用本身十分容易,配置好环境后,即可选择自己熟悉的语言快速的编写脚本。花费力气引入设计模式增加首次构建难度,主要是为了以下两点:

1、减少维护成本。

Web产品往往界面变动频繁,如果每次更新后都需要花大量时间更新用例,自动化测试的收益大大降低。长期使用问题会越积越多,最后导致自动化方案破产。良好的设计模式能够减少重复代码,将元素操作与用例实现隔离开来,增加用例层的可读性,减少元素属性变化带来的测试用例重构工作,使得用例维护更加容易。

2、增加用例稳定性。

现在的Web前端存在大量AJAX与DOM元素操作,如果采用简单的面条代码编写用例,需要重复地去解决前端的AJAX等待和元素刷新等操作带来的StaleElementReference、NoSuchElement、NotClickable等异常。

一、Page Object设计模式简介

Page Object设计模式是Selenium官网推荐的一种自动化构建模式。PageObject设计模式对网页进行一个简单抽象,将每个页面设计成一个类,页面元素定位、元素操作、用户行为都被封装进对应的类。编写测试用例时不再直接操作页面元素,而是调用对应页面类的方法。使得测试人员在编写用例时能更多的关注业务逻辑,而不是页面结构与元素。

举个简单的例子,假设待测产品包含两个页面:登录页、个人中心。需要编写一个测试用例,实现用户登录并且点击签到按钮的过程。

用传统的线性方法实现时,用例逻辑、元素查找、元素locator等混在一起。如果是做冒烟测试或者简单页面的测试可以选择这种方式:

采用Page Object模式实现该用例的简要步骤如下:

第一步,为每个页面创建一个Class,页面Class内包含此页面元素的定位和行为:

第二步,调用创建的页面Class来编写用例:

虽然初始构建工作量更大,但是在产品变更频繁时,使用PageObject模式的优点明显:

1、测试用例的代码和页面元素操作的代码分离,用例可读性;

2、同一个元素的定位器不会出现在多个用例中,元素变更时只需要修改元素所在页面类;

3、登录的方法可以复用于多个用例中,如果产品登录流程发生变动,只需要修改登录页登录方法的实现。

为了在页面Class里更好的管理定位器和减少元素查找的代码量,Selenium提供了PageFactory类,使得我们在实现页面类时只需要用注解描述元素定位即可。调用元素的各个方法时,工厂类会自动根据定位器实时查找元素,在减少代码量的同时还能够帮忙避免DOM刷新带来的不稳定,详细使用见selenium官网。

二、Page Object基于控件的实现

既然PageObject设计模式这么棒,那为什么不直接拿来用呢?原因是在自动化工程的建设过程中,作者发现不同页面之间的DOM元素存在复用,页面并不是最小的UI单元,控件才是。这也是目前许多Web产品的特征,基于一套开源或者自研的前端控件库,页面结构由控件组成。同一控件的不同实例DOM结构类似,用户在页面上的操作可以看作是对各类控件动作的组合。

对这种类型的Web产品如果直接采用Page Object模式构建自动化测试,会导致在页面Class里重复对同一种控件的内部元素进行解析和操作,造成了大量重复代码并且加大了维护的难度。因此本次自动化测试工程的构建从控件出发,对每个控件的属性和方法进行封装,通过不同控件方法的组合来模拟用户操作,通过对用户操作的组合实现用例的自动化。如下图,自动化测试框架现在从下往上分为三层。

控件层,对前端所用控件在自动化工程中的抽象,继承自BaseConrol基类,基类中包含返回元素引用的方法getControl、查找子元素的方法getChildElement、等待元素加载的方法waitElementLoad等。派生的控件类包含各自特殊功能的实现,如小说目录包含翻页、按钮功能包含点击、导航栏包括后退等。

工具类层,包含两部分。

第一部分是一个控件查找类ControlFinder,可以根据XPath、CSS Selector等返回对应的控件。

第二部分是按照功能区划分的helper类,以本产品为例,按照找书、读书、个人中心等功能区创建了SearchHelper、ReadHelper、UserCenterHelper等工具类。这些类继承自BaseHelper,各个helper内通过对各种控件方法的组合对用户常见动作进行模拟。

用例层,继承自BaseStory,BaseStory内包含了用例执行前准备、用例结束后数据清理、失败截图、driver管理等功能。Story的划分按照产品用例类别进行划分,每个Story中包含多个用例,用例的编写依靠对工具层用户行为的组合。控件层和工具类层的实现在下一章结合具体问题给出。

基于这种模式,最后实现的用例如下:

三、提高测试稳定性

AJAX异步和DOM元素更新给Web自动化测试的稳定性带来了巨大的挑战,下面列举了常见的几个问题和它们在该模式下的解决办法。

1、查找元素时遇到NoSuchElementException

出现此问题一般是因为Selenium的查找操作在元素加载之前就已经结束。许多测试脚本在这里会选择用Thread.sleep()来等待元素加载,但是等待时间如果选大了浪费,选小了元素可能还没来的及加载。这种问题在此设计模式中可以用工具类ControlFinder集中解决,在根据定位器查找控件时,等待控件元素在页面出现,然后再返回对应的控件。

这里举的例子为每个控件都创建了一个查找方法。如果被测产品的控件提供返回控件名的方法,ControlFinder可以在查找到元素后,用javascript调用该方法返回控件名称,然后通过反射返回对应控件的实例。这样能实现一个查找方法查找多种控件。

2、StaleElementReference

这是Selenium测试脚本常见的异常,出现此问题的原因一般是所操作的DOM元素被刷新了。如下图的搜索页面的提示词,红框圈出来的提示词在页面中的层级一致,元素属性一致,但是从左图到右图,该提示词所在的DOM元素其实是被刷新过,两个看起来一样的元素在Selenium看来有着不一样的element id。就像两个人即使长得一样,但是身份证不一样,并不是同一个人。

现实现这样一个用例:

1)搜索“雪”,检查第一个提示词。

2)然后再输入“中”,检查第一个提示词。

问题写法如下:

上图代码在第二次调用hintWord.getText()时,DOM元素已经被刷新,hintWord所指向的DOM元素已经不是最初的那个元素了,因此会抛出异常。这类解决思路是,在输入“鹰”之后,重新查找一次该元素,于是有以下代码:

但是上面的代码运行过程中还是会有一定概率抛出

StaleElementReferenceException,抛出异常时的执行顺序如下图。脚本输入“中”之后,前端准备更新提示词,在更新完成之前第二次查找提示词元素的脚本已经执行了,这是hintWord还是指向ID=1的元素,然后前端完成DOM更新,测试脚本调用hintWord.getText()。因为此时ID=1的元素已经不在页面上了,所以程序出错。

下面看一下这个问题在该设计模式下的处理方法:

首先在控件基类中定义一个getControl()方法,此函数根据控件的定位器查找控件,并等待控件可见。

然后编写Label控件的类,Label控件包含一个检查文本的方法,该方法在默认等待时间内循环检查控件的文本。

1)如果元素查找和获取元素文本都发生在DOM刷新之前,那么获取的文本是刷新前文本,循环继续。

2)如果DOM刷新发生在元素查找和获取元素文本之间,则抛出异常。异常被处理,程序继续循环,下一次即可正常获取文本的值。

3)如果元素查找和获取元素文本都发生在DOM刷新之后,程序获取到最新值,检查通过。

4)如果DOM刷新超时,Assert不通过。

这样当DOM刷新后,测试程序马上获取到更新后的文本。如果超过规定的响应时间,也认为是待测产品异常,用例不通过。

下面是用自定义控件实现用例的代码,通过实时查找实现了控件变量的一次声明多处调用。

3、Element is not clickable

出现这个问题一般有三种原因:

1)该元素处于非点击状态

2)该元素被其它元素遮挡

3)该元素处于浏览器窗口外

为了规避第一种情况,在实现控件的点击方法时,应该先等待其变成可点击状态。

对于第二种情况,在浏览器上这种被遮挡的元素本来就不应该对其进行操作,应该尽量避免操作被遮盖的元素。

第三种情况是使用ChromeDriver时特有的异常,点击视野外的元素时有一定概率触发异常,官方傲娇的表示不修复此问题。为了提高这里的稳定性,在点击之前应该利用Selenium的Action移动到待点击元素。

综上,一个安全的点击事件应该长这样:

获取更多测试干货,请搜索微信公众号:腾讯移动品质中心TMQ!

版权所属,禁止转载!

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

1 条评论
登录 后参与评论

相关文章

来自专栏大前端开发

微信小程序从子页面退回父页面时的数据传递

我们知道,在微信小程序中,从一个页面转到另一个页面,一般情况下可以通过navigate或redirect时候的url来携带参数,然后在目标页面的onLoad函数...

481
来自专栏知道一点点

bootstrap快速入门笔记(三)响应式,行,列,偏移量,排序

702
来自专栏iKcamp

追溯 React Hot Loader 的实现

文:萝卜(沪江金融前端开发工程师) 本文原创,转载请注明作者及出处 如果你使用 React ,你可以在各个工程里面看到 Dan Abramov 的身影。他于...

42614
来自专栏ytkah

整理的dedecms标签大全,方便查找

  平时用dedecms开发经常会用到一些标签,特别是首页、栏目页、内容页,这些页面都会用到标签的调用,比如title、keywords、description...

2585
来自专栏Android知识点总结

4-SIII-Android数据固化之Xml的Dom解析和存储

482
来自专栏JetpropelledSnake

Vue学习笔记之Vue知识点补充

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 。你可以添加 lazy 修饰符,从而转变为使用 change 事件进行同...

892
来自专栏五毛程序员

五毛的cocos2d-x学习笔记06-处理用户交互

1122
来自专栏搞前端的李蚊子

关于echarts使用的常见问题总结

关于echarts使用的问题总结 1.legend图例不显示的问题: 在legend中的data为一个数组项,数组项通常为一个字符串,每一项需要对应一个系列...

3534
来自专栏技术墨客

React学习(3)——列表、键值与表单 原

    例子中使用map方法将每个元素的值*2,最后得到的数组为:[2, 4, 6, 8, 10]。在React中,处理组件数组的方式与之类似。

813
来自专栏谭伟华的专栏

Vue 组件开发实践之 scopedSlot 的传递

在使用Vue开发我们的vhtml-ui的组件库的过程中遇到了组件嵌套组件时需要传递scopedSlot的情况,官方的文档和教程目前还没有比较明确的指引,所以摸着...

2.5K1

扫码关注云+社区