クマは森で用を足しますか?

アウトプットは重要です。

Mockito や Shadow を使ってテストコードを書くのは楽しい

Kotlin でコード書くのはとても楽しいですし、Mockito や Shadow を使ってテストコードを書くのも楽しいですね。業務で携わっているコードにはテストコードがあまり用意されていないものもありますので、このように自分で書いてみるのはとても有意義です。しばらく使わないとすぐに忘れてしまうでしょうから、今回のテストコードを書く際に使ったものたちについて簡単にメモしておこうと思います。

github.com

テストコードにパーミッションを与える

今回のテストの対象としたクラスでは、指定した SIM カードスロットに対応するアクティブな SubscriptionInfo を取得するために、SubscriptionManager の getActiveSubscriptionInfoForSimSlotIndex() を呼んでいます。API の実行には READ_PHONE_STATE パーミッションが必要になるので、テストコードには GrantPermissionRule を使って同パーミッションを与えました。

    @Rule @JvmField
    val grantPermissionRule: GrantPermissionRule =
            GrantPermissionRule.grant(android.Manifest.permission.READ_PHONE_STATE)

Kotlin コード上で機能させるために、@JvmField アノテーションを付けています。

developer.android.com

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() が一度ずつ呼び出されていたかどうかをチェックしています。

関連のある記事

普段の業務では使わない Kotlin を学ぶべく、のんびりコードを書いています。

前回の記事はこちら。

cheerio-the-bear.hatenablog.com

次回の記事はこちら。

cheerio-the-bear.hatenablog.com