【Android】SDKバージョンごとのUNITテストを書く

#はじめに
Androidを開発しているとSDKバージョン（OSバージョン）によって処理を分岐させる必要が出てきます。
そういった処理はネット上に多く見つけることができます。
その場合のテストもしっかり考えるべきと思います。
今回はSDKバージョン別のUNITテストを紹介していきます。
#OSごとに分岐が必要な処理例
早速、SDKバージョンごとに分岐させるような実装を考えてみます。
テストを書くことが目的なので、インスタンス化するパターンと静的(companionObject、static)なパターン2つを用意します（処理は同じにします）
今回は実際に判定が処理の例としてネットワーク判定処理を実装します。trueでWIFI
に接続されている判定になる処理です。
判定を返す処理をしますが、その際、AndroidM(SDKバージョン23以上)と未満で使用するAPIを変えます。

詳しくは公式サイトを見てください。

https://developer.android.com/reference/android/net/ConnectivityManager

```kotlin:Connectivity.kt
/**
 * インスタンス化するパターン.
 * @param context コンストラクタでContextを渡す
 */
class Connectivity(val context: Context) {

    fun isWiFiConnected(): Boolean {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            isWiFiConnected(connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork))
        } else {
            isWiFiConnected(connectivityManager.activeNetworkInfo.type)
        }
    }

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    private fun isWiFiConnected(networkCapabilities: NetworkCapabilities?): Boolean =
            networkCapabilities != null && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)

    private fun isWiFiConnected(activeNetworkInfoType: Int): Boolean =
            activeNetworkInfoType == ConnectivityManager.TYPE_WIFI
}
```

```kotlin:ConnectivityUtil.kt
/**
 * 静的(companionObject、static)なパターン.
 * インスタンス化せずメソッドでContextを渡す
 */
object ConnectivityUtil {

    fun isWiFiConnected(context: Context): Boolean {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            isWiFiConnected(connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork))
        } else {
            isWiFiConnected(connectivityManager.activeNetworkInfo.type)
        }
    }

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    private fun isWiFiConnected(networkCapabilities: NetworkCapabilities?): Boolean =
        networkCapabilities != null && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)

    private fun isWiFiConnected(activeNetworkInfoType: Int): Boolean =
        activeNetworkInfoType == ConnectivityManager.TYPE_WIFI
}
```

これは分岐処理の1例ですが、AndroidはこういったSDKバージョンごとの分岐処理が必要な場合があります。

#UNITテストを書く準備
テストの為にgradleに定義を追加します。
今回のテストには以下の定義が必要です。

```gradle:build.gradle
dependencies {
    // ↓を追加(最新のバージョンにしてください)
    testImplementation 'junit:junit:4.13'
    testImplementation 'androidx.test:core:1.3.0'
    testImplementation 'org.robolectric:robolectric:4.3.1'
    testImplementation 'io.mockk:mockk:1.10.5'
}
```

#シンプルにUNITテストを書く
まずはSDKバージョンごとに分岐しないシンプルなテストを実装してみましょう。
テストクラスはそれぞれ ConnectivityTest、ConnectivityUtilTest とします。

```kotlin:ConnectivityTest.kt
@RunWith(RobolectricTestRunner::class)
class ConnectivityTest {

    private lateinit var context: Context

    @Before
    fun setup() {
        // これでContextが取得できる
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun シンプルなテスト() {
        val expected = false
        val connectivity = mockk<Connectivity>()

        every { connectivity.isWiFiConnected() } returns expected
        val result = connectivity.isWiFiConnected()
        Assert.assertEquals(expected, result)
    }
}
```

```kotlin:ConnectivityUtilTest.kt
@RunWith(RobolectricTestRunner::class)
class ConnectivityUtilTest : TestCase() {

    private lateinit var context: Context

    //private lateinit var contextMock: Context

    @Before
    fun setup() {
        // これでContextが取得できる
        context = ApplicationProvider.getApplicationContext()

        // Context取得方法はいくつかあります
        //contextMock = mockk<Context>(relaxed = true)
    }

    @Test
    fun companionObjectをmockするシンプルなテスト() {
        val expected = false
        mockkObject(ConnectivityUtil)
        every { ConnectivityUtil.isWiFiConnected(any()) } returns expected
        val result = ConnectivityUtil.isWiFiConnected(context)
        Assert.assertEquals(expected, result)
        unmockkObject(ConnectivityUtil)
    }
}
```

これでテストは通ります。

#SDKバージョンごとのUNITテストを書く
次にSDKバージョンごとのUNITテストを追加しましょう。
やりたいテストはSDKバージョンによって利用されるprivateメソッドが違うことを担保するテストです。

```kotlin:ConnectivityTest.kt
@RunWith(RobolectricTestRunner::class)
class ConnectivityTest {

    private lateinit var context: Context

    @Before
    fun setup() {
        // これでContextが取得できる
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun シンプルなテスト() {
        val expected = false
        val connectivity = mockk<Connectivity>()

        every { connectivity.isWiFiConnected() } returns expected
        val result = connectivity.isWiFiConnected()
        Assert.assertEquals(expected, result)
    }


    @Test
    fun SDK19の分岐テスト() {
        // SDK19にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 19)
        // コンストラクタにContextを渡してインスタンス化
        // privateメソッドの呼び出しを有効
        val connectivity = spyk(Connectivity(context), recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivity["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        connectivity.isWiFiConnected()

        // AndroidM以上の分岐に入らない
        verify(exactly = 0) { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入る
        verify(exactly = 1) { connectivity["isWiFiConnected"](allAny<Int>()) }
    }

    @Test
    fun SDK27の分岐テスト() {
        // SDK27にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 27)
        // コンストラクタにContextを渡してインスタンス化
        // privateメソッドの呼び出しを有効
        val connectivity = spyk(Connectivity(context), recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivity["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        connectivity.isWiFiConnected()

        // AndroidM以上の分岐に入る
        verify(exactly = 1) { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入らない
        verify(exactly = 0) { connectivity["isWiFiConnected"](allAny<Int>()) }
    }

    @Test
    fun SDK27の分岐テスト_2回実行() {
        // SDK27にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 27)
        // privateメソッドの呼び出しを有効
        val connectivity = spyk(Connectivity(context), recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivity["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        connectivity.isWiFiConnected()

        // AndroidM以上の分岐に2回入る
        verify(exactly = 2) { connectivity["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入らない
        verify(exactly = 0) { connectivity["isWiFiConnected"](allAny<Int>()) }
    }
}
```

```kotlin:ConnectivityUtilTest.kt
@RunWith(RobolectricTestRunner::class)
class ConnectivityUtilTest : TestCase() {

    private lateinit var context: Context

    //private lateinit var contextMock: Context

    @Before
    fun setup() {
        // これでContextが取得できる
        context = ApplicationProvider.getApplicationContext()

        // Context取得方法はいくつかあります
        //contextMock = mockk<Context>(relaxed = true)
    }

    @After
    fun teardown() {
    }

    @Test
    fun companionObjectをmockするシンプルなテスト() {
        val expected = false
        mockkObject(ConnectivityUtil)
        every { ConnectivityUtil.isWiFiConnected(any()) } returns expected
        val result = ConnectivityUtil.isWiFiConnected(context)
        Assert.assertEquals(expected, result)
        unmockkObject(ConnectivityUtil)
    }

    @Test
    fun SDK19の分岐テスト() {
        // SDK19にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 19)
        // privateメソッドの呼び出しを有効
        val connectivityUtil = spyk<ConnectivityUtil>(recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivityUtil["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        connectivityUtil.isWiFiConnected(context)

        // AndroidM以上の分岐に入らない
        verify(exactly = 0) { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入る
        verify(exactly = 1) { connectivityUtil["isWiFiConnected"](allAny<Int>()) }
    }

    @Test
    fun SDK27の分岐テスト() {
        // SDK27にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 27)
        // privateメソッドの呼び出しを有効
        val connectivityUtil = spyk<ConnectivityUtil>(recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivityUtil["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        connectivityUtil.isWiFiConnected(context)

        // AndroidM以上の分岐に入る
        verify(exactly = 1) { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入らない
        verify(exactly = 0) { connectivityUtil["isWiFiConnected"](allAny<Int>()) }
    }

    @Test
    fun SDK27の分岐テスト_2回実行() {
        // SDK27にセット
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 27)
        // privateメソッドの呼び出しを有効
        val connectivityUtil = spyk<ConnectivityUtil>(recordPrivateCalls = true)

        // それぞれprivateメソッドが呼ばれた際に何を返すか定義
        every { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) } returns mockk<Boolean>(relaxed = true)
        every { connectivityUtil["isWiFiConnected"](allAny<Int>()) } returns mockk<Boolean>(relaxed = true)

        // 2回実行する
        connectivityUtil.isWiFiConnected(context)
        connectivityUtil.isWiFiConnected(context)

        // AndroidM以上の分岐に2回入る
        verify(exactly = 2) { connectivityUtil["isWiFiConnected"](allAny<NetworkCapabilities>()) }
        // AndroidM未満の分岐に入らない
        verify(exactly = 0) { connectivityUtil["isWiFiConnected"](allAny<Int>()) }
    }
}
```

これでSDKによる分岐処理も網羅できました。

#最後に
応用すれば様々なSDKバージョンの分岐処理にも対応できると思います。
みんなで不具合の少ないアプリを作っていきましょう。

https://qiita.com/HanaleiMoon/items/dab555b45b7f7385f7e8