首页 > 代码库 > Complete Page model UI automation framework
Complete Page model UI automation framework
前言
假设一个场景:在公司或部门里你们有多条业务线,如果有这么一个框架,当你告诉它你想在哪种浏览器上执行那条业务线的自动化用例后,你可以通过一个适配器类完成你想要的页面访问,而且只需要把你写在配置文件里面的页面跟元素identity告诉它,它就会给你返回你需要的element让你完成你本来需要手工操作的一些行为(这里说的identity不是dom里的元素id,是配置文件中自定义的一个类似数据库主键的东西)。这样是不是会很便捷? 你不需要再去调研用什么开源的框架,不需要再去写一些浏览器跟页面元素 属性,行为封装的类。
你要做的就有两件事:一是写配置:把你要操作的页面跟元素写到配置文件里,二是:翻译手工case,把你手工操作的过程翻译成自动化,其它任何事情都不需要做!!!这也是为什么标题里要加一个complete的原因
先来个直观的demo看下我说的这两步。
配置是这样的:
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
手工case翻译过来是这样的。
public class Demo { public static void main(String[] args){ PageAdapter.openBrowser("https://passport.xx.com/login"); WebElement userName = PageAdapter.getRuntimeElement("MainCLoginPage", "usernameTextBox"); userName.sendKeys("**"); WebElement passWord = PageAdapter.getRuntimeElement("MainCLoginPage", "passwordUserText") passWord.sendKeys("**"); WebElement signInBtn = PageAdapter.getRuntimeElement("MainCLoginPage", "loginBtn"); signInBtn.click(); } }
开始详细框架介绍之前,先大概谈谈自己对UI自动发展历史的理解,不感兴趣可直接跳过。
在移动互联网之前,大部分互联网产品都是CS或者BS架构的,当时的开发测试工作主要是接口自动化,工具开发与UI自动化,UI自动化有很多的开源框架,对于刚入行的同学来说,因UI自动化上手简单且执行可观性强,其吸引力可见一般。如果你也曾走过这条道,是否现在还清晰的记得当第一次看到自己写的程序可以打开一个浏览器在上面按你指定的方式点点点的时候的那份喜悦。本文因主要讲解的是自己写的一个UI自动化框架,在次不会对于接口自动化做过多的分析。但给看到此文的初学者一点建议,在入行之后,不要被UI自动化偏应用的假象所迷惑,一定要坚持内功修炼,多看一些开源框架的实现,结合自己对于设计模式的理解不断深入下去,不断培养自己的架构思维并在垂直的接口自动化及其它领域入行并深入进去。好了,不多说了,现在进入正文。
内容大纲:
1。设计思路
2。核心实现
3。使用方法及应用场景
设计思路:
目前主流的网站,从样式上大致可分为两类:
一种是页面深度比较浅且页面模块比较固定的业务类网站,此类网站大都只有首页,列表页,详情页这几大主要页面。
另一种网站是信息类网站,如论坛,百科这类的网站,此类网站最大的特点就是页面展示信息量庞大且模块不固定。当你大脑中有这两种网站的大概印象之后,我们可以简单的利用建模的思路去对他们进行抽象,第一种很简单,比较符合page model的应用场景,每个页面对象包含对应的元素域。第二种,对于这类网站,大多人很容易选择的一种方式是采用过程式的方法去根据业务场景实现case。这样的结果会是大量重复代码的堆积,case易读性及可维护性都很差。这种方式我也使用过,但后续这样写出来的case基本就是一次性的了,很难维护也很难在测试过程中应用起来。后来去超市买东西的时候,超市的货架给了我一个灵感,正常我们找东西,是先到对应的商品区,然后再到具体的货架找到想要的商品。这时候如果我们把影响我们的所有商品都清空,我们能看到的就是一个个的货架,到此,我们可以对应的抽象出一个模型,商品区对应具体的页面,货架对应具体的重复元素(a标签)的父元素,这时我们尝试着去用page model去做封装会发现,这样的封装会更简洁,因为我们只有页面,容器类元素及特定元素特征的封装。而不会像第一类网站对应的page model那么复杂,因为第一类网站我们会把不同页面的很多不同的元素都封装到页面中。
因为我目前接触的业务网站是第一种类型,所以我以此类网站的为例开始下文的具体说明。
Page Model 的封装如下:
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
我所在的部门,业务分为四块,因此会有不同业务线的不同page的封装,项目结构如下:
有了个业务线配置之后我们需要一个全局的配置(说明及截图如下),此配置决定了:
我要测试的业务线(决定加载哪个page model配置文件)
在哪个浏览器上跑
浏览器的一些基本设置
业务线的base url,此url与page model里面page的relative url 组成最终的页面地址,这主要是为了满足在不同环境执行case的需求。
<Configuration> <!--runOn 代表测试使用的浏览器目前支持firefox,IE 不要随便写,代码里面会用到这个值来做driver的初始化--> <runOn>firefox</runOn> <!--aut 代表被测业务线,b代表企业端,c代表求职者端 不要随便写,代码里面会用到这个值来读取元素配置文件--> <aut>b</aut> <!--baseURL 代表被测业务线的根路径,与配置文件中的relativeURL拼接形成完整的URL--> <!--<baseURL>http://www.chinahr.com</baseURL>--> <baseURL>http://qy.chinahr.com</baseURL> <driverGroup> <firefox isEnabled="false"> <poolMinIdle>0</poolMinIdle> <poolMaxActive>12</poolMaxActive> <pageLoadTimeout>600</pageLoadTimeout> <implicitWaitTimeout>50</implicitWaitTimeout> <scriptTimeout>50</scriptTimeout> <windowWidth>1024</windowWidth> <windowHeight>768</windowHeight> <userAgent>Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0</userAgent> <!--# userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:45.0) Gecko/20100101 Firefox/45.0--> </firefox> </driverGroup> </Configuration>
再回到page model对应的xml文件的结构
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
根节点是pages,实际用途就是对应反序列化后最外层的实体类
内层是pages,relative url是对应页面的相对地址, identity这个需要额外的说明一下,这个很重要,他对应的是最终要生成的page model的class name。
page下面是具体的元素,发现了么,也有个identity,它对应的是我们page class 内部的field name
元素的属性包括了常见的发现元素使用的属性:id,className,xPath,在代码里我使用了状态模式,封装的ElementFinder类可以根据这些配置的属性逐一去获取元素,一旦获取到就终止获取操作。element节点还有一个属性叫findType,正常我们是不配置它的值的,如果配置了ElementFinder就会根据此attribute的值去获取元素,而不会去使用状态模式逐一去获取元素。
selinum这些开源的框架,我们常用的获取元素的方法如下:
driver.findElements(by)
对于dirver的获取我们采用工厂模式,根据全局配置去实例化具体的driver并用thread local存储起来。在实例化driver之前我们会根据全局配置去反序列化对应的page model的配置文件。有了这个内存对象之后,我们可以把页面,元素以及元素,属性的对应关系生成出来,大家可以先简单的理解为一个复合map。这个map里面的key 就是我们配置文件里面配置的 page identity 跟 element identity。所以我们可以通过这两个identiy 拿到页面个元素的所有属性,然后使用状态模式及selenium 封装的具体方法去获取具体的元素。
我们的page model的类名,page model里元素对应field的name是与配置文件中page与element的identity的值完全一致的,获取元素我们封装成统一的方法,其接收两个参数page identity 跟 element identity,代码如下:
CHomePage WebPageBase { WebElement searchBoxCHomePage() { } WebElement getsearchBox() { String methodName = Thread.currentThread().getStackTrace()[].getMethodName()String elemetIdentity = CommonUtils.removeGetFromMethodName(methodName)ElementFactory.getElementByPageAndElementIdentity(.identityelemetIdentity)} WebElement getsearchBtn() { String methodName = Thread.currentThread().getStackTrace()[].getMethodName()String elemetIdentity = CommonUtils.removeGetFromMethodName(methodName)ElementFactory.getElementByPageAndElementIdentity(.identityelemetIdentity)} }
到此我们会发现,page model 这个类完全没有硬编码的部分,因此我们是可以在运行时通过解析配置文件把它动态的创建出来的,此处使用jdk javaassist的ClassPool,CtClass,CtField etc.
import javassist.*
到此我们需要考虑用户使用的问题了,现在我们有了:
根据配置创建出来的具体driver
根据配置动态创建出来的page model对象及包含他们的pages对象
除此之外,为了方便用户使用,我们把pages跟dirver(浏览器)的一些常用方法封装到一个PageAdapter类里面。并且暴漏底层对于dirver通用方法的封装类Dirvers给用户提供特方法的调用如:执行js操作。
最终在PageAdapter里面我们获取元素的方法形式如下:
方法接手的就配置文件中page跟elment的identity
public static WebElement getRuntimeElement(String pageIdentity, String elementIdentity) throws NotFoundException, IllegalAccessException, InstantiationException { try { try { ClassPool.getDefault().getCtClass("com.*.pages").toClass(); } catch (Exception ex) { } cls = Class.forName("com.*.pages").newInstance().getClass(); Method method = getMethodByName(pageIdentity); WebPageBase page = (WebPageBase) method.invoke(null, null); cls = Class.forName("com.*.models." + pageIdentity).newInstance().getClass(); Method elementGetMethod = getMethodByName("get" + elementIdentity); return (WebElement) elementGetMethod.invoke(page, null); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; }
除此框架还重写了junit的执行流程,支持自定义的日志收集及邮件发送报告(报告中可以包括执行失败的截图)的功能。
最后上个demo:
用户只需要按照手动case的方式去把它翻译成自动化的case。
public class Demo { public static void main(String[] args) throws IllegalAccessException, NotFoundException, InstantiationException { String originMobile = "159****3229"; PageAdapter.openBrowser("https://passport.xx.com/login?path=&PGTID=0d000000-0000-081a-3eb9-09f2e0f1996f&ClickID=1"); WebElement userName = PageAdapter.getRuntimeElement("MainCLoginPage", "usernameTextBox"); userName.sendKeys("**"); WebElement passWord = PageAdapter.getRuntimeElement("MainCLoginPage", "passwordUserText"); passWord.sendKeys("**"); WebElement signInBtn = PageAdapter.getRuntimeElement("MainCLoginPage", "loginBtn"); signInBtn.click(); }
}
使用说明:
使用此框架编写case,只需要三步:
1. 创建java或者maven工程,引用此框架jar包
2. 创建自己业务对应的page model配置文件
3. 翻译手动case
工程的结构如下,是不是你的工程一下变得so simple,so tidy
一点总结:
我们做任何东西的初衷肯定是让它能够最大限度的满足我们的需求,之后才是不断的优化,让它在易用性,通用性方面不断的提高。在框架设计的后期,我们会逐渐发现我们对于底层开源框架的封装及抽象已经逐渐让我们不用再去关心他们的具体实现了。大家也可以思考下,此框架是不是可以快速的扩展去支持app ui自动化?需要做哪些工作? 剧透一下,它的第一版是为了实现app ui自动化,我在老东家写的。后来有web ui自动化的需求,利用几天你的时间把它改早了下,产出了现在的这个框架。
最后希望大家看完之后,可以多给提一些宝贵的意见,一起通过技术的碰撞相互学习提高。
本文出自 “12422384” 博客,请务必保留此出处http://12432384.blog.51cto.com/12422384/1912423
Complete Page model UI automation framework