Espresso 方法

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

比對另一個檢視畫面旁的檢視畫面

版面配置可以包含其本身重複的特定檢視區塊。舉例來說,聯絡人資料表中的重複通話按鈕可以有相同的 R.id、包含相同的文字,以及與檢視區塊階層中其他呼叫按鈕相同的屬性。

例如,在這個活動中,含有文字 "7" 的檢視畫面會在多個資料列中重複:

清單活動顯示 3 項目清單中相同檢視畫面元素的 3 個副本

一般而言,非不重複檢視畫面會與旁邊的特定標籤配對,例如通話按鈕旁的聯絡人姓名。在這種情況下,您可以使用 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,並將所有其他失敗作業委派給 DefaultFailureHandlerCustomFailureHandler 可以在測試的 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.javatestClickFooter() 方法中找到。