ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

IntentのresolveActivityを考慮したRobolectricテスト

こんにちは、tkyです。

今日はテストの話です。

画面遷移をテストする時Robolectricを使うと比較的簡単にテストできます。

今回はRobolectric v4.3を使用して

  • 自身のアプリの別Activityに遷移する場合 (=明示的Intent)
  • 外部アプリに遷移する場合 (=暗黙的Intent)

この2つについてこんな感じでできるよ〜というのを紹介したいと思います。

また、Robolectricの説明や導入方法などは記載いたしませんのでご了承願います。

補足ですが、検証にはAssertJを使用しています。

例えば次のような画面遷移をする関数があるとします。

class MainActivity: AppComponentActivity() {

    // 自アプリ内の別Activityへ遷移する
    fun openSubActivity() {
        val intent = SubActivity.createIntent(this)
        startActivity(intent)
    }

    // 別アプリのブラウザアプリを開く
    fun openBrowser() {
        val uri = Uri.parse("https://google.com")
        startActivity(Intent(Intent.ACTION_VIEW, uri))
    }
}

テストコードは次のように書くことができます。

外部ブラウザのように暗黙的インテントによって画面遷移する場合、何が起動するかわからないのでActivityを検証することが難しいです。

そのため、発行したインテントを検証することでコードの正当性を担保します。

class MainActivityTest{

    @Test
    fun `openSubActivityしたらSubActivityに遷移すること`() {
        val activity = Robolectric.buildActivity(MainActivity::class.java)
        activity.openSubActivity() // ここでSubActivityに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // intentのShadowを作成して次に起動するActivityを検証できるようにする
        val shadowIntent = Shadows.shadowOf(intent)
        val nextActivity = shadowIntent.intentClass
        // 遷移先が正しいこと
        Assertions.assertThat(nextActivity).isEqualTo(SubActivity::class.java)
    }

    @Test
    fun `openBrowser_外部ブラウザへ遷移する`() {
        val activity = Robolectric.buildActivity(RootActivity::class.java)
        activity.openBrowser() // ここで外部ブラウザに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // urlスキームが正しくパースできること
        Assertions.assertThat(intent.data?.toString()).isEqualTo("https://google.com")
        // 外部ブラウザなので何が起動するか不明。暗黙的インテントのACTIONでassertする
        Assertions.assertThat(intent.action).isEqualTo(Intent.ACTION_VIEW)
    }
}

これで一応テスト可能となりますが、ここで公式のIntent起動ドキュメントを見てみましょう。

developer.android.com

注意:端末に暗黙的インテントを受け取ることができるアプリがない場合、startActivity() を呼び出すとアプリがクラッシュします。まず、インテントを受け取るアプリの存在を確認するために、Intent オブジェクトの resolveActivity() を呼び出してください。結果が null 以外の場合は、インテントを処理できるアプリが少なくとも 1 つあるということなので、startActivity() を安全に呼び出すことができます。結果が null の場合は、そのインテントは使用しないでください。可能であればそのインテントを呼び出す機能を無効にしてください。

要するに以下のようにIntentが起動できるか resolveActivity を使ってチェックしてから起動することを推奨しています。

    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
class MainActivity: AppComponentActivity() {

    // 自アプリ内の別Activityへ遷移する
    fun openSubActivity() {
        val intent = SubActivity.createIntent(this)
        startActivity(intent)
    }

    // 別アプリのブラウザアプリを開く
    fun openBrowser() {
        val uri = Uri.parse("https://google.com")
        val intent = Intent(Intent.ACTION_VIEW, uri)
        // intentを受け取れるアプリがいるか確認してから起動する
        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }
    }
}

こうすると何が起こるのかというとRobolectricではresolveActicvity()の戻りがnullになってしまいstartActivityが実行されないことでテストが通らなくなってしまいます。

これを解決するためにShadowPackageManagerを使用してresolveActivity()をモックできるようにします。

    @Test
    fun openByUrlScheme_open_browser_外部ブラウザへ遷移する() {
        val activity = Robolectric.buildActivity(RootActivity::class.java)
        // ▼▼▼▼ 追加ここから ▼▼▼▼
        // ダミーでComponentNameを返す。Intentを検証するのでパッケージ名とActivity名は何でも良い
        val componentName = ComponentName(“com.some.other.package”, “MainActivity”)
        val intentFilter = IntentFilter(Intent.ACTION_VIEW).apply {
            addCategory(Intent.CATEGORY_DEFAULT)
            addCategory(Intent.CATEGORY_BROWSABLE)
            addDataScheme(“https”)
        }
        // PackageManagerのShadowを作成してIntentFilterを追加しておく
        Shadows.shadowOf(ApplicationProvider.getApplicationContext<MyApplication>().packageManager).apply {
            addActivityIfNotPresent(componentName)
            addIntentFilterForActivity(componentName, intentFilter)
        }
        // ▲▲▲▲ 追加ここまで ▲▲▲▲
        activity.openBrowser() // ここで外部ブラウザに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // urlスキームが正しくパースできること
        Assertions.assertThat(intent.data?.toString()).isEqualTo(“https://google.com”)
        // 外部ブラウザなので何が起動するか不明。暗黙的インテントのACTIONでassertする
        Assertions.assertThat(intent.action).isEqualTo(Intent.ACTION_VIEW)
    }

この辺調べても古いバージョンのRobolectricでの対応方法などが出てきて新しいバージョンでの解決方法がなかったので結構ハマりました。

これでGoogleが推奨する外部ブラウザを開くような暗黙的インテントの実装をした状態でテストもできるようになりました。

是非試してみてください。