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 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网杂技

前端面试题整理

交互设计前端开发 前言: 现在前端面试主要考察以下几个方面: 初级的:html、css、js,jquery,开发工具git的使用,对其他框架稍微了解; 中级的:...

4049
来自专栏更流畅、简洁的软件开发方式

【自然框架】——页面基类与设计模式(二) 模板模式

前篇:【自然框架】——页面基类与设计模式(一)桥接模式 桥接模式的补充:(下面的一段是桥接模式里后补充的一段,桥接模式的一个小结) 什么是交接模式?引用《大话设...

23610
来自专栏菩提树下的杨过

解决JQuery中的ready函数冲突

jQuery确实是一个提高前端开发效率的好框架(虽然很多大牛们都说它效率不咋地),但是用好它有时候并不容易,也许你也遇到过以下情况: 一个aspx页面通常可以包...

1788
来自专栏技术分享

.NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)

阅读目录: 4.ModelMetadata(ModelMetadata元数据如何支撑Model与View之间的组合关系) 4.1.ModelMetadata元...

2485
来自专栏更流畅、简洁的软件开发方式

可以通过基类实现的几种功能。vs2008 .net 2.0

    我很懒,很不喜欢写重复的代码,一行重复的都不想写,所以当同一段代码要写第二遍的时候,我就会去想避免的方法。OO的特性之一 —— 继承 —— 可以帮很大...

2059
来自专栏difcareer的技术笔记

so加固-加密特定section中的内容

本文参考自:Android逆向之旅—基于对so中的section加密技术实现so加固,增加了自己的实践过程,以及一些额外的验证和解释。

1264
来自专栏.NET后端开发

如何通过经纬度获取地址信息?

摘要 Google Maps API Web Services,是一个为您的地图应用程序提供地理数据的 Google 服务的 HTTP 接口集合。具体包括:Go...

37911
来自专栏智能大石头

手工调试自定义控件各主要方法执行顺序(分运行时和设计时)

继承TextBox,override各个方法,分别下断点调试。 ctor为构造函数 在构造函数中,通过代码: this.Text = (new Random(D...

18310
来自专栏吴伟祥

关于前端使用SiteMesh的一些介绍 转

在网站开发的过程中,通常一个网站会有一个整体的风格,页面都有很多共同的菜单,横栏的底部信息。以前我们会采用include标签在每个jsp页面中来不断的包含各种h...

551
来自专栏JetpropelledSnake

Vue学习笔记之Vue的使用

我们能发现,引入vue.js文件之后,Vue被注册为一个全局的变量,它是一个构造函数。

773

扫码关注云+社区