初探UiAutomator2.0中使用Xpath定位元素
題外話
最近更新有點延遲哈,那是因為接了一個外包專案的活(就是移動端自動化相關的),忙的“外黑裡焦”的,好在應該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自動化的童鞋,後面有時間可以考慮一下,我更希望有童鞋主動來實現(哈哈,不做測試了,沒以前那麼大的熱情和精力來搞這個了)。
原文來自下方公眾號,轉載請聯絡作者,並務必保留出處。
想第一時間看到更多原創技術好文和資料,請關注公眾號: 測試開發棧