Espresso 方法

本文說明如何設定各種常見的 Espresso 測試。

比對檢視畫面與其他檢視畫面

版面配置中可以包含不同的特定檢視畫面。適用對象 例如,在聯絡人表格中設定重複通話按鈕時,可以 R.id,包含相同的文字,以及與其他呼叫相同的屬性 按鈕。

例如,在這個活動中,含有文字 "7" 的檢視區塊會在多個項目中重複出現 列:

清單活動顯示同一個檢視畫面元素的 3 個副本
     在 3 項商品清單中

非唯一資料檢視通常會與 ,例如通話按鈕旁的聯絡人姓名。在本例中 可以使用 hasSibling() 比對器縮小選取範圍:

KotlinJava
onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
   
.perform(click())
onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
   
.perform(click());

比對動作列中的檢視畫面

ActionBarTestActivity 有兩個不同的動作列:一般 動作列,以及從選項選單建立的關聯動作列。兩者皆有 動作列包含一個隨時顯示的項目,以及兩個 顯示在溢位選單中使用者點選項目時,會將 TextView 變更為 點擊項目的內容。

如上圖所示,兩個動作列的對應圖示非常簡單明瞭 :

KotlinJava
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")))
}
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")));
}

儲存按鈕位於活動頂端的動作列

程式碼針對關聯動作列的程式碼看起來一樣:

KotlinJava
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")))
}
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 會處理 。

一般動作列:

KotlinJava
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")))
}
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")));
}

系統會顯示溢位選單按鈕,
          動作列位於畫面頂端

在有硬體溢位選單按鈕的裝置上,顯示畫面如下:

沒有溢位選單按鈕,底部附近會顯示一份清單
          螢幕。

針對關聯動作列,您可以再簡單不過:

KotlinJava
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")))
   
}
}
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() 比對器:

KotlinJava
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())))
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():

KotlinJava
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())
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,然後再使用另一個 比對器來處理檢視畫面中的資料。

首先,比對器:

KotlinJava
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
       
}
   
}
}
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

KotlinJava
fun testDataItemNotInAdapter() {
    onView
(withId(R.id.list))
         
.check(matches(not(withAdaptedData(withItemContent("item: 168")))))
   
}
}
@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter() {
    onView
(withId(R.id.list))
         
.check(matches(not(withAdaptedData(withItemContent("item: 168")))));
   
}
}

此外,對於等於「item: 168」的項目,系統會斷言將會失敗 存在於 ID 清單的轉接程式檢視畫面中。

如需完整範例,請查看以下程式碼中的 testDataItemNotInAdapter() 方法: AdapterViewTest.java 類別。

使用自訂失敗處理常式

將 Espresso 中的預設 FailureHandler 替換成自訂值,以供下列用途: 其他或不同的錯誤處理方式,例如擷取螢幕畫面或傳送 以及額外的偵錯資訊

CustomFailureHandlerTest 範例說明如何實作 失敗處理常式:

KotlinJava
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)
       
}

   
}
}
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,並將所有其他失敗作業委派給 DefaultFailureHandlerCustomFailureHandler 可用來註冊 在測試的 setUp() 方法中使用 Espresso:

KotlinJava
@Throws(Exception::class)
override fun setUp() {
   
super.setUp()
    getActivity
()
    setFailureHandler
(CustomFailureHandler(
           
ApplicationProvider.getApplicationContext<Context>()))
}
@Override
public void setUp() throws Exception {
   
super.setUp();
    getActivity
();
    setFailureHandler
(new CustomFailureHandler(
           
ApplicationProvider.getApplicationContext()));
}

詳情請參閱 FailureHandler敬上 介面和 Espresso.setFailureHandler()

目標非預設視窗

Android 支援多個視窗。通常對使用者來說,這是公開透明的 和應用程式開發商,但在特定情況下,系統會顯示多個視窗,例如 就像在應用程式的主應用程式視窗上繪製自動完成視窗一樣 搜尋小工具為了簡化內容,Espresso 預設會使用經驗法則 猜猜您打算與哪個Window互動。這種啟發式演算法將 永遠很好;不過在極少數的情況下,您必須指定 互動應設為目標方法是自行提供根視窗 比對器或 Root 比對器:

KotlinJava
onView(withText("South China Sea"))
   
.inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
   
.perform(click())
onView(withText("South China Sea"))
   
.inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
   
.perform(click());

就像使用 ViewMatchers、 我們會提供一組預先提供的 RootMatchers。 當然,您隨時都可實作自己的 Matcher 物件。

查看 MultipleWindowTest 範例

ListViews透過 addHeaderView()addFooterView() 方法。確保 Espresso.onData() 知道哪個資料物件 因此,請務必將預設的資料物件值當做第二個參數 傳送至 addHeaderView()addFooterView()。例如:

KotlinJava
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)
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);

接著,您可以撰寫頁尾比對器:

KotlinJava
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))
}
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));
}

而且在測試中載入檢視畫面很困難:

KotlinJava
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())

   
// ...
}
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());

   
// ...
}

查看完整程式碼範例,位於以下程式碼的 testClickFooter() 方法中: AdapterViewTest.java