在設計Mobile UI的時候免不了有一些需要客制化的共用元件,可能是你自己寫的或是引用3rd party library(如果你的Application只有用到Android原生元件那就可以跳過這一節),而這些UI元件我們如果想用Espresso去做測試的時候常常會遇到難題,例如元件找不到或動作無法執行,這是因為View Hierachy或是預設的ViewAssertion與ViewAction只能應付Android原生元件的操作。如果遇到這種狀況也有方法來解決。
舉例有一個CustomWidget的類別繼承LinearLayout來實作。包含一個TextView,一個EditText以及一個Save Button,要做的事就是把EditText的input透過Save Button的ClickListener放到TextView(placeholder)中去顯示。
CustomWidget程式碼
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class CustomWidget @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {
init {
//inflate layout
LayoutInflater.from(context).inflate(R.layout.custom_widget, this, true)
save.setOnClickListener {
textView.text = editText.text
}
}
}
CustomWidget layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/wrapper"
android:orientation="horizontal" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
android:layout_width="100dp"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/editText"
android:layout_width="100dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="item"
android:textSize="24dp" />
<Button
android:id="@+id/save"
android:text="Save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
在MainActivity內插入CustomWidget
<com.daniel.demotest.CustomWidget
android:id="@+id/customWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
我們來測試CustomWidget的功能,因為我們是放在MainActivity裡,所以一樣launch MainActivity來做測試。
我們要測試CustomWidget的Save功能是否正常,所以我們要在EditText裡輸入一個字串"testString",然後按Save觸發onClickListener後應該要在TextView裡看到"testString"這個字串。
class ExampleInstrumentedTest {
@get:Rule
val activityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun testWidget() {
val text = "testString"
onView(withId(R.id.editText)).perform(replaceText(text))
onView(withId(R.id.save)).perform(click())
onView(withId(R.id.textView)).check(matches(withText(text)))
}
}
看似很直覺的測試結果程式一執行就出錯了,我們來看看怎麼一回事。
照理說我們有CustomWidget的layout檔也知道裡面元件的id應該用onView(withId("R.id.save")).perform(click())就應該要按到CustomWidget裡的按鈕了,但是確會出錯。
Caused by: java.lang.RuntimeException: Action will not be performed because the target view does not match one or more of the following constraints:
at least 90 percent of the view's area is displayed to the user.
不管你怎麼把save button改變UI,Espresso就是會跟你說上述錯誤,所以這時就只好也用客制化的ViewAction直接操作CustomWidget內部元件如下。我們建立一個新的Class CustomViewAction繼承ViewAction後,有下列三個functions需要override
class CustomViewAction(private val text: String) : ViewAction {
override fun getDescription(): String {
return "CustomViewAction applied $text"
}
override fun getConstraints(): Matcher<View> {
return isDisplayed()
}
override fun perform(uiController: UiController?, view: View?) {
val editText = view!!.findViewById<EditText>(R.id.editText)
val save = view.findViewById<Button>(R.id.save)
editText.text.clear()
editText.text.append(text)
save.callOnClick()
}
}
下一步要測試CustomWidget的TextView是否真的有收到字串,我們就用onView(withId(R.id.textView)).check(matches(withText(text))),結執行這行也出錯了,
androidx.test.espresso.AmbiguousViewMatcherException: 'with id: com.daniel.demotest:id/textView' matches multiple views in the hierarchy.
因為MainActivity裡我們己經有一個元件叫textView了,Espresso不知道要抓哪一個textView來判斷,這時候有一個解法就是你把其中一個id叫textView的改掉,但這樣做不好,難不成為了測試我們每個View的ID都要取不重複的名稱。因此我們可以用實作BoundedMatcher介面的方法來做判斷,這裡我們建立一個customTextViewMatcher來實作BoundedMatcher,這裡必須override兩個functions
private fun customTextViewMatcher(text: String) : BoundedMatcher<View, CustomWidget> {
Checks.checkNotNull(text)
return object : BoundedMatcher<View, CustomWidget>(CustomWidget::class.java) {
override fun describeTo(description: Description?) {
}
override fun matchesSafely(view: CustomWidget?): Boolean {
val textView = view?.findViewById<TextView>(R.id.textView)
return text == textView?.text.toString()
}
}
}
最後我們在testWidget()改成先對CustomWidget perform我們客制化的CustomViewAction後再用客制化的customTextViewMatcher去判斷就完成了對客制化元件CustomWidget測試
class ExampleInstrumentedTest {
@get:Rule
val activityTestRule = ActivityTestRule(MainActivity::class.java)
@Test
fun testWidget() {
val text = "testString"
onView(withId(R.id.customWidget))
.perform(CustomViewAction(text))
onView(withId(R.id.customWidget))
.check(matches(customTextViewMatcher(text)))
}
}
在這一章節我們用很基本的範例來示範Custom Widget的測試方式,大致上就是用parent的layouot去抓取child view的細節來進一步的操作其實很簡單,下一節我們來講講如果Application有很多的Server連線要怎麼在Integration test做處理。