Androidアプリのテストを自動化

こんにちはmisocaアプリの評価が3.5になって嬉しいtijinsです。

今日はAndroidアプリのインストルメンテーションテストについて紹介します。

Misocaアプリのテスト

MisocaアプリはCIによる自動テストとリリース前の手動テストを行っているのですが、開発スピードと品質のバランスを保つ為、手動のテストの自動化を進めています。

3種類の自動テスト

Misocaアプリでは3種類の自動テストを使用しています。 今回は、インストルメンテーションテストについて説明します。

インストルメンテーション テスト

エミュレーターまたは実機上で実行されるテストです。 実際のアプリを動作させているので手動テストと同等の検証が可能ですが、実行速度が遅いです。

Unitテスト

開発PCのJVM上で実行されるテストです。 エミュレーターを介さない為、高速なテストが可能ですが、AndroidOSやContextに依存する処理はテストできません。 (Robolectricを使用すればAndroidOSに依存する部分もテスト可能ですが、非対応の機能がありインストルメンテーションテストの完全な代替には至っていません)

スクリーンショット比較テスト

レイアウトやテーマの異常を見つけるテストです。 インストルメンテーションテスト中にSpoonを使用して保存したスクリーンショット画像で、レイアウトの崩れなどをチェックしています。

サンプルアプリ

今回のテストコードが対象にしているのは、以下のサンプル用アプリです。

サンプルコード

  • メイン画面で編集ボタンを押すと、編集画面に遷移する
  • 編集画面で保存ボタンを押すと、編集された文字列がメイン画面に反映される
  • メイン画面で共有ボタンを押すと、IntentChooser付きで共有される

インストルメンテーションテスト

テスト対象アプリの操作にespressoを使用します。

espresso-intentsは、Activity間の遷移を検証したり、ファイル選択等外部のアプリと連携する部分のスタブ化に使用しています。

Espressoを使ったテスト

以下は、メイン画面から編集画面に遷移し、編集結果がメイン画面に反映される事を確認するサンプルです。

app/build.gradle

dependencies {
    // InstrumentTestで使用するライブラリ
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

SampleActivityTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleActivityTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testEdit() {
        // メイン画面で実行される
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        // 編集画面で実行される
        // 初期表示の確認
        Espresso.onView(ViewMatchers.withId(R.id.edit_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Hello")
            )
        )
        // EditTextに入力する
        Espresso.onView(ViewMatchers.withId(R.id.edit_text)).perform(
            ViewActions.replaceText("Bye!")
        )
        // 保存ボタンをクリックする
        Espresso.onView(ViewMatchers.withId(R.id.btn_save)).perform(
            ViewActions.click()
        )

        // メイン画面で実行される
        // 編集結果が反映されている
        Espresso.onView(ViewMatchers.withId(R.id.txt_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Bye!")
            )
        )
    }
}

Espresso-intentsを使ったテスト

startActivity()で送信されたIntentの検証や、ファイル選択など他のアプリと連携する部分のスタブ化が可能です。

送信されたIntentの検証(IntentChooserが無い場合)

app/build.gradle

dependencies {
    // InstrumentTestで使用するライブラリ
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    // import androidx.test.ext.truth.content.IntentSubject.assertThatに必要です
    androidTestImplementation 'androidx.test.ext:truth:1.3.0'

    // espresso-intents
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'

    // 戻るボタンの操作などに利用します
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}

SampleIntentsTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleIntentsTest {

    // IntentTestRuleを使用します
    @get:Rule
    val activityRule = IntentsTestRule(MainActivity::class.java)

    // バックキーを操作する場合に必要です
    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    @Test
    fun testEdit() {
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        // getIntents()はactivityが起動されてから発行されたIntentのリストを返します
        val intents = Intents.getIntents()
        val intent = intents.first()
        assertThat(intent).hasComponent(
            ComponentName(
                "jp.misoca.sampleintentstest",
                "jp.misoca.sampleintentstest.EditActivity"
            )
        )
        // androidx.test.ext.truth.content.IntentSubject.assertThatを使用します
        assertThat(intent).extras().string(Intent.EXTRA_TEXT).isEqualTo("Hello")
    }
}

送信されたIntentの検証(IntentChooserが有る場合)

IntentChooserでラップされたIntentでは Intent.EXTRA_INTENTに元のIntentが入っています。

    @Test
    fun testShare() {
        Espresso.onView(ViewMatchers.withId(R.id.btn_share)).perform(
            ViewActions.click()
        )

        // IntentChooserが使用されている
        val chooser = Intents.getIntents().first()
        assertThat(chooser).hasAction(Intent.ACTION_CHOOSER)

        // 元のIntentを取得する
        val intent = chooser.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
        assertThat(intent).hasAction(Intent.ACTION_SEND)
        assertThat(intent).hasType("text/plain")
        assertThat(intent).extras().string(Intent.EXTRA_TEXT).isEqualTo("Hello")

        // バックキーを操作してChooserを閉じる(Chooserを閉じるまで次のテストに遷移しない為)
        device.pressBack()
    }

startActivityForResult()をスタブ化する

indending()使用するとstartActivityForResult()の実行後`onActivityResult()`がすぐ実行されます。 画像選択など、他のパッケージに依存するテストに便利です。

    @Test
    fun testOnEdit() {
        // onActivityResultに入力されるダミーデータ
        val resultData = Intent().apply {
            putExtra(Intent.EXTRA_TEXT, "Bye!")
        }
        val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)

        // intendingを使用して、編集画面をスタブ化する
        // androidx.test.espresso.intent.Intents.intendingを使用します
        intending(
            IntentMatchers.hasComponent(
                ComponentName(
                    "jp.misoca.sampleintentstest",
                    "jp.misoca.sampleintentstest.EditActivity"
                )
            )
        ).respondWith(result)

        // intendingされているので、編集画面が起動せずにonActivityResultが実行される
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        //結果をチェック
        Espresso.onView(ViewMatchers.withId(R.id.txt_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Bye!")
            )
        )
    }

宣伝

弥生モバイルチームは先月1名メンバーが増えて拡大中です。 AndroidもiOSのエンジニアまだまだ募集中です!! www.wantedly.com

www.wantedly.com