◆版權聲明:本文出自胖喵~的博客,轉載必須注明出處。
轉載請注明出處:http://www.cnblogs.com/by-dream/p/5997557.html
前言
Espresso的提供了不少API支持使用者來和界面元素進行交互,但同時它又阻止使用者直接獲取Activity和View,它為的就是想保持讓這些對象在UI線程中執行,以防發生線程不安全的情況。因此在Espresso中我們看不到getView、getCurrentActivity類似這樣的方法。但是我們可以通過實行自己的ViewAction和ViewAssertion來安全的操作View。這就是Espresso的思想。
認識組件
Espresso:與視圖交互的入口(通過onView和onData),還包含一些不綁定到任何元素上的API(例如pressBack)。
ViewMatchers: 實現Matcher<? super View>接口對象的集合,可以將一個或者多個傳遞給onView,從而定位當前視圖中的元素。
ViewActions:可以傳遞到ViewInteraction.perform方法的集合(例如click)。
ViewAssertions:可以傳遞到ViewInteraction.check方法的集合。 大多數時候需要matches斷言,即使用View Matcher來和當前選擇的視圖的狀態進行斷言。
舉例:
定位元素onView
onView使用的是一個hamcrest匹配器,該匹配器只匹配當前視圖層次結構中的一個(且只有一個)視圖。如果你不熟悉hamcrest匹配器,建議先看看這個。通常情況下一個控件的id是唯一的,但是有些特定的視圖是無法通過R.id拿到,因此我們就需要訪問Activity或者Fragment的私有成員找到擁有R.id的容器。有的時候也需要使用ViewMatchers來縮小定位的范圍。
最簡單的onView就是這樣的形式:
onView(withId(R.id.my_view))
有的時候多個視圖之間共享R.id值,當這種情況下,我們調用系統會拋出這樣的異常AmbiguousViewMatcherException:
java.lang.RuntimeException: com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException: This matcher matches multiple views in the hierarchy: (withId: is <123456789>)
當然系統給出你詳細的信息,讓你進行排查:
+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button, visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true, window-focus=true, is-focused=false, is-focusable=false, enabled=true, selected=false, is-layout-requested=false, text=, root-is-layout-requested=false, x=0.0, y=625.0, child-count=1} ****MATCHES**** | +------>OtherView{id=123456789, res-name=plus_one_standard_ann_button, visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true, window-focus=true, is-focused=false, is-focusable=true, enabled=true, selected=false, is-layout-requested=false, text=Hello!, root-is-layout-requested=false, x=0.0, y=0.0, child-count=1} ****MATCHES****
通過上面的信息對比,我們可以發現text字段是不一致的,因此我們就可以根據這個組合匹配來縮小定位范圍,方法如下:
onView(allOf(withId(R.id.my_view), withText("Hello!")))
你也可以使用這樣的方法:
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
對於大部分的控件,使用上述的方法就可以搞定了,如果你發現使用“withText”或“withContentDescription”都無法定位到元素的時候,谷歌建議你可以給開發提一個可訪問性的bug了。
下面這種情況,出現了很多同樣的數字,但是它旁邊有可以識別出的唯一元素,這個時候我們就也可以使用hasSibling來進行篩選:
onView(allOf(withText("7"), hasSibling(withText("item: 0")))).perform(click());
另外說兩個常用的menu,如果是 overflow menu也就是下面這種情況的下:
需要使用:
openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
如果是下面這樣的:
使用:
openContextualActionModeOverflowMenu();
注意:如果目標視圖在AdapterView(例如ListView,GridView,Spinner)中,onView方法可能無法正常工作,這個時候需要用到onData方法。
定位元素onData
假設一個Spinner的控件,我們要點擊“Americano”,我們使用默認的Adaptor,它的字段默認是String的,因此當我們要進行點擊的時候,就可以使用如下方法:
onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
假設是一個Listview,我們需要點擊Listview中第二個item的按鈕,那么我們需要這樣寫:
onData(Matchers.allOf()) .inAdapterView(withId(R.id.photo_gridview)) // listview的id .atPosition(1) // 所在位置 .onChildView(withId(R.id.imageview_photo)) // item中子控件id .perform(click());
執行操作
當你獲取到了目標的控件后,就可以使用perform來執行操作了。例如一個點擊操作:
onView(...).perform(click());
也可以通過一個命令執行多個操作:
// 輸入hello,並且點擊 onView(...).perform(typeText("Hello"), click());
當你操作的對象如果是ScrollView,在執行其他操作(例如click、typeText)之前,必須確保當前的控件是出現在當前的可視范圍的,若沒有可以使用scrollTo的方法:
onView(...).perform(scrollTo(), click());
如果可視范圍已經出現了該元素,scrollTo將不起作用,因此當你的屏幕分辨率大或小的時候,都可以放心安全地使用它。
校驗
使用check方法可以斷言當前選擇的界面, 常用的斷言是matches,它使用ViewMatcher來斷言當前選定視圖的狀態。例如,要檢查視圖中是否包含“Hello”這個字符串:
onView(...).check(matches(withText("Hello")));
千萬不要使用下面這樣的方法做斷言,谷歌是不推薦這樣的:
// Don't use assertions like withText inside onView. onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
所以當我們需要斷言一個指定的內容是否在AdapterView當中的時候,我們需要做一些特殊的處理。做法就是找到AdapterView,然后訪問它的內部元素,這里不適用onData,而是使用onView和我們自己寫的matcher來進行處理。我們自定義一個matcher叫withAdaptedData,實現如下:
private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("with class name: "); dataMatcher.describeTo(description); } @Override public boolean matchesSafely(View view) { if (!(view instanceof AdapterView)) { return false; } @SuppressWarnings("rawtypes") Adapter adapter = ((AdapterView) view).getAdapter(); for (int i = 0; i < adapter.getCount(); i++) { if (dataMatcher.matches(adapter.getItem(i))) { return true; } } return false; } }; }
然后我們就可以使用它來進行斷言了:
// 當list當中是否存在一個bryan的item,就斷言失敗 onView(withId(R.id.list)).check(matches(not(withAdaptedData(withItemContent("bryan")))));
因為里面還用到了一個withItemContent,我們也需要實現它:
public static Matcher<Object> withItemContent(final Matcher<String> itemTextMatcher) { // use preconditions to fail fast when a test is creating an invalid matcher. checkNotNull(itemTextMatcher); return new BoundedMatcher<Object, Map>(Map.class) { @Override public boolean matchesSafely(Map map) { return hasEntry(equalTo("STR"), itemTextMatcher).matches(map); } @Override public void describeTo(Description description) { description.appendText("with item content: "); itemTextMatcher.describeTo(description); } }; }
所以當我們要斷言時,如果遇到了一些沒有實現的內容,就需要我們重寫matcher了。
參考圖
下面這幅圖,我們在寫代碼中可以快速查看,這里面包含了大部分我們經常用的API:
細心的人應該能看到圖中有intent相關的內容,這部分內容我目前沒有用到,因此也沒有深入的了解。有興趣的可以自己看看。
參考鏈接:https://google.github.io/android-testing-support-library/docs/espresso/intents/index.html