本文說明如何設定各種常見的 Espresso 測試。
比對另一個檢視畫面旁的檢視畫面
版面配置可以包含其本身重複的特定檢視區塊。舉例來說,聯絡人資料表中的重複通話按鈕可以有相同的 R.id
、包含相同的文字,以及與檢視區塊階層中其他呼叫按鈕相同的屬性。
例如,在這個活動中,含有文字 "7"
的檢視畫面會在多個資料列中重複:
一般而言,非不重複檢視畫面會與旁邊的特定標籤配對,例如通話按鈕旁的聯絡人姓名。在這種情況下,您可以使用 hasSibling()
比對器縮小選取範圍:
Kotlin
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click())
Java
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click());
比對動作列內的檢視畫面
ActionBarTestActivity
有兩個不同的動作列:一般動作列以及透過選項選單建立的關聯動作列。兩個動作列都有一個一律顯示的項目,以及兩個只能在溢位選單中顯示的項目。有人點選項目時,系統會將 TextView 變更為點選項目的內容。
兩個動作列的顯示圖示相當簡單,如以下程式碼片段所示:
Kotlin
fun testClickActionBarItem() { // We make sure the contextual action bar is hidden. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()) // Click on the icon - we can find it by the r.Id. onView(withId(R.id.action_save)) .perform(click()) // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Save"))) }
Java
public void testClickActionBarItem() { // We make sure the contextual action bar is hidden. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()); // Click on the icon - we can find it by the r.Id. onView(withId(R.id.action_save)) .perform(click()); // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Save"))); }
以下為關聯動作列的程式碼看起來完全相同:
Kotlin
fun testClickActionModeItem() { // Make sure we show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()) // Click on the icon. onView((withId(R.id.action_lock))) .perform(click()) // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Lock"))) }
Java
public void testClickActionModeItem() { // Make sure we show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()); // Click on the icon. onView((withId(R.id.action_lock))) .perform(click()); // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Lock"))); }
部分裝置會有硬體溢位選單按鈕,點選後會開啟一般溢位選單,因此在一般動作列中點選項目會比較難,幸好,Espresso 幫我們處理這個問題
如果是一般動作列:
Kotlin
fun testActionBarOverflow() { // Make sure we hide the contextual action bar. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()) // Open the options menu OR open the overflow menu, depending on whether // the device has a hardware or software overflow menu button. openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext<Context>()) // Click the item. onView(withText("World")) .perform(click()) // Verify that we have really clicked on the icon by checking // the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("World"))) }
Java
public void testActionBarOverflow() { // Make sure we hide the contextual action bar. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()); // Open the options menu OR open the overflow menu, depending on whether // the device has a hardware or software overflow menu button. openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext()); // Click the item. onView(withText("World")) .perform(click()); // Verify that we have really clicked on the icon by checking // the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("World"))); }
這是在有硬體溢位選單按鈕的裝置上顯示外觀:
就情境動作列而言,這其實非常簡單:
Kotlin
fun testActionModeOverflow() { // Show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()) // Open the overflow menu from contextual action mode. openContextualActionModeOverflowMenu() // Click on the item. onView(withText("Key")) .perform(click()) // Verify that we have really clicked on the icon by // checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Key"))) } }
Java
public void testActionModeOverflow() { // Show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()); // Open the overflow menu from contextual action mode. openContextualActionModeOverflowMenu(); // Click on the item. onView(withText("Key")) .perform(click()); // Verify that we have really clicked on the icon by // checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Key"))); } }
如要查看這些範例的完整程式碼,請查看 GitHub 上的 ActionBarTest.java
範例。
斷言未顯示檢視畫面
執行一系列動作後,一定會想要宣告測試中的 UI 狀態。有時候,這可能是負面情況,例如部分沒有發生。請注意,您可以使用 ViewAssertions.matches()
,將任何吊床檢視比對器轉換為 ViewAssertion
。
在以下範例中,我們會擷取 isDisplayed()
比對器,並使用標準 not()
比對器撤銷該比對器:
Kotlin
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.hamcrest.Matchers.not onView(withId(R.id.bottom_left)) .check(matches(not(isDisplayed())))
Java
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.Matchers.not; onView(withId(R.id.bottom_left)) .check(matches(not(isDisplayed())));
如果檢視區塊仍屬於階層結構,上述方法就能派上用場。如果不是,您會收到 NoMatchingViewException
,您需要使用 ViewAssertions.doesNotExist()
。
斷言檢視畫面不存在
如果檢視畫面已從檢視區塊階層中移出 (在動作導致轉換到其他活動時可能發生),則應使用 ViewAssertions.doesNotExist()
:
Kotlin
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withId onView(withId(R.id.bottom_left)) .check(doesNotExist())
Java
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.matcher.ViewMatchers.withId; onView(withId(R.id.bottom_left)) .check(doesNotExist());
斷言資料項目不在轉接器中
如要證明特定資料項目不在 AdapterView
內,您必須稍加不同操作。我們必須找到想要的 AdapterView
,並調查其所持有的資料。我們不需要使用 onData()
。我們會改用 onView()
找出 AdapterView
,然後使用其他比對器處理檢視表中的資料。
首先,比對器:
Kotlin
private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> { return object : TypeSafeMatcher<View>() { override fun describeTo(description: Description) { description.appendText("with class name: ") dataMatcher.describeTo(description) } public override fun matchesSafely(view: View) : Boolean { if (view !is AdapterView<*>) { return false } val adapter = view.adapter for (i in 0 until adapter.count) { if (dataMatcher.matches(adapter.getItem(i))) { return true } } return false } } }
Java
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; } }; }
接著,我們只需要 onView()
來尋找 AdapterView
:
Kotlin
fun testDataItemNotInAdapter() { onView(withId(R.id.list)) .check(matches(not(withAdaptedData(withItemContent("item: 168"))))) } }
Java
@SuppressWarnings("unchecked") public void testDataItemNotInAdapter() { onView(withId(R.id.list)) .check(matches(not(withAdaptedData(withItemContent("item: 168"))))); } }
此外,如果在包含 ID 清單的轉接程式檢視畫面中,有等於「item: 168」的項目,斷言就會失敗。
如需完整範例,請查看 GitHub 上 AdapterViewTest.java
類別中的 testDataItemNotInAdapter()
方法。
使用自訂失敗處理常式
將 Espresso 中的預設 FailureHandler
替換為自訂項目,即可執行其他或不同的錯誤處理機制,例如擷取螢幕截圖或傳遞額外的偵錯資訊。
CustomFailureHandlerTest
範例示範如何實作自訂故障處理常式:
Kotlin
private class CustomFailureHandler(targetContext: Context) : FailureHandler { private val delegate: FailureHandler init { delegate = DefaultFailureHandler(targetContext) } override fun handle(error: Throwable, viewMatcher: Matcher<View>) { try { delegate.handle(error, viewMatcher) } catch (e: NoMatchingViewException) { throw MySpecialException(e) } } }
Java
private static class CustomFailureHandler implements FailureHandler { private final FailureHandler delegate; public CustomFailureHandler(Context targetContext) { delegate = new DefaultFailureHandler(targetContext); } @Override public void handle(Throwable error, Matcher<View> viewMatcher) { try { delegate.handle(error, viewMatcher); } catch (NoMatchingViewException e) { throw new MySpecialException(e); } } }
這個失敗處理常式會擲回 MySpecialException
而不是 NoMatchingViewException
,並將所有其他失敗作業委派給 DefaultFailureHandler
。CustomFailureHandler
可以在測試的 setUp()
方法中向 Espresso 註冊:
Kotlin
@Throws(Exception::class) override fun setUp() { super.setUp() getActivity() setFailureHandler(CustomFailureHandler( ApplicationProvider.getApplicationContext<Context>())) }
Java
@Override public void setUp() throws Exception { super.setUp(); getActivity(); setFailureHandler(new CustomFailureHandler( ApplicationProvider.getApplicationContext())); }
詳情請參閱 FailureHandler
介面和
Espresso.setFailureHandler()
。
目標非預設視窗
Android 支援多個視窗。一般而言,對使用者和應用程式開發人員而言都很透明,但是在某些情況下,畫面上會有多個視窗,例如當自動完成視窗繪製在搜尋小工具的主要應用程式視窗上時。為了簡化作業,根據預設,Espresso 會使用經驗法則推測您打算互動的Window
資料。這個經驗法則幾乎總是已足夠;不過,在極少數情況下,您必須指定互動的目標視窗。如要執行此操作,請提供自己的根視窗比對器或 Root
比對器:
Kotlin
onView(withText("South China Sea")) .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView())))) .perform(click())
Java
onView(withText("South China Sea")) .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))) .perform(click());
就像使用 ViewMatchers
的情況一樣,我們提供了一組預先提供的 RootMatchers
。當然,您隨時可以實作自己的 Matcher
物件。
查看 GitHub 上的 MultipleWindowTest 範例。
在清單檢視中比對頁首或頁尾
使用 addHeaderView()
和 addFooterView()
方法可將標頭和頁尾新增至 ListViews
。為確保 Espresso.onData()
知道要比對的資料物件,請務必將預設資料物件值做為第二個參數傳遞至 addHeaderView()
和 addFooterView()
。例如:
Kotlin
const val FOOTER = "FOOTER" ... val footerView = layoutInflater.inflate(R.layout.list_item, listView, false) footerView.findViewById<TextView>(R.id.item_content).text = "count:" footerView.findViewById<TextView>(R.id.item_size).text = data.size.toString listView.addFooterView(footerView, FOOTER, true)
Java
public static final String FOOTER = "FOOTER"; ... View footerView = layoutInflater.inflate(R.layout.list_item, listView, false); footerView.findViewById<TextView>(R.id.item_content).setText("count:"); footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size())); listView.addFooterView(footerView, FOOTER, true);
接著,您就可以編寫頁尾的比對器:
Kotlin
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` fun isFooter(): Matcher<Any> { return allOf(`is`(instanceOf(String::class.java)), `is`(LongListActivity.FOOTER)) }
Java
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @SuppressWarnings("unchecked") public static Matcher<Object> isFooter() { return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER)); }
在測試中載入檢視畫面並不容易:
Kotlin
import androidx.test.espresso.Espresso.onData import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.sample.LongListMatchers.isFooter fun testClickFooter() { onData(isFooter()) .perform(click()) // ... }
Java
import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.sample.LongListMatchers.isFooter; public void testClickFooter() { onData(isFooter()) .perform(click()); // ... }
查看完整的程式碼範例,您可以在 GitHub 上的 AdapterViewTest.java
的 testClickFooter()
方法中找到。