自动遍历工具Java版 (开源)

本帖已被设为精华帖!,

为什么重新写一个

刚才iOS版的也测试稳定了,顺便分享艰辛的喜悦。
两周前写过一篇文章关于深度优先的探索性遍历工具的设计和实现,写明了原因。前期调研,这方面的资料不多,感谢testerhome的创始人seveniruby,已经用scala开发完成品,还有两篇相关的文章,给我提供了很好的借鉴,没有他前面走过的路,我必定要走很多弯路,也不可能这么快就能做出来。所以,很荣幸能进入testerhome这个社区,开拓了的视野。在做的过程中踩了不少坑,以每周少睡20多个小时的代价寻求解决方案,今天主要是说一下设计思路和技术细节。

关于开源

我相信有很多代码能力在我之上的,开源的目的是为了提供参考方案和解决思路,目前只是基础版,还请提出宝贵建议,源码传送门

关于Appium参数

方案一、把支持的参数都封装成bean读取,后来想了想觉得不够灵活,如果以后有弃用或增加的参数还要改代码;
方案二、以键值对的方式存入字典对象,遍历读取,这样就不必担心扩展了。

关于窗口唯一性

方案一、用md5,容易受到各种干扰,特别是窗口位置的细微变化。
方案二、用activity,普通窗口没问题,带TAB页的窗口就无法区分,切换TAB页内容不同就不要区分。
方案三、用xpath表达式寻求能鉴别窗口唯一性的特征元素,例如:Title。有的app对于控件的命名很不规范,导致很难发现共性。
方案四、也就是现在的方案,从窗口头部开始往下取几个节点(可配置),去掉坐标干扰后再生成md5,这个方案对于窗口鉴别还算稳定。

<!--窗口鉴定策略,默认取前8个节点生成md5-->
<identify-default>8</identify-default>
<!--Tab窗口用selected区别,可能要多选几个节点到达-->
<identify-special>
<define>专题,直播,要闻,选股>>24</define>
<define>简单理财,投资,保险,贷款,信用卡>>30</define>
</identify-special>

关于引导流

有两种场景需要用到:
1.欢迎界面划屏至登录成功
2.app可能由多个项目组完成,每个项目组负责一个模块,需要引导到指定模块
考虑再三,用简单的关键字驱动方式实现引导

<iosGuideFlow>
<!--滑动类型设置-->
<step>slide>>2</step>
<!--点击类型设置-->
<step>click>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAButton[1]</step>
<step>click>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAButton[3]</step>
<!--输入类型设置-->
<step>input>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIATextField[1]|13012345678</step>
<step>click>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAButton[1]</step>
<step>input>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIASecureTextField[1]|123456</step>
<step>click>>xpath:://UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAButton[1]</step>
<step>input>>xpath:://UIAApplication[1]/UIAWindow[3]/UIATextField[1]|8888</step>
<step>click>>xpath:://UIAApplication[1]/UIAWindow[3]/UIAButton[2]</step>
<!--手势密码设置-->
<step>gesture>>xpath:://UIAButton[@name='blue circle']</step>
</iosGuideFlow>

关于提升遍历效率

这个踩的坑最多,也耗费了我大部分时间。
1.获取可操作元素
a)预加载:获取窗口后,用类似于这种方法取到当前窗口的所有可执行元素。对于Android遍历影响不大,对于IOS遍历简直就是灾难,慢的无法接受;

List<WebElement> elementList = ((AppiumDriver) driver).findElements(By.xpath("//*[@clickable='true' and @enabled='true']"));

b)懒加载:获取窗口后,生成所有可执行元素的xpath,遍历出栈后再根据xpath获取WebElement。

2.减少无效操作
a)引入控件白名单机制
名单内的类才会被允许去遍历。UIAStaticText对iOS来说,误杀1%可以减少很多不必要的遍历

