UIAutomator定位Android控制項的方法實踐和建議(Appium姊妹篇),uiautomatorappium
在本人之前的一篇文章<<Appium基於安卓的各種FindElement的控制項定位方法實踐和建議>>第二章節談到Appium可以通過使用UIAutomator的方法去定位Android介面上的控制項,當時只是一筆帶過舉了個例子。如該文給自己的承諾,今天特撰寫此文以描述UIAutomator各種控制項定位的方法,以作為前文的姊妹篇互連有無。
1. 背景
為了和前文達成一致,這次的實踐對象同樣也是使用SDK內建的NotePad應用,同樣是嘗試去獲得在NotesList那個Activity裡的Menu Options上面的那個Add note菜單選項。以下是UIAutomatorViewer對介面的一個.
但有一個例外的地方是下文的”通過偽xpath方法定位控制項“章節執行個體需要使用到的是NoteEditor這個activity裡面的Menu options,因為需要示範通過子控制項獲得父控制項然後得到兄弟控制項的功能,UIAutomatorViewer如下。
2. 通過文本資訊定位通過控制項的text屬性定位控制項應該是最常用的一種方法了,畢竟行動裝置 App的螢幕大小有限,存在text重複的可能性並不大,就算真的有重複,可以添加其他定位方法來縮寫誤差。2.1 UISelector.text方法
addNote = new UiObject(new UiSelector().text("Add note")); assertEquals(addNote.getText(),"Add note");該方法通過直接尋找當前介面上所有的控制項來比較每個控制項的text屬性是否如預期值來定位控制項,挺好理解的,所以就沒有必要細說了。2.2. UISelector.textContains方法
addNote = new UiObject(new UiSelector().textContains("Add")); assertEquals(addNote.getText(),"Add note");此方法跟以上方法類似,但是不需要輸入控制項的全部text資訊。
2.3 UISelector.textStartsWith方法
addNote = new UiObject(new UiSelector().textStartsWith("Add")); assertEquals(addNote.getText(),"Add note");顧名思義,通過判斷一個控制項的text的開始是否和預期的字串相吻合來獲得控制項,其實個人覺得這個方法存在的必要性不強,因為它的功能完全可以用上面的方法或者下面的Regex的方法取代。況且既然你提供了textStartsWith方法,為什麼你不提供個textEndWith的方法呢!2.4 UISelector.textMatches方法
addNote = new UiObject(new UiSelector().textMatches("^Add.*")); assertEquals(addNote.getText(),"Add note");這個方法是通過Regex的方式來比較控制項的text來定位控制項,這裡有意思的是使用者使用的Regex是有限制的,請看該方法的官方描述:”
Set the search criteria to match the visible text displayed for a widget (for example, the text label to launch an app). The text for the widget must match exactly with the string in your input argument“。第一句我們不用管它,關鍵是第二句,翻譯過來就是”目標控制項的text(的所有內容)必須和我們輸入的Regex完全符合“。什麼意思呢?意思就是你不能像往常的Regex那樣通過比較text的部分吻合來獲得控制項。以下面代碼為例子:
addNote = new UiObject(new UiSelector().textMatches("^Add")); assertEquals(addNote.getText(),"Add note");正常來說這個Regex是沒有問題的,它的意思就是想要“擷取以Add開頭的text的控制項,至於Add字串口面是什麼值,沒有必要去管它”。但是按照我們上面的官方描述,這樣子是不行的,你必須要把Regex補充完整以使得正而運算式和控制項的text完全進行匹配,至於你用什麼萬用字元或者字串就完全按照Regex的文法了。注意這個限制在UISlector使用所有的Regex相關的方法中都有效哦。3 通過控制項的ClassName定位
通過這種方法定位控制項存在的一個問題是很容易發生重複,所以一般都是先用這種方法去narrow down目標控制項,然後再去添加其他如text判斷等條件進行控制項定位。
3.1 UISelector.className方法
addNote = new UiObject(new UiSelector().className("android.widget.TextView").text("Add note")); assertEquals(addNote.getText(),"Add note");執行個體中首先通過ClassName找到所有的TextView控制項,然後再在這些TextView控制項尋找text是”Add note“的控制項。3.2 UISelector.classNameMatches方法
addNote = new UiObject(new UiSelector().classNameMatches(".*TextView$")); assertEquals(addNote.getText(),"Add note");通過Regex判斷className是否和預期的一致,注意Regex的限制和章節2.4描述的一致。4. 通過偽xpath方法定位UISelector類提供了一些方法根據控制項在介面的XML布局中的層級關係來進行定位,但是UIAutomator又沒有真正的提供類似Appium的findElementWithXpath相關的方法,所以這裡我就稱之為偽xpath方法。這個章節使用到的不再是NotesList那個activity裡面的menu options,而是NoteEditor這個activity裡面的Menu options,裡面不止有一個Menu entry。4.1 通過UiSelector.fromParent或UiObject.getFromParent方法這種方法我覺得最有用的情況是測試代碼當前在操作的是同一層級的一組控制項中的某一個控制項,轉而又需要操作同一層級的另外一個控制項的時候。下面的執行個體就是通過save控制項的父控制項找到其同一層級的兄弟控制項delete。這裡分別列出了通過UiObject.getFromParent方法和UiSelector.fromParent方法的執行個體,事實上他們的功能是一樣的。UiObject.getFromPatrent方法:
save = new UiObject(new UiSelector().text("Save")); assertEquals(save.getText(),"Save"); delete = save.getFromParent(new UiSelector().text("Delete")); assertEquals(delete.getText(),"Delete");UiSelector.fromParent方法(這個例子有點迂迴笨拙,但為了示範功能就將就著看吧):
delete = new UiObject(new UiSelector().text("Save").fromParent(new UiSelector().text("Delete"))); assertEquals(delete.getText(),"Delete");4.2 通過UiSelector.childSelector或UiObject.getChild方法這種方法是在已知父控制項的時候如何快速的尋找該父控制項下面的子控制項。UiObject.getChild方法:
UiObject parentView = new UiObject(new UiSelector().className("android.view.View")); save = parentView.getChild(new UiSelector().text("Save")); assertEquals(save.getText(),"Save");UiSelector.childSelector方法:
save = new UiObject(new UiSelector().className("android.view.View").childSelector(new UiSelector().text("Save"))); assertEquals(save.getText(),"Save");5. 通過控制項ID定位在Android API Level18及其以上的版本增加了一個Android控制項的屬性ResourceId,所以要注意在使用這種方法之前先確保你的目標測試裝置和你的UIAutomoator庫jar包使用的都是API Level 18以上的版本。例如我自己使用的就是本地sdk中版本19的庫:
D:\Develops\AndroidSDK\platforms\android-19\uiautomator.jar5.1 UiSelector.resourceId方法
addNote = new UiObject(new UiSelector().resourceId("android:id/title")); assertEquals(addNote.getText(),"Add note");5.2 UiSelector.resourceIdMatches方法
addNote = new UiObject(new UiSelector().resourceIdMatches(".+id/title")); assertEquals(addNote.getText(),"Add note");注意Regex的限制和章節2.4描述的一致
6. 通過contentDescription定位在UiAutomator架構和使用了Uiautomator架構的Appium中,控制項的屬性contentDescription一直是強調開發人員需要添加進去的,因為
- 有些控制項使用其他辦法很難或者根本沒有辦法定位
- 最重要的是給每個控制項的contentDescription設計個唯一值讓我們可以非常快速的定位控制項,讓我們足夠敏捷!
以下的執行個體並沒有真正跑過的,因為Notepad應用上面的控制項是沒有contentDescription這個屬性的,但是如果我們假設Add note這個控制項的contentDescription是“AddNoteMenuDesc”的話,代碼的相應寫法應該就如下了。6.1 UiSelector.description方法
addNote = new UiObject(new UiSelector().description("AddNoteMenuDesc)); assertEquals(addNote.getText(),"Add note");
</pre><h2>6.2 UiSelector.descriptionContains方法</h2></div><div><pre name="code" class="java"> addNote = new UiObject(new UiSelector().descriptionContains("AddNote")); assertEquals(addNote.getText(),"Add note");6.3 UiSelector.descriptionStartWith方法
addNote = new UiObject(new UiSelector().descriptionStartsWith("AddNote")); assertEquals(addNote.getText(),"Add note");6.4 UiSelector.descriptionMatches方法
//addNote = new UiObject(new UiSelector().descriptionMatches("^AddNote.*$")); //assertEquals(addNote.getText(),"Add note");7.通過其他方法定位除了以上比較常用的方法外,UIAutomator還支援其他一些方法,比如根據控制項屬性是否可點擊可聚焦可長按等來縮小要定位的控制項的範圍,具體使用方法不一一列舉,可以查看以下測實驗證代碼。
package majcit.com.UIAutomatorDemo;import com.android.uiautomator.core.UiDevice;import com.android.uiautomator.core.UiObject;import com.android.uiautomator.core.UiObjectNotFoundException;import com.android.uiautomator.core.UiScrollable;import com.android.uiautomator.core.UiSelector;import com.android.uiautomator.testrunner.UiAutomatorTestCase;import static org.hamcrest.Matchers.*;import static org.hamcrest.MatcherAssert.assertThat;public class UISelectorFindElementTest extends UiAutomatorTestCase { public void testDemo() throws UiObjectNotFoundException { UiDevice device = getUiDevice(); device.pressHome(); // Start Notepad UiObject appNotes = new UiObject(new UiSelector().text("Notes")); appNotes.click(); //Sleep 3 seconds till the app get ready try { Thread.sleep(3000); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } //Evoke the system menu option device.pressMenu(); UiObject addNote = new UiObject(new UiSelector().text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject (new UiSelector().checked(false).clickable(true)); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().className("android.widget.TextView").text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().classNameMatches(".*TextView$")); assertEquals(addNote.getText(),"Add note"); //addNote = new UiObject(new UiSelector().description("AddNoteMenuDesc)); //assertEquals(addNote.getText(),"Add note"); //addNote = new UiObject(new UiSelector().descriptionContains("AddNote")); //assertEquals(addNote.getText(),"Add note"); //addNote = new UiObject(new UiSelector().descriptionStartsWith("AddNote")); //assertEquals(addNote.getText(),"Add note"); //addNote = new UiObject(new UiSelector().descriptionMatches("^AddNote.*$")); //assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().focusable(true).text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().focused(false).text("Add note")); assertEquals(addNote.getText(),"Add note"); //TBD //addNote = new UiObject(new UiSelector().fromParent(selector)) addNote = new UiObject(new UiSelector().index(0).text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().className("android.widget.TextView").enabled(true).instance(0)); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().longClickable(false).text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().text("Add note")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().textContains("Add")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().textStartsWith("Add")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().textMatches("Add.*")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().resourceId("android:id/title")); assertEquals(addNote.getText(),"Add note"); addNote = new UiObject(new UiSelector().resourceIdMatches(".+id/title")); assertEquals(addNote.getText(),"Add note"); //Go to the editor activity, need to cancel menu options first device.pressMenu(); //Find out the new added note entry UiScrollable noteList = new UiScrollable( new UiSelector().className("android.widget.ListView")); //UiScrollable noteList = new UiScrollable( new UiSelector().scrollable(true)); UiObject note = null; if(noteList.exists()) { note = noteList.getChildByText(new UiSelector().className("android.widget.TextView"), "Note1", true); //note = noteList.getChildByText(new UiSelector().text("Note1"), "Note1", true); } else { note = new UiObject(new UiSelector().text("Note1")); } assertNotNull(note); //Go to the NoteEditor activity note.click(); device.pressMenu(); UiObject save = null; UiObject delete = null; save = new UiObject(new UiSelector().text("Save")); assertEquals(save.getText(),"Save"); delete = save.getFromParent(new UiSelector().text("Delete")); assertEquals(delete.getText(),"Delete"); delete = new UiObject(new UiSelector().text("Save").fromParent(new UiSelector().text("Delete"))); assertEquals(delete.getText(),"Delete"); save = new UiObject(new UiSelector().className("android.view.View").childSelector(new UiSelector().text("Save"))); assertEquals(save.getText(),"Save"); UiObject parentView = new UiObject(new UiSelector().className("android.view.View")); save = parentView.getChild(new UiSelector().text("Save")); assertEquals(save.getText(),"Save"); } }