オープンソースのUIライブラリを使っているとFragmentのユニットテストやGUIテストを書けない場合があります。今回は例として下記のUIライブラリのテストコードを書けるようにします。

https://github.com/bitvale/Switcher

このライブラリはマテリアルデザインのSwitchを置き換える目的で使っていたのですが、UIテストがうまく動きませんでした。動かなかったテストコードは以下になります。

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import java.util.*

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SwitcherFragmentSpec {
    @Before
    fun setUp() {
        // 必要な初期化処理
    }

    private fun launchFragment() {
        launchFragmentInContainer<SwitcherFragment>(themeResId = R.style.Theme_MaterialComponents_Light)
    }

    @Test
    fun onResume_shouldShowParameters() {
        launchFragment()
        // SwitcherXがチェックされているか
        onView(withId(R.id.switcherx)).check(matches(isChecked()))
        // SwitcherXがチェックされていないか
        onView(withId(R.id.switcherx)).check(matches(isNotChecked()))
    }

    @After
    fun tearDown() {
        // 必要な終了処理
    }
}

一見するとこのコードは動きそうですが、SwitcherXがチェックされていてもmatches(isChecked())で失敗するか、SwitcherXのチェックがされていなくてもmatches(isNotChecked())で失敗します。

これはそもそもライブラリがCheckBoxやSwitchを継承しているのではなく、Viewを継承しているために発生しています。

https://github.com/bitvale/Switcher/blob/568ea7a4153b3d2e530ee64d7e5fb71c3ea1189d/library/src/main/java/com/bitvale/switcher/Switcher.kt#L30

つまりmatches(isChecked())やmatches(isNotChecked())はCheckboxやSwitchを継承したViewでのみ動作するのですが、SwitcherXはViewを継承しているので正しく動作しないです。こういうときはどうするかというとEspresso用にMatcherを用意することで正しくテストコードを動作させることができます。

import android.view.View
import androidx.test.espresso.matcher.BoundedMatcher
import com.bitvale.switcher.Switcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.core.Is.`is`

object SwitcherMatcher {
    fun isChecked(checked: Boolean): IsCheckedMatcher =
            IsCheckedMatcher(`is`(checked))

    class IsCheckedMatcher(private val matcher: Matcher<Boolean>) :
            BoundedMatcher<View, Switcher>(Switcher::class.java) {
        override fun describeTo(description: Description?) {
            description?.appendText("is checked: ")
        }

        override fun matchesSafely(view: Switcher?): Boolean =
                matcher.matches(view?.isChecked)
    }
}

このようにBoundedMatcherを継承したIsCheckedMatcherクラスを作成し、view?.isCheckedでチェックされているかのマッチャーを作成しています。

テストコードで使用する場合は下記のようにします。

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import java.util.*

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SwitcherFragmentSpec {
    @Before
    fun setUp() {
        // 必要な初期化処理
    }
    
    private fun launchFragment() {
        launchFragmentInContainer<SwitcherFragment>(themeResId = R.style.Theme_MaterialComponents_Light)
    }

    @Test
    fun onResume_shouldShowParameters() {
        launchFragment()
        // SwitcherXがチェックされているか
        onView(withId(R.id.switcherx)).check(matches(SwitcherMatcher.isChecked(true)))
        // SwitcherXがチェックされていないか
        onView(withId(R.id.switcherx)).check(matches(SwitcherMatcher.isChecked(false)))
    }

    @After
    fun tearDown() {
        // 必要な終了処理
    }
}

これでUIのテストができるようになります。UIライブラリにゲッターメソッドがあれば独自のMatcherを作れるので他のUIライブラリでも同じようにMatcherを作成しテストコードが書けると思います。