题外话
最近更新有点延迟哈, 那是因为接了一个外包项目的活(就是移动端自动化相关的), 忙的 "外黑里焦" 的, 好在应该 2 个星期的努力已经进入尾期, 项目整体功能都已经实现, 后面有空给大家分享, 今天的主题是讲一下在使用过程中遇到的一个问题, 如何在 UiAutomator2.0 中使用 Xpath 定位元素?
背景
现在的 App 在打包成 apk 的时候都是有加固处理的, 各种混淆加固, 所以已经破坏了或扰乱了原本的代码变量命名形式, 这就给我们要基于界面来做自动化测试带来了灾难性的阻碍, 因为那些混淆过的 id 是不固定的, 下一次再出个新版本, 这一切都变了, 所以这就没办法用 id 来定位混淆过的 App 元素, 那还有什么好的方法吗? 还记得 web 自动化测试中神乎其技的 xpath 吗? 不管什么元素都可以用它定位出来, 所以我就想在 UiAutomator2.0 中也使用它来定位混淆的 App 元素, 这要如何操作? UiAutomator2.0 的 API 中并没有给出 xpath 这种方式, 那我们只能自己去写一个了.
思路
参考 UI Automator Viewer 中抓取到的结构层次, 不能用 resource-id, 又要体现出层次关系, 那就只能是 class 属性了, 这里的 class 可以对应 Web xpath 中的标签, 使用业界统一的斜杠 / 来保持层次, 那么最原始状态下的 xpath 大概就是这个样子了:
Android.view.ViewGroup/Android.widget.ImageView
再加上下标
Android.view.ViewGroup[2]/Android.widget.ImageView[0]
xpath 的格式定义出来了之后, 我们就开始一层一层去遍历, 很简单通过斜杠 / 来分隔出一个 class 数组, 然后依次去查找这些 class 对应的元素, 通过父子关系拼接起来, 直到最后一个 class, 存在就返回对应的对象, 不存在就返回 null.
由于时间关系, 这一次就是初探, 只实现了这种绝对路径 (/) 下的定位, 其实要想完整完成这个功能, 还需要支持相对路径 (//) 的定位, 以及各种属性的组合定位, 其实基于这个版本上面改改也不远了, 这就留给有兴趣的童鞋去完成吧.
实现
1, 首先要实现根据 class 或其他属性去找到某个元素的子元素, 我这里实现了支持传入各种参数, 代码如下:
- public static UiObject2 getChild(Object root, Map<String,String> params) {
- if (params == null || !params.containsKey("class")) {
- log.e("[Error]参数错误: 为空或未包含[class]key");
- return null;
- }
- String clazz = params.get("class");
- String className = clazz;
- int index = 0;
- if (clazz.endsWith("]") && clazz.contains("[")) { // 有下标
- className = clazz.substring(0, clazz.lastIndexOf("["));
- String num = clazz.substring(clazz.lastIndexOf("[") + 1, clazz.lastIndexOf("]"));
- index = num != null && !"".equals(num) ? Integer.parseInt(num) : index;
- }
- List<UiObject2> childList = null;
- if (root instanceof UiObject2) {
- childList = ((UiObject2) root).getChildren();
- } else {
- childList = hasObjects(By.clazz(className)) ? mDevice.findObjects(By.clazz(className)) : null;
- }
- List<UiObject2> tempList = new ArrayList<UiObject2>();
- if (childList != null && !childList.isEmpty()) {
- for (UiObject2 child : childList) {
- boolean isMatch = child.getClassName().equals(className);
- if (params.containsKey("pkg")) {
- isMatch = isMatch && child.getApplicationPackage().equals(params.get("pkg"));
- }
- if (params.containsKey("text")) {
- isMatch = isMatch && child.getText().equals(params.get("text"));
- }
- if (params.containsKey("desc")) {
- isMatch = isMatch && child.getContentDescription().equals(params.get("desc"));
- }
- if (isMatch) {
- tempList.add(child);
- }
- }
- }
- if(tempList.isEmpty()) {
- return null;
- }
- if (index>= tempList.size()) {
- log.e(String.format("[Error]查找 class[%s] 下标 [%d] 越界[%d]", clazz, index, tempList.size()));
- return null;
- }
- return tempList.get(index);
- }
2, 再写一个通过 class 获取子元素的简单实现, 因为这种方式用的多:
- public static UiObject2 getChild(Object root, String clazz) {
- Map<String,String> params = new HashMap<String,String>();
- params.put("class", clazz);
- return getChild(root, params);
- }
3, 加入解析 xpath 表达式的部分, 将解析和查找整个过程连起来:
- public static UiObject2 findObjectByXpath(UiObject2 root, String xpath) {
- if (xpath == null && "".equals(xpath)) {
- log.e("[Error]xpath expression[" + xpath + "] is invalid");
- return null;
- }
- String[] xpaths = null;
- if (xpath.contains("/")) {
- xpaths = xpath.split("/");
- } else {
- xpaths = new String[]{xpath};
- }
- UiObject2 preNode = root;
- for (String path : xpaths) {
- preNode = getChild(preNode, path);
- if (preNode == null) {
- //log.e(String.format("按 xpath[%s]查找元素失败, 未找到 class[%s]对应的节点", xpath, path));
- break;
- }
- }
- return preNode;
- }
4, 使用演示:
- String commentXpath = "android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView[0]";
- UiObject2 commentView = findObjectByXpath(root, commentXpath);
总结
既然是初探就先写这么多吧, 给个实现思路, 如果把整个功能都完成, 可以考虑开源到 GitHub 上方便千千万万其他 U2 自动化的童鞋, 后面有时间可以考虑一下, 我更希望有童鞋主动来实现(哈哈, 不做测试了, 没以前那么大的热情和精力来搞这个了).
来源: http://www.jianshu.com/p/c2cfdd05bf35