はじめに
Androidを開発しているとSDKバージョン(OSバージョン)によって処理を分岐させる必要が出てきます。
そういった処理はネット上に多く見つけることができます。
その場合のテストもしっかり考えるべきと思います。
今回はSDKバージョン別のUNITテストを紹介していきます。
OSごとに分岐が必要な処理例
早速、SDKバージョンごとに分岐させるような実装を考えてみます。
テストを書くことが目的なので、インスタンス化するパターンと静的(companionObject、static)なパターン2つを用意します(処理は同じにします)
今回は実際に判定が処理の例としてネットワーク判定処理を実装します。trueでWIFI
に接続されている判定になる処理です。
判定を返す処理をしますが、その際、AndroidM(SDKバージョン23以上)と未満で使用するAPIを変えます。
詳しくは公式サイトを見てください。
/**
* インスタンス化するパターン.
* @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
}
/**
* 静的(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に定義を追加します。
今回のテストには以下の定義が必要です。
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 とします。
@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)
}
}
@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メソッドが違うことを担保するテストです。
@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>()) }
}
}
@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バージョンの分岐処理にも対応できると思います。
みんなで不具合の少ないアプリを作っていきましょう。