Mockito や Shadow を使ってテストコードを書くのは楽しい
Kotlin でコード書くのはとても楽しいですし、Mockito や Shadow を使ってテストコードを書くのも楽しいですね。業務で携わっているコードにはテストコードがあまり用意されていないものもありますので、このように自分で書いてみるのはとても有意義です。しばらく使わないとすぐに忘れてしまうでしょうから、今回のテストコードを書く際に使ったものたちについて簡単にメモしておこうと思います。
テストコードにパーミッションを与える
今回のテストの対象としたクラスでは、指定した SIM カードスロットに対応するアクティブな SubscriptionInfo を取得するために、SubscriptionManager の getActiveSubscriptionInfoForSimSlotIndex() を呼んでいます。API の実行には READ_PHONE_STATE パーミッションが必要になるので、テストコードには GrantPermissionRule を使って同パーミッションを与えました。
@Rule @JvmField val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.READ_PHONE_STATE)
Kotlin コード上で機能させるために、@JvmField アノテーションを付けています。
ShadowSubscriptionManager に SubscriptionInfo のモックを適用する
Android Studio 上でテストコードを実行する際、当然ながらアクティブな SubscriptionInfo はありません。それがある状態を模するために、SubscriptionManager の Shadow (ShadowSubscriptionManager) と Mockito のモックを併用しました。どっちも使ってみたかったんです。
private val smShadow = shadowOf(context.getSystemService( Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager) @Mock private lateinit var subInfoMock: SubscriptionInfo @Before fun setUp() { MockitoAnnotations.initMocks(this) // Associate the subscription info #TEST_SUBS_ID with the slot #TEST_SLOT_ID. `when`(subInfoMock.simSlotIndex).thenReturn(TEST_SLOT_ID) `when`(subInfoMock.subscriptionId).thenReturn(TEST_SUBS_ID) smShadow.setActiveSubscriptionInfos(subInfoMock) ...
これにより、SIM カードスロット #TEST_SUBS_ID に紐づくアクティブな SubscriptionInfo を取得できるようになりました。
ShadowTelephonyManager に TelephonyManager のモックを適用する
Shadow を使わずに全部 Mockito で出来たんじゃないかと思ったりもしますが、いいんです。Shadow も使ってみたかったんです。
private val tmShadow = shadowOf(context.getSystemService( Context.TELEPHONY_SERVICE) as TelephonyManager) @Mock private lateinit var tmMock: TelephonyManager @Before fun setUp() { MockitoAnnotations.initMocks(this) tmShadow.setTelephonyManagerForSubscriptionId(TEST_SUBS_ID, tmMock) ...
これで、#TEST_SUBS_ID 用の TelephonyManager をモックに差し替えることができました。
TelephonyManager のモックに適当な応答を返させる
下記コード例の setUp() では、AID1 または AID2 を指定して論理チャネルを開こうとした場合に、STATUS_NO_ERROR を応答させるようにモックを設定しています。
@Mock private lateinit var respMock: IccOpenLogicalChannelResponse @Before fun setUp() { MockitoAnnotations.initMocks(this) ... // Create the interface for the slot #TEST_SLOT_ID. tif = TelephonyInterface.from(context, TEST_SLOT_ID) // By default, both AID1 and AID2 are accessible. `when`(respMock.channel).thenReturn(TEST_CHANNEL_ID) `when`(respMock.status).thenReturn(IccOpenLogicalChannelResponse.STATUS_NO_ERROR) `when`(tmMock.iccOpenLogicalChannel(AID1_STRING, TEST_OPEN_P2)).thenReturn(respMock) `when`(tmMock.iccOpenLogicalChannel(AID2_STRING, TEST_OPEN_P2)).thenReturn(respMock) } @Test fun openClose_logicalChannel_share() { assertThat(tif.openChannel(AID1)).isEqualTo(Interface.OpenChannelResult.SUCCESS) assertThat(tif.openChannel(AID1)).isEqualTo(Interface.OpenChannelResult.SUCCESS) tif.closeRemainingChannel() verify(tmMock, times(1)).iccOpenLogicalChannel(AID1_STRING, TEST_OPEN_P2) verify(tmMock, times(1)).iccCloseLogicalChannel(TEST_CHANNEL_ID) }
このテストケースでは、iccOpenLogicalChannel() と iccCloseLogicalChannel() が一度ずつ呼び出されていたかどうかをチェックしています。