<!--控件白名单-->
<click>
<class>UIAImage</class>
<class>UIAButton</class>
<class>UIASwitch</class>
<class>UIATableCell</class>
<!--<class>UIAStaticText</class>-->
<class>UIAPickerWheel</class>
<class>UIACollectionCell</class>
</click>
<input>
<class>UIATextField</class>
<class>UIASearchBar</class>
<class>UIASecureTextField</class>
</input>

b)增加过滤策略
有的窗口会隐藏重复的入口,遍历工具能遍历到,这些过滤掉也能提升效率
以iOS为例,设计了4种过滤策略,不设置为默认不过滤

<!--1:id+clazz+name 2:id+clazz+name+label 3:id+clazz+name+label+value 4:no filter-->
<filter>1</filter>

c)runtime黑名单机制,出栈的节点任务会加入黑名单列表,确保不再重复执行

关于提升稳定性

a)黑名单机制
有些窗口想屏蔽掉,最好的办法是从根源解决,就是屏蔽入口,以iOS为例,支持text,name,label等属性的模糊匹配,xpath精确匹配

<!--黑名单-->
<blackList>
<item>相机</item>
<item>相册</item>
<item>照片</item>
<item>退出登录</item>
<item>拍摄名片</item>
<item>credit card camera</item>
<item>如您已完成添加,请重新登录</item>
<item>重新登录</item>
<item>//UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIAButton[1]</item>
<item>//UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIAButton[2]</item>
<item>//UIAApplication[1]/UIAWindow[1]/UIATableView[1]/UIAButton[3]</item>
</blackList>

b)触发器机制
满足xx条件,触发xx操作。根据遍历中遇到的情况,支持返回、延时、点击、手势密码盘解锁。

<trigger>
<item>分享到>>back</item>
<item>我的权益>>delay->8</item>
<item>温馨提示|立即开通|取消>>//UIAApplication[1]/UIAWindow[4]/UIAButton[1]</item>
<item>更多解锁方式>>gesture->//UIAButton[@name='blue circle']</item>
</trigger>

关于日志

日志分为系统日志和APP日志,系统日志又分为info和error,error的格式是固定的,便于分析出报告。
关于APP日志的设计
最初,如何打印日志,程序内部写死的,灵活性不高;
最终,如何打印日志,用什么工具、用哪些参数,支持用户定制化。

<log>
<ios>idevicesyslog -u #udid#</ios>
<android>adb -s #udid# logcat -v time -b events *:I | grep pingan</android>
</log>

关于截屏和录像

a)截屏功能
Android没有用appium的方法,直接调用命令比基于请求的效率更高;
iOS使用idevicescreenshot,比appium截图提高10倍效率。
b)录像功能
把操作过程转化为流媒体,提升用户体验

关于系统参数

可定制化Appium Server的port和host
可配置遍历深度和遍历时间
可配置截图和视频的目录

<global>
<!--Appium port-->
<port>5757</port>
<!--Appium host-->
<host>127.0.0.1</host>
<!--测试类型 1.android 2.ios 3.web-->
<mode>1</mode>
<!--遍历深度-->
<depth>0</depth>
<!--截图和视频的目录-->
<screenshot>/Users/mac/Desktop/png</screenshot>
<!--遍历时间 -->
<duration>0</duration>
<!--延时等待 -->
<interval>3</interval>
<!--超时 -->
<timeout>30</timeout>
</global>

关于算法

之前测试用的非完整版,水平有限勿喷,主要分为节点任务执行前、执行后的处理,后进先出的结构

/**
* 基于dfs的探索性遍历
*
* @param taskStack
* @param depth
*/

