この記事では、Androidエミュレーターを使ったUIテスト(Espresso)を分割・並列実行することによって、実行時間を短縮する方法についてわかりやすくご紹介します。
はじめに
Android アプリケーション開発において、品質を継続的に向上させるために、自動テスト・CircleCI などのCI/CDツールの導入は、もはや欠かせないものとなりました。
そして、自動テスト・CI/CDを1回導入するだけでなく、継続的に改善していくことが重要です。
例えば、アプリケーション規模や自動テストの数、開発規模が大きくなっていくと、CI/CD におけるビルド・テストの実行時間は長くなってしまい、結果として開発スピードを低下させてしまいます。
特に 今回紹介する Espresso などのUIテストでは、実際に Android 実機や Android エミュレーターを動かしてテストを実行する必要があるため、実行時間が長くなりがちです。
CircleCI では Android アプリケーション開発で、実行時間を短縮するためのさまざまな機能が揃っています。
今回は CircleCI のテスト分割・並列実行を活用して、UIテスト(Espresso)の実行時間を短縮する方法について紹介します。
まずは、CircleCIに無料で登録してこのチュートリアルを実行して行きましょう。
CircleCI を使って UIテスト(Espresso)を分割・並列実行する方法(概要)
こちらが、今回紹介する Android アプリケーションのサンプルコードです。
GitHub - tadashi0713/circleci-demo-android
複数の UIテスト(Espresso)が用意されており、実行しているテスト内容は一緒ですが、Thread.sleep() を入れることによって実行時間を変えています。
@HiltAndroidTest
class GardenActivity10Test {
    private val hiltRule = HiltAndroidRule(this)
    private val activityTestRule = ActivityTestRule(GardenActivity::class.java)
    @get:Rule
    val rule = RuleChain
        .outerRule(hiltRule)
        .around(activityTestRule)
    @Test fun clickAddPlant_OpensPlantList() {
        // Given that no Plants are added to the user's garden
        // When the "Add Plant" button is clicked
        onView(withId(R.id.add_plant)).perform(click())
        Thread.sleep(100000)
        // Then the ViewPager should change to the Plant List page
        onView(withId(R.id.plant_list)).check(matches(isDisplayed()))
    }
}
CircleCI でこのテストを実行する際には、Android Orb を使うことによって、下記のように簡潔にパイプラインを作ることが可能です。
CircleCI Developer Hub - circleci/android
integration_test:
  executor:
    name: android/android-machine
    resource-class: xlarge
    tag: 2023.07.1
  steps:
    - checkout
    - android/start-emulator-and-run-tests
    - store_test_results:
        path: ./app/build/outputs/androidTest-results/connected
android/start-emulator-and-run-tests には以下が含まれています。
- AVD(Android仮想デバイス)の作成・Androidエミュレーターの起動
 - Gradle のキャッシュを利用
 - UIテストを実行するための事前ビルド(
./gradlew assembleDebugAndroidTest) - Android エミュレーターが起動するまで待機
 - UIテスト(Espresso)の実行(
./gradlew connectedDebugAndroidTest) 
この複数ある UIテスト を分割・並列実行する手順としては、以下になります。
- UIテストを実行するための事前ビルド(
./gradlew assembleDebugAndroidTest)を行う - 複数のLinux VM・ Android エミュレーターを起動する
 - (実行時間に応じて) テストを分割する
 - 分割されたテストを並列実行する
 - 実行時間が含まれるテスト結果をアップロードする
 
UIテストを実行するための事前ビルドを行う
Espresso を含め、Android アプリケーションでテストを実行する際には、実行前にアプリケーションのビルドが必要になります。
今回は、後ほど複数のAndroidエミュレーターで並列でテストを実行させるために、ビルドの部分のみ(./gradlew assembleDebugAndroidTest)を事前に行います。
CircleCI のジョブ(build_for_integration_test)は以下になります。
build_for_integration_test:
  executor:
    name: android/android-machine
    resource-class: xlarge
    tag: 2023.07.1
  steps:
    - checkout
    - android/restore-gradle-cache
    - run: ./gradlew assembleDebugAndroidTest
    - android/save-gradle-cache
    - persist_to_workspace:
        root: ~/
        paths: .
