又是漫漫长夜,无心睡眠……这次文章是技术总结,不是随想,所以省去开头和暖场,直接切入正题。本文档对于如何使用RFT+XPATH提高自动化测试的执行效率提供了一种新的思路,就是缓存对象路径,(PS:没用过RFT做自动化测试的同学可以忽略了 ,用watir的同学也可以忽略了。)
UI层的自动化测试,提高脚本的执行效率是永远的主题,我们必须把一轮脚本的执行时间控制在可接受的范围内,当脚本的总体执行时间很长时,我们就需要对其进行优化,优化的方法无非是三种,1、优化用例,减少无效用例的个数;2、提高脚本识别UI对象的效率;3、分布式执行。本文介绍的方法属于第二种,提高脚本识别UI对象的效率
RFT是IBM的一款自动化测试工具,它的作用和QTP类似,使用JAVA作为脚本语言,ECLIPSE作为IDE,优雅并且强大。RFT识别对象的核心方法是RootTestObject的find方法。我们做自动化测试的时候,通过对FIND方法的封装实现了自己的对象识别框架。但是find方法的执行效率问题一直是我的心病。之前的情况是查找对象时,执行一次find方法至少要4-6秒的时间,几百个用例执行下来执行时间就让人无法忍受了,十几个小时……只能晚上跑脚本,早上来看结果,真赶到上午发版的时候,根本指望不上自动化测试能帮上什么忙,所以必须要想办法缩短脚本执行的总体时间,提高对象的识别效率。
XPATH是一种XML的查询语言。简洁,高效。在IBM的开发者社区论坛里,有专门的一篇文章讲解如何结合RFT+XPATH来实现动态识别对象,很赞。这里给出文章的链接:《使用 XPath 在 Rational Functional Tester 中动态识别对象》,作者刘哲,段雪飞,邢静。在此表示感谢并崇拜一下。另外多说一下,IBM的开发者社区真的很有料,他们的文档库里关于自动化测试的文章建议大家通读,保证你获益匪浅。
XPATH识别对象的效率明显优于find方法,为什么?原因在于XPATH查找对象是基于某个对象的具体路径,而find方法是动态查找,也就是全局查找,这样遍历的对象多,必然就慢。举一个xpath的例子,来源于刚才的那篇文章,对于对象,使用XPATH来查找时路径可能是:"org.eclipse.swt.widgets.Shell[@captionText=
'Hello']/org.eclipse.swt.widgets.Button[@text='OK']"),而使用rft的find方法如果想实现同样的效果,就只能用atChild来拼了。写过RFT脚本的同学应该都有体会,事实上,如果在xpath中不使用路径而直接传入/::desander[@captionText='Hello'],那么他的执行效率和find的全局查找是类似的。
但是XPATH虽然快,可是仍然有缺点,那就是路径的维护问题。在一个自动化测试工程中,我们可能会使用到近百个测试对象,我们当然可以把每个对象的XPATH路径存到文本文件里,但问题是,如果程序的页面结构发生变化了,怎么办,这么多的缓存路径改起来不是闹着玩的,而且排查错误也不容易。所以我一直在寻找一种方法,能结合RFT动态识别和XPATH效率快两个优点,并且维护简单,后来就有了这个想法:把一个测试对象的父对象的class属性依次读取出来,拼成该对象的一个路径并写入到缓存文件中,当脚本在识别对象时,首先从缓存文件中把路径读取出来,并传递给XPATH去识别,如果对象能识别到,则执行操作;如果识别不到,则重新执行find方法进行识别,并把识别到的对象的路径重新更新到缓存路径中,如果find方法也找不到对象,就返回null,此时就需要人工介入排查识别的原因了。这种做法就是我标题中提到缓存对象路径的作法。
关于缓存文件的存储格式,我采用的策略是为每一个UI层的测试对象分配唯一个的objectID作为标识。规则是脚本名称 + object_id + objecg_value。其中object_id和object_value是你在识别测试对象时使用的属性。写过RFT脚本的同学应该都懂,不懂的就给我发私信吧,因为也不是一句两句能解释清楚的,毕竟这不是一篇教程,大家见谅;
缓存对象路径的做法,其实核心就是两点:
1、在识别测试对象时,永远先从缓存文件中读取对象路径并由XPATH识别;
2、当路径失效或者不存在时,由自动化测试代码自动更新路径,不然想自己手写XPATH路径的同学您最好先考虑清楚。
以下是几个核心代码的实现,供大家参考。(PS:想要直接拷代码的同学可以忽略下面的内容了,因为你没有这些代码底层的支持代码,拷过去也没用的)
通过递归读取对象路径的方法:
private void addPropertyToCatch(TestObject to, StringBuffer str) { Hashtable<Object, Object> p = to.getProperties(); String _class = p.get(".class").toString(); str.append("child::").append(_class); if (_class.contains("Html.TR")) { int rowIndex = this.getHtmlTRIndex(to); str.append("[").append(rowIndex + 1).append("]_@/@"); } else if (_class.contains("Html.TD")) { // 在IE的document模型中,所有的索引都是从1开始的 int index = this.getHtmlTDIndex(to); str.append("[").append(index + 1).append("]_@/@"); } else { int classIndex = this.getHtmlTOIndex(to); if (classIndex == -1) { if (_class.contains("Html.HtmlDocument") && p.containsKey(".title")) { String title = p.get(".title").toString(); str.append("[contains(@_").append(".title,'").append(title) .append("')]"); } str.append("_@/@"); } else { str.append("[").append(classIndex + 1).append("]").append( "_@/@"); } } }
有了这个方法,我们就可以传入一个TestObject对象来获取到它的xpath。需要注意的是,通过脚本读取出来的TD、TR其classIndex是从0开始的,但是在IE和XPATH中所有元素的位置是从1开始的,这点切记
public String getTestObjectTreeStr(TestObject to, String id, String value) { if(to == null){ return "null"; } StringBuffer xpath = new StringBuffer(); addPropertyToCatch(to, id, value, xpath); this.getTestObjectProperty(to, xpath); String path = xpath.toString(); String rs = parseXpath(path); return rs += "[1]"; }
获取到对象的XPATH路径,我们就可以将对象路径字符串写入缓存文件,示意代码如下:
public void writeXpathToFile1(String ObjectID, String xpath, String filename) { File file = new File(filename); try { if (this.isObjectXpathExists(ObjectID, file)) { this.updataXpathbyObjectID(ObjectID, xpath, filename); } else { BufferedWriter writer = new BufferedWriter(new FileWriter(file, true)); writer.write(ObjectID + "=" + xpath); writer.write("\r\n"); writer.flush(); writer.close(); } } catch (IOException e) { e.printStackTrace(); } String rootStr = this.getFirstRootStr(xpath); String rootStr_ID = ObjectID + "_head"; try { if (this.isObjectXpathExists(rootStr_ID, file)) { return; } else { BufferedWriter writer = new BufferedWriter(new FileWriter(file, true)); writer.write(rootStr_ID + "=" + rootStr); writer.write("\r\n"); writer.flush(); writer.close(); } } catch (IOException e) { e.printStackTrace(); } file = null; }
我们还需要考虑页面结构或者对象属性发生变化的情况,所以我们需要写一个方法实现自动更新缓存文件中的xpath路径。其实就是把更新后的XPATH路径读出去来,然后覆盖原来的路径
public void updataXpathbyObjectID(String objectID,String xpath,String filename){ ArrayList<String> list = new ArrayList<String>(); StringBuffer sb = new StringBuffer(); File file = new File(filename); BufferedReader in = null; BufferedWriter ut = null; try { in = new BufferedReader(new FileReader(file)); String temp = ""; while((temp = in.readLine()) != null){ list.add(temp); } //查找并删除 for(int i = 0; i < list.size(); i++){ if(list.get(i).contains(objectID + "=")){ sb.append(objectID + "=").append(xpath).append("\r\n"); }else{ sb.append(list.get(i)).append("\r\n"); } } out = new BufferedWriter(new FileWriter(file)); out.write(sb.toString()); out.flush(); out.close(); out = null; in.close(); in = null; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
最后是使用缓存查找对象,这就 很简单了。不管你是用什么方法获得对象的,在调用你的代码之前,首先调用缓存对象的方法,实例代码如下:
String xpath = this.getObjectCachePath(ObjectID, filename); to = GuiTestObjectFactory.getObjectByXPathAndWaitTime(rootGuiTestObject, xpath, 5); if(to == null){//如果根据缓存文件中的路径不能识别到对象,则转用find方法来执行以保证脚本的健壮性,这一步骤至关重要 RootTestObject root = this.getRootTestObject(); TestObject[] to = root.find(atDescant(".text","useName)); return to[0]; } |
缓存对象路径的方法现在由我和我的同事在各自负责的自动化测试工程中分别使用,效果还算可以,自动化测试执行效率提高显著,以我的脚本为例,原来全部的脚本执行下来需要14个小时,使用这个方法后,执行时间缩短到了8个小时,只是执行的成功率有所下降,由原来的92%一度下降到75%……猜猜原因是什么?是因为脚本执行的太快了!原先find方法执行一次至少要几秒钟的时间,但是现在识别一个对象最快只要几百毫秒,所以很多健壮性的问题就暴露出来。现在我已经花了一个星期的时间不断的去完善,才有了现在87%的通过率(当然,还有部分是程序缺陷),所以如果您想开始用这个方法来完善自己的脚本,那么先做好心理准备吧,当然也要先和上级沟通好,不然一两个星期的人时砸进去但是他却看不到工作产出的,那可就……后面您懂的。
到这里,我的方法基本就介绍完了,本文所说的方法只是一个优化方法之一,提高脚本的识别效率其实有很多方法可以采用,比如直接注入IE的进程获取到其document,然后用JS操作IE的元素,不骗您,这个方法真的可行,但是难度也确实有。有兴趣的同学可以试一下。本文中所列出的代码您拷过去是没法用的,因为有很多底层的代码没法贴,大家就参考代码了解思路吧,思路永远比代码重要。下一步我回把这部分代码写成一个JAR包发布出来,到时大家用起来就方便了。
最后PS一下,千万别问我为什么不用FIREBUG插件直接获取XPATH路径……因为程序不支持FIREFOX,回答完毕。