本文說明如何使用 Espresso API 完成常見的自動化測試工作。
Espresso API 鼓勵測試作者思考使用者與應用程式互動時可執行的操作,也就是找出 UI 元素並與使用者互動。同時,該架構會防止直接存取應用程式的活動和檢視畫面,因為將這些物件保留在 UI 執行緒上,是測試不穩定性的主要來源。因此,Espresso API 中不會顯示 getView()
和 getCurrentActivity()
等方法。您還是可以藉由實作自己的 ViewAction
和 ViewAssertion
子類別,安全地在 View 上執行作業。
API 元件
Espresso 的主要元件包括:
- Espresso – 與檢視畫面互動的進入點 (透過
onView()
和onData()
)。此外,也會提供未必與任何檢視區塊相關聯的 API,例如pressBack()
。 - ViewMatchers – 實作
Matcher<? super View>
介面的物件集合。您可以將一或多個方法傳遞至onView()
方法,在目前的檢視區塊階層中尋找檢視區塊。 - ViewActions - 可傳遞至
ViewInteraction.perform()
方法的ViewAction
物件集合,例如click()
。 - ViewAssertions:一組可傳遞
ViewInteraction.check()
方法的ViewAssertion
物件。在多數情況下,您將使用比對斷言,此方法會使用 View 比對器宣告目前所選檢視畫面的狀態。
例子:
Kotlin
// withId(R.id.my_view) is a ViewMatcher // click() is a ViewAction // matches(isDisplayed()) is a ViewAssertion onView(withId(R.id.my_view)) .perform(click()) .check(matches(isDisplayed()))
Java
// withId(R.id.my_view) is a ViewMatcher // click() is a ViewAction // matches(isDisplayed()) is a ViewAssertion onView(withId(R.id.my_view)) .perform(click()) .check(matches(isDisplayed()));
尋找檢視畫面
在絕大多數情況下,onView()
方法會取用應在目前的檢視區塊階層中,與一個檢視表相符的吊床比對器。比對工具功能強大,且與 Mockito 或 JUnit 搭配使用的使用者熟悉這些比對工具。如果您不熟悉吊床比對器,建議您先快速觀看這種呈現方式。
您需要的檢視畫面通常具有專屬的 R.id
,且使用簡易的 withId
比對器會縮小檢視搜尋範圍。不過,許多合理情況都是在測試開發時無法決定 R.id
。例如,特定檢視畫面可能沒有 R.id
,或是 R.id
重複。這可能會導致正常的檢測設備測試作業變得複雜又複雜,因為透過 findViewById()
存取檢視畫面的正常方式無法運作。因此,您可能需要存取含有該檢視畫面的 Activity 或 Fragment 私人成員,或是尋找具有已知 R.id
的容器,然後前往特定檢視畫面的內容。
Espresso 可讓您使用現有或自己的自訂 ViewMatcher
物件來縮小檢視畫面範圍,妥善處理這個問題。
使用 R.id
尋找檢視畫面,就像呼叫 onView()
一樣簡單:
Kotlin
onView(withId(R.id.my_view))
Java
onView(withId(R.id.my_view));
有時候,多個檢視畫面會共用 R.id
值。當嘗試使用特定 R.id
時,您會收到例外狀況,例如 AmbiguousViewMatcherException
。例外狀況訊息會以文字表示目前的檢視區塊階層,方便您搜尋並找出符合非不重複 R.id
的檢視畫面:
java.lang.RuntimeException: androidx.test.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****
查看檢視畫面的各種屬性,您將能找到可明確識別的屬性。在上述範例中,其中一個檢視畫面具有 "Hello!"
文字。您可使用組合比對條件來縮小搜尋範圍:
Kotlin
onView(allOf(withId(R.id.my_view), withText("Hello!")))
Java
onView(allOf(withId(R.id.my_view), withText("Hello!")));
您也可以選擇不反轉任何比對器:
Kotlin
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
Java
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));
如要瞭解 Espresso 提供的檢視畫面比對器,請參閱 ViewMatchers
。
考量重點
- 在功能完善的應用程式中,使用者可以進行互動的所有檢視畫面都應包含說明文字或含有內容說明。詳情請參閱「讓應用程式更容易使用」。如果您無法使用
withText()
或withContentDescription()
縮小搜尋範圍,請考慮將其視為無障礙功能錯誤。 - 使用最描述性的比對器來尋找所需資料檢視。請勿過度指定,因為這樣會強制架構執行不必要的工作。例如,如果檢視區塊的文字可明確識別該檢視區塊,您不必指定檢視區塊也可從
TextView
指派。對於許多檢視畫面,R.id
的檢視畫面應已足夠。 - 如果目標檢視畫面位於
AdapterView
中 (例如ListView
、GridView
或Spinner
),onView()
方法可能無法運作。在這種情況下,您應該改用onData()
。
對檢視畫面執行動作
當您找到適用於目標檢視畫面的比對器時,可以使用執行方法在其上執行 ViewAction
例項。
舉例來說,如要點選檢視畫面:
Kotlin
onView(...).perform(click())
Java
onView(...).perform(click());
您可以一次執行多個動作:
Kotlin
onView(...).perform(typeText("Hello"), click())
Java
onView(...).perform(typeText("Hello"), click());
如果您使用的檢視畫面位於 ScrollView
(垂直或水平),請考慮使用 scrollTo()
顯示需要顯示檢視畫面的動作,例如 click()
和 typeText()
。這樣可確保先顯示檢視畫面,再繼續其他動作:
Kotlin
onView(...).perform(scrollTo(), click())
Java
onView(...).perform(scrollTo(), click());
如要瞭解 Espresso 提供的檢視動作,請參閱 ViewActions
。
查看檢視畫面宣告
您可以使用 check()
方法,將斷言套用至目前選取的檢視畫面。最常用的斷言是 matches()
斷言。此方法會使用 ViewMatcher
物件宣告目前所選檢視區塊的狀態。
舉例來說,如要檢查檢視畫面是否含有 "Hello!"
文字:
Kotlin
onView(...).check(matches(withText("Hello!")))
Java
onView(...).check(matches(withText("Hello!")));
如要宣告 "Hello!"
是檢視畫面的內容,系統會將以下行為視為不當做法:
Kotlin
// Don't use assertions like withText inside onView. onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))
Java
// Don't use assertions like withText inside onView. onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
另一方面,如果您想宣告有 "Hello!"
文字的檢視區塊 (例如在變更檢視畫面瀏覽權限標記後顯示),程式碼就沒問題。
查看斷言簡易測試
在這個範例中,SimpleActivity
包含 Button
和 TextView
。當使用者點選按鈕時,TextView
的內容會變更為 "Hello Espresso!"
。
使用 Espresso 進行測試的方法如下:
按一下頂端按鈕
首先,請找出有助於找到按鈕的屬性。SimpleActivity
中的按鈕具有專屬的 R.id
,如預期相同。
Kotlin
onView(withId(R.id.button_simple))
Java
onView(withId(R.id.button_simple));
現在我要執行點擊:
Kotlin
onView(withId(R.id.button_simple)).perform(click())
Java
onView(withId(R.id.button_simple)).perform(click());
驗證 TextView 文字
含有要驗證文字的 TextView
也具有專屬的 R.id
:
Kotlin
onView(withId(R.id.text_simple))
Java
onView(withId(R.id.text_simple));
現在要驗證內容文字:
Kotlin
onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))
Java
onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));
在轉接程式檢視畫面中檢查載入資料
AdapterView
是一種特殊類型的小工具,可從 Adapter 動態載入其資料。ListView
是最常見的 AdapterView
範例。與 LinearLayout
等靜態小工具相反,目前的檢視區塊階層只能載入 AdapterView
子項子集。簡單的 onView()
搜尋會找不到目前未載入的檢視畫面。
Espresso 透過提供獨立的 onData()
進入點來處理這種情況,可先載入有問題的轉接程式項目,並在用於其或其任何子項之前將其移至焦點。
警告:如果 AdapterView
的自訂實作會破壞繼承合約 (尤其是 getItem()
API),就可能無法順利使用 onData()
方法。在這種情況下,最好的做法就是重構應用程式程式碼。如果無法執行,您可以實作相符的自訂 AdapterViewProtocol
。詳情請參閱 Espresso 提供的預設
AdapterViewProtocols
類別。
轉接程式檢視畫面簡易測試
這個簡單的測試示範如何使用 onData()
。SimpleActivity
包含 Spinner
,內含幾個代表咖啡飲品類型的項目。選取項目後,TextView
就會變更為 "One %s a day!"
,其中 %s
代表所選項目。
這項測試的目標是開啟 Spinner
並選取特定項目,然後驗證 TextView
是否包含該商品。由於 Spinner
類別是以 AdapterView
為基礎,建議使用 onData()
而非 onView()
來比對項目。
開啟所選項目
Kotlin
onView(withId(R.id.spinner_simple)).perform(click())
Java
onView(withId(R.id.spinner_simple)).perform(click());
選取項目
針對項目選取項目,Spinner
會建立含有其內容的 ListView
。這個檢視畫面可能會很長,且該元素可能無法影響檢視區塊階層。使用 onData()
時,系統會強制要求所需的元素進入檢視區塊階層。Spinner
中的項目是字串,因此我們希望比對等於字串 "Americano"
的項目:
Kotlin
onData(allOf(`is`(instanceOf(String::class.java)), `is`("Americano"))).perform(click())
Java
onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
確認文字正確
Kotlin
onView(withId(R.id.spinnertext_simple)) .check(matches(withText(containsString("Americano"))))
Java
onView(withId(R.id.spinnertext_simple)) .check(matches(withText(containsString("Americano"))));
偵錯
Espresso 會在測試失敗時提供實用的偵錯資訊:
記錄
Espresso 會將所有檢視動作記錄至 Logcat。例如:
ViewInteraction: Performing 'single click' action on view with text: Espresso
檢視區塊階層
在 onView()
失敗時,Espresso 會在例外狀況訊息中顯示檢視區塊階層。
- 如果
onView()
找不到目標檢視畫面,就會擲回NoMatchingViewException
。您可以查看例外狀況字串中的檢視區塊階層,分析比對器與任何檢視畫面不相符的原因。 - 如果
onView()
找到多個符合指定比對器的檢視畫面,就會擲回AmbiguousViewMatcherException
。系統會列印檢視區塊階層,且所有符合的檢視畫面都會加上MATCHES
標籤:
java.lang.RuntimeException: androidx.test.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****
處理複雜的檢視區塊階層,或小工具非預期的行為時,使用 Android Studio 中的階層檢視器一直都有幫助。
轉接程式檢視畫面警告
Espresso 會在出現 AdapterView
小工具時警告使用者。當 onView()
運算擲回 NoMatchingViewException
和 AdapterView
小工具出現在檢視區塊階層時,最常見的解決方案是使用 onData()
。例外狀況訊息會包含警告訊息,當中列出轉接程式檢視畫面。您可以使用這項資訊叫用 onData()
來載入目標檢視畫面。
其他資源
如要進一步瞭解如何在 Android 測試中使用 Espresso,請參閱下列資源。
範例
- CustomMatcherSample:說明如何擴充 Espresso,以符合
EditText
物件的提示屬性。 - RecyclerViewSample:Espresso 的
RecyclerView
動作。 - (更多...)