この記事では、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 アプリケーション開発のパフォーマンスを上げたい方は、是非参考にしていただければと思います。