public void dfsSearch(Stack<UiNode> taskStack, int depth) {
int thisDepth, newTaskCount, repeatCount = 0;
UiNode thisNode;
WebElement element;
Stack<UiNode> children;
Stack<UiNode> existsTaskStack;
List<String> triggerList;
List<UiNode> blackList = new ArrayList<>();
String xpath, thisWindow, preWindow, thisPageSource, doBackWin;

// 首次获取窗口内容和窗口标识
thisPageSource = driver.getPageSource();
thisWindow = parser.getCurrentWindowID(thisPageSource);
preWindow = thisWindow;
triggerList = config.getTriggerList();

while (!taskStack.isEmpty()) {
if (repeatCount > config.getAllowSameWinTimes())
break;
thisNode = taskStack.pop();
blackList.add(thisNode);

try {
// 截图
screenShot();

// 触发器预处理
if (triggerProcessing(driver.getPageSource(), triggerList)) {
screenShot();
thisPageSource = driver.getPageSource();
thisWindow = parser.getCurrentWindowID(thisPageSource);
}

if (!thisWindow.equals(thisNode.getWindowID())) {
// 在任务栈中搜索当前窗口,如果存在,则获取该窗口下所有任务节点
existsTaskStack = searchByWindowID(thisWindow, taskStack);

// 如果当前窗口已存在任务栈中
if (null != existsTaskStack) {
repeatCount = 0;
resetTaskStack(taskStack, existsTaskStack);
} else {
Log.logInfo(thisNode.getWindowID() + " >> " + thisWindow + ", 窗口迁移至新窗口......");
if (preWindow.equals(thisWindow)) {
repeatCount = repeatCount + 1;
} else {
repeatCount = 0;
}
preWindow = thisWindow;
thisDepth = thisNode.getDepth();

// 遍历深度控制,0表示未限制
if (depth == 0 || thisDepth < depth) {
thisPageSource = driver.getPageSource();
thisWindow = parser.getCurrentWindowID(thisPageSource);
children = getTaskStack(Type.XML, thisPageSource, thisNode.getDepth() + 1);

// 是否获取到新窗口节点任务
newTaskCount = null != children ? children.size() : 0;

Log.logInfo(newTaskCount + "个新任务准备入栈......");
children = removeNodes(blackList, children);
children = filterNodes(taskStack, children);
children = updateTaskStack(children, thisNode);

// 如果有新的节点任务生成,把当前节点任务先压栈,新生成的节点任务出栈
if (null != children && children.size() > 0) {
Log.logInfo(children.size() + "个新任务允许入栈......");
taskStack.push(thisNode);
taskStack.addAll(children);

// 更新任务栈后,新任务出栈
thisNode = taskStack.pop();
blackList.add(thisNode);
}

if (newTaskCount == 0 || children.size() == 0 && needBack(thisWindow, taskStack)) {
doBack();
}

} else {
doBack();
}
}
}

// 每次迭代懒加载元素对象
xpath = thisNode.getId().split("-")[3];
element = driver.findElement(By.xpath(xpath));
if (thisNode.getAction().equals(Action.CLICK)) {
element.click();
} else if (thisNode.getAction().equals(Action.INPUT)) {
// todo something
}

// 任务执行后获取窗口内容和窗口标识
TimeUnit.SECONDS.sleep(config.getInterval());
thisPageSource = driver.getPageSource();
thisWindow = parser.getCurrentWindowID(thisPageSource);

// 如果同窗口的任务栈已处理完毕,并且还停留在该窗口,返回至上一个窗口
if (thisNode.getWindowID().equals(thisWindow) && needBack(thisNode.getWindowID(), taskStack)) {
// 获取返回后的窗口内容和窗口标识
doBackWin = doBack();
thisPageSource = null == doBackWin ? thisPageSource : doBackWin;
thisWindow = parser.getCurrentWindowID(thisPageSource);
}
} catch (NoSuchElementException e) {
continue;
} catch (org.openqa.selenium.ElementNotVisibleException e) {
continue;
} catch (org.openqa.selenium.NoSuchSessionException e) {
break;
} catch (org.openqa.selenium.SessionNotCreatedException e) {
break;
} catch (org.openqa.selenium.NotFoundException e) {
continue;
} catch (Exception e) {
continue;
}
}
}

转载请注明作者和出处

* 注:本文来自网络投稿,不代表本站立场,如若侵犯版权,请及时知会删除