LoginSignup
2
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-30

はじめに

Androidを開発しているとSDKバージョン(OSバージョン)によって処理を分岐させる必要が出てきます。
そういった処理はネット上に多く見つけることができます。
その場合のテストもしっかり考えるべきと思います。
今回はSDKバージョン別のUNITテストを紹介していきます。

OSごとに分岐が必要な処理例

早速、SDKバージョンごとに分岐させるような実装を考えてみます。
テストを書くことが目的なので、インスタンス化するパターンと静的(companionObject、static)なパターン2つを用意します(処理は同じにします)
今回は実際に判定が処理の例としてネットワーク判定処理を実装します。trueでWIFI
に接続されている判定になる処理です。
判定を返す処理をしますが、その際、AndroidM(SDKバージョン23以上)と未満で使用するAPIを変えます。

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

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
}
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に定義を追加します。
今回のテストには以下の定義が必要です。

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 とします。

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)
    }
}
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 companionObjectmockするシンプルなテスト() {
        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メソッドが違うことを担保するテストです。

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>()) }
    }
}
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 companionObjectmockするシンプルなテスト() {
        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バージョンの分岐処理にも対応できると思います。
みんなで不具合の少ないアプリを作っていきましょう。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2