Espressoで下タブメニューのタップ操作やWebViewコンテンツの検証を行う
本章では、Espressoを用いて、下タブメニューのタップ方法とWebViewコンテンツの検証方法について共有します。
最新の記事はこちら kikki.hatenablog.com
背景
前回までのコードでは、Androidでメニュー操作ができない問題が発生しました。
kikki.hatenablog.com
Espressoで、下タブメニューであるBottomNavigationをタップ操作を行おうと試みた際に、Viewが見えずタップ操作ができなかったためです。
そこで解決方法として、Viewの表示確認で見えていると偽装して、タップ操作を行うように対応しました。
また新たに、WebViewのコンテンツの検証も行いたく、EspressoのWebのAssertionも採用しました。
コード
過去のコードにBottomNavigationを操作できるView Action「NoCheckedViewAction」を定義し、関数「onNoCheckedClick」からActionを参照するよう変更しています。
また、関数「onAssertWebText」にて、WebViewのコンテンツに指定した文言が含まれているかの検証を行えます。
まずは、ViewActionです。
[NoCheckedViewAction.kt]
import android.view.View import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.matcher.ViewMatchers import org.hamcrest.Matcher // Viewが見えない要素をクリック class NoCheckedViewAction : ViewAction { override fun getConstraints(): Matcher<View> { return ViewMatchers.isEnabled() } override fun getDescription(): String { return "click button" } override fun perform(uiController: UiController?, view: View?) { view!!.performClick() } }
次に、Espressoで参照するUtilityの関数です。
[EspressoUtil]
import android.view.View import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.DataInteraction import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.Locator import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.Matcher import org.hamcrest.Matchers.* private const val shortTimeout: Long = 3000 private const val longTimeout: Long = 7000 // 特定の文言を含む要素があるかを確認 fun existsViewInteraction(text: String): Boolean { try { Thread.sleep(shortTimeout) Espresso.onView(withText(text)).check(matches(isDisplayed())) } catch (e: NoMatchingViewException) { return false } return true } // 特定の文言を含む要素を検証 fun onAssertText(id: Int? = null, text: String, sleepTime:Long= shortTimeout){ Thread.sleep(sleepTime) val matcher = id?.let { allOf(withId(id), withText(text)) } ?: withText(text) Espresso.onView(matcher) .check(matches(isDisplayed())) } // 特定の文言を含むWebページを検証 fun onAssertWebText(locator: Locator, locatorValue: String, webText:String, sleepTime: Long = shortTimeout){ Thread.sleep(sleepTime) Web.onWebView() .withElement(findElement(locator, locatorValue)) .check(webMatches(getText(), containsString(webText))) } // 特定の要素の表示を待機 fun onWaitDisplaying(id: Int, text: String) { wait({ findViewInteraction(allOf(withId(id), withText(text))) }) } // 特定の要素の有効化を待機 fun onWaitEnableAndClickable(id: Int) { wait({ findViewInteraction(allOf(withId(id), isClickable(), isEnabled())) }) } // 特定の要素までスクロール fun onScrollTo(id: Int, text: String? = null) { val matcher = text?.let { allOf(withId(id), withText(text)) } ?: withId(id) wait({ findViewInteraction(matcher) }, ViewActions.scrollTo()) } // 特定の要素をクリック fun onClick(id: Int, text: String? = null) { val matcher = text?.let { allOf(withId(id), withText(text), isCompletelyDisplayed()) } ?: allOf(withId(id), isCompletelyDisplayed()) wait({ findViewInteraction(matcher) }, ViewActions.click()) } // 特定の文言を含む要素をクリック fun onClick(text: String) { val matcher = withText(text) wait({ findViewInteraction(matcher) }, ViewActions.click()) } // 表示状態に関わらず、特定の要素をクリック fun onNoCheckedClick(id: Int, text: String? = null) { val matcher = text?.let { allOf(withId(id), withText(text)) } ?: withId(id) wait({ findViewInteraction(matcher) }, NoCheckedViewAction()) } // 文言を書き換え fun onReplaceText(id: Int, text: String) { wait({ findViewInteraction(withId(id)) }, ViewActions.replaceText(text)) } // Spinnerの特定の要素を選択 fun onSelectSpinnerText(text: String, position: Int) { wait({ findDataInteraction(`is`(text), position) }, ViewActions.click()) } // Adapterの特定の要素を選択 fun onSelectAdapter(id: Int, position: Int) { wait({ findDataInteractionInAdapter(withId(id), position) }, ViewActions.click()) } // RecyclerViewの特定の要素を選択 fun onSelectRecyclerPosition(id: Int, position: Int) { wait({ findViewInteraction(withId(id)) }, actionOnItemAtPosition<RecyclerView.ViewHolder>(position, ViewActions.click())) } // Viewを取得 fun findViewInteraction(viewMatcher: Matcher<View>): ViewInteraction? { return try { Espresso.onView(viewMatcher) .check(matches(isEnabled())) } catch (e: NoMatchingViewException) { null } } // ListViewの特定要素を取得 fun <T> findDataInteraction(viewMatcher: Matcher<T>, position: Int): DataInteraction? { return try { Espresso.onData(viewMatcher) .atPosition(position) } catch (e: NoMatchingViewException) { null } } // Adapter内の特定のView要素を取得 fun findDataInteractionInAdapter(viewMatcher: Matcher<View>, position: Int): DataInteraction? { return try { Espresso.onData(anything()) .inAdapterView(viewMatcher) .atPosition(position) } catch (e: NoMatchingViewException) { null } } // アプリ外の要素をクリック fun onClickUiSelector(device: UiDevice, vararg ids: String): Boolean { var exists = false for (id in ids) { val obj = device.findObject(UiSelector().resourceId(id)) if (obj.waitForExists(longTimeout)) { exists = true obj.click() } } return exists } // バックキーをクリック fun onBack(device: UiDevice){ device.pressBack() }
筆休め
Androidでも、メニューが下タブ形式のアプリが増えてきました。網羅的なテスト実現のため、検証できる項目を増やせるといいですね。
以上、「Espressoで下タブメニューのタップ操作やWebViewコンテンツの検証を行う」でした。