kikki's tech note

技術ブログです。UnityやSpine、MS、Javaなど技術色々について解説しています。

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コンテンツの検証を行う」でした。


※無断転載禁止 Copyright (C) kikkisnrdec All Rights Reserved.