以下の手順を行っています。
- Gradle のキャッシュを利用(Android Orb を利用)
 - 事前ビルドの実行(
./gradlew assembleDebugAndroidTest) - 事前ビルドの成果物を次のジョブで利用できるようにする(
persist_to_workspace) 
UIテストを分割・並列実行する
事前ビルドのジョブ(build_for_integration_test)が完了したら、実際にテストを分割・並列実行するジョブ(integration_test_parallel)を実行します。
integration_test_parallel:
  parallelism: 6
  executor:
    name: android/android-machine
    resource-class: xlarge
    tag: 2023.07.1
  steps:
    - checkout
    - attach_workspace:
        at: ~/
    - run:
        name: Split Espresso tests
        command: |
          cd app/src/androidTest/java
          CLASSNAMES=$(circleci tests glob "**/*Test.kt" \
            | sed 's@/@.@g' \
            | sed 's/.kt//' \
            | circleci tests split --split-by=timings --timings-type=classname)
          echo "export GRADLE_ARGS='-Pandroid.testInstrumentationRunnerArguments.class=$(echo $CLASSNAMES | sed -z "s/\n//g; s/ /,/g")'" >> $BASH_ENV
    - android/create-avd:
        avd-name: test
        install: true
        system-image: "system-images;android-29;default;x86"
    - android/start-emulator:
        avd-name: test
        post-emulator-launch-assemble-command: ""
    - run:
        name: Run Espresso tests
        command: ./gradlew connectedDebugAndroidTest $GRADLE_ARGS
    - store_test_results:
        path: ./app/build/outputs/androidTest-results/connected
まず、並列でテストを実行するために、複数の Linux VM を立ち上げます。
parallelism を指定することで、並列で実行する Linux VM の数を増減することが可能です。
次に以下の手順を実行しています。
- 事前ビルド成果物を利用(attach_workspace)
 - AVD(Android仮想デバイス)の作成・Androidエミュレーターの起動(Android Orb を利用)
 - (実行時間に応じて) テストを分割、Gradle コマンドに渡すパラメーターを作成
 - 分割されたテストを並列実行
 - 実行時間が含まれるテスト結果をアップロード(store_test_results)
 
テスト分割、並列実行の部分を詳しく解説します。
UIテスト(Espresso)を実行するGradleコマンド(./gradlew connectedDebugAndroidTest)はデフォルトで全てのテストを実行します。
特定のテストを実行したい場合には、クラス名を使って以下のようなパラメーターを指定する必要があります。
./gradlew connectedAndroidTest 
-Pandroid.testInstrumentationRunnerArguments.class=com.google.samples.apps.sunflower.GardenActivity1Test,com.google.samples.apps.sunflower.GardenActivity2Test
実際にテストファイルを分割して、上記のパラメーターを作成している部分が以下になります。
cd app/src/androidTest/java
          CLASSNAMES=$(circleci tests glob "**/*Test.kt" \
            | sed 's@/@.@g' \
            | sed 's/.kt//' \
            | circleci tests split --split-by=timings --timings-type=classname)
          echo "export GRADLE_ARGS='-Pandroid.testInstrumentationRunnerArguments.class=$(echo $CLASSNAMES | sed -z "s/\n//g; s/ /,/g")'" >> $BASH_ENV
CircleCI CLI である、circleci tests glob によって対象となるテストクラスを取得し、circleci tests split によって分割しています。
circleci tests split には --split-by=timings フラグを付けています。
後の store_test_results で JUnit 形式のテストレポートをアップロードしているのが確認できると思います。
--split-by=timings フラグを有効にすることによって、テストレポートに含まれるタイミングデータを利用して均等にテストを分割しようとします。
これによって、より全体のテスト実行時間を短縮させることが可能です。

実際の並列化されたテストの実行時間については、CircleCI の UI (TIMING タブ)から見ていただくことが可能です。

実行時間の異なるテストを6並列で実行していますが、テスト実行時間にばらつきが少ないことが確認できると思います。
おわりに
この記事では、Androidエミュレーターを使ったUIテスト(Espresso)を分割・並列実行することによって、実行時間を短縮する方法について紹介しました。
今回紹介したソリューションには以下の特徴があります。
- 各テストの実行時間に応じてテストを分割・並列実行することができるため、より短時間でテスト実行を終了させることができる
 - parallelism を増やすだけで、CircleCI のクラウド上で簡単に並列数をスケールさせることができる
 
CircleCI では、並列数による課金ではなく、1分毎の Linux VM の使用量(クレジット)によって課金されます。
今回のような複数の Linux VM を並列で実行した場合でも、コストパフォーマンスが高い形でご利用していただくことが可能です。
今後より Android アプリケーション開発のパフォーマンスを上げたい方は、是非参考にしていただければと思います。