
はじめに
こんにちは、ブランドソリューション開発本部FAANS部でAndroidアプリを担当している田中です。本記事ではバグ件数削減の施策の1つとしてFAANS Androidで実施したJetpack ComposeのUIテストの自動化についてご紹介します。
目次
- はじめに
- 目次
- 背景
- Firebase Test Labについて
- 料金について
- UIテストを記載する
- UIテストで使用するテストファイル
- GitHub ActionsでFirebase Test Labを実行する
- マルチモジュールプロジェクトでFirebase Test Labを実行する
- UIテストを導入してみて
- 終わりに
背景
FAANSでは、リリースまでに下記のようなフローを踏んでいます。

昨年、変更行数は約6万行で開発期間が約半年の比較的大きなリリースを伴う案件がありました。品質保証を担当するQAチームによるテスト実施において、Androidアプリは166件という非常に多くの不具合の指摘を受けてしまいました。
FAANSでは、各案件のリリース後、QAチームが品質管理の観点から開発プロセスの改善点を見出す品質報告会を実施しています。その品質報告会で当案件について、Androidで検知された不具合のうち約8割がQA実施フェーズに入る前の段階で検知可能であると共有されました。
Androidチームでも不具合を分析してみると、たしかにその多くが仕様に対する認識漏れや実装漏れといった開発フェーズで対処できる軽微な内容でした。
このような軽微なバグがQA実施フェーズまで流れ込むのを防ぐため、Androidチームでは不具合を検知・防止するアプローチの1つとしてUIテストを導入することにしました。
本記事では、Firebase Test Labを活用してGitHub Actions上でUIテストを自動化した方法と、その運用状況について紹介します。
Firebase Test Labについて
以前弊社が公開したFirebase Test Labを使ったAndroidアプリのテストに詳しい内容が記載されているのでご参照ください。
Firebase Test Labは、GoogleのFirebaseが提供するクラウドベースのアプリテストサービスです。多様な実機や仮想デバイス上で自動化されたテストを実行し、アプリの動作やパフォーマンスを検証できます。
本記事では、UIテストを実行するためにFirebase Test Labで実行できるInstrumentation Testについて触れます。Firebase Test Labを活用することで、GitHub Actionsを用いた自動テストにUIテストを組み込むことが可能になります。
料金について
Test Labの利用料金は、1日あたりのテスト実行数や実行時間で算出されます。
FAANS AndroidはFirebaseのBlazeプランを利用しており、また、Firebase Test Labでは仮想デバイスを使用するテストを選択しました。その場合の料金体系は以下のとおりです。
- 無料枠: 1日あたり60分のテスト時間
- 超過時: 各仮想デバイスにつき1時間あたり$1
料金の詳細はTest Lab公式ドキュメントの割り当てをご覧ください。
UIテストを記載する
本記事ではFirebase Test Labの運用についての内容を主軸としているためUIテストについての説明は比重を低くしています。
UIテストを記載、実行するために以下を設定します。
build.gradleの設定
dependencies {
androidTestImplementation "androidx.test:runner:$androidXTestVersion"
androidTestImplementation "androidx.test:rules:$androidXTestVersion"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
}
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
詳細な設定方法については、Instrumented Testsの設定およびCompose レイアウトをテストするをご参照ください。
UIテストで使用するテストファイル
デフォルトで作成されるExampleInstrumentedTest.ktがあれば、最低限テストを実行できます。このファイルはmodule-name/src/androidTest/java/に配置されます。
/** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("jp.android.faans.base.test", appContext.packageName) } }
FAANS AndroidではFragmentごとにテストを作成しています。実際に記載しているテストケースを3つ紹介します。
1. 特定の文字列が表示されているかのテスト
このテストでは、投稿された動画一覧の画面で投稿が0件の場合に、特定の文言が表示されるかを確認します。
onNodeWithTextは特定の文字列が画面に表示されている場合に検知でき、assertIsDisplayed()で表示されているかを検証できます。
@Test fun when_video_items_is_empty_then_display_enroll_video_nothing_text() { composeTestRule.setContent { FaansTheme { RegisterRelatedVideoScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = fakeSuccessState.copy(videoItems = emptyList()), initialSelectedVideoItems = emptyList(), onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithText("ノウハウ動画は\nまだ投稿されていません").assertIsDisplayed() }
2. 特定のコンポーネントが表示されているかのテスト
こちらのテストではUIを構成するStateがLoadingの状態の時にローディングのコンポーネントが表示されているかをテストしています。
onNodeWithTagは、Modifier.testTagで設定したタグ名のコンポーネントを検知でき、1と同様にassertIsDisplayed()で表示されているかを検証できます。
@Test fun when_state_is_loading_then_ui_is_loading() { composeTestRule.setContent { FaansTheme { RegisterCoordinateTagScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = RegisterCoordinateTagState.Loading, onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithTag("progress").assertIsDisplayed() }
3. アイコン押下で意図したダイアログが表示されているかのテスト
このテストでは、特定のアイコンを押下した後に表示されるボタンを選択した際、意図したダイアログが正しく表示されるかを確認します。
assertIsDisplayed()を使った検証は上記2つと同様で、performClick()を利用することで特定のコンポーネントを押下できます。
@Test fun when_video_delete_menu_tap_then_is_display_video_delete_dialog() { composeTestRule.setContent { FaansTheme { CoordinateDetailScreen( page = 0, state = fakeCoordinateState.copy( isTransitionFromRanking = false, wayDetail = fakeWayDetail, ), actionDispatcher = { }, onVolumeChanged = { }, onPopBackStack = { }, onTransitionToReviewComments = { _, _, _ -> }, onTransitionToEditPage = { _, _ -> }, onTransitionToBreakDownDetail = { _, _ -> }, onNavigateWearPreview = {}, onNavigateVideoRegistration = {}, ) } } composeTestRule.onNodeWithTag("know_how_menu_icon").performClick() composeTestRule.onNodeWithText("投稿を削除する").performClick() composeTestRule.onNodeWithText("削除の確認").assertIsDisplayed() composeTestRule.onNodeWithText("ノウハウ動画を削除しますがよろしいですか?").assertIsDisplayed() composeTestRule.onNodeWithText("削除する").assertIsDisplayed() composeTestRule.onNodeWithText("キャンセル").assertIsDisplayed() }
他にもperformScrollTo()を利用した画面のスクロール操作やperformTextInput()を利用した文字入力によるUIのテストなども作成しています。
Jetpack ComposeのUIテストで可能なことは、公式ドキュメントのテスト早見表にまとめられているのでご参照ください。
GitHub ActionsでFirebase Test Labを実行する
Firebase Test Labを使ったUIテストを自動化させるにあたり、CIシステムでテストするではJenkins CIでの実行について記載があります。
FAANS AndroidではCIシステムをGitHub Actionsで行なっているため、要件に記載がある手順を自チームの環境に適応させて以下の流れで導入しました。
- Google CloudのAPIの有効化
- サービスアカウントとCloud Storageバケットの作成
- GitHub ActionsからGoogle Cloudへの認証
こちらの手順について、FAANS Androidで実施した内容を説明します。
1.Google CloudのAPIの有効化
要件に記載がある通り、Google Cloud Testing API とCloud Tool Results API を有効にします。
2.サービスアカウントとCloud Storageバケットの作成
Cloud Storageバケットの種類の選定
要件には、Test Labで使用するサービスアカウントに関して次のような記載があります。
Create a service account with an Editor role in the Google Cloud console and then activate it.
「Editor role」とは、Google CloudのIAMの基本ロールの1つである編集者ロール(roles/editor)を指しています。
このロールはGoogle Cloudプロジェクト内の全てのプロダクトに跨る大量のIAM権限を有した強い権限を持つため、サービスアカウントへの使用はセキュリティの観点から一般的には推奨されません。
そのため、編集者ロールを付与したサービスアカウントの使用はできれば避けたいと考えました。
調査したところ、Test Labで生成されたテスト結果保存先のCloud Storageバケットとして、以下の2種類から選択できることが分かりました。
- 構築・管理不要なFirebaseが自動管理するデフォルトバケット
- 自前で構築・管理する独自バケット
独自バケットを利用すると、編集者ロールよりもずっと少ないIAM権限(※後述)で済みます。ただし、デフォルトバケットはテスト結果の保持期間に90日という制約がある一方、ストレージにかかるコストが無料である点にメリットがあります。
FAANSでは、Test Lab上で想定されるテストの実行頻度や保存期間を基に、独自バケットのストレージコストを見積もりました。その結果、独自バケットの場合に上乗せされるCloud Storageのコストとセキュリティ強度のトレードオフを踏まえ、独自バケットを使用することに決めました。以降は独自バケットを使用する前提の説明になります。
以下は、独自バケットを作成するTerraform定義の例です。
resource "google_storage_bucket" "firebase_test_lab" { name = "<バケット名>" location = "us-central1" uniform_bucket_level_access = true # Firebase Test Labのテスト結果を過去7日分保持する場合のライクサイクル設定 lifecycle_rule { action { type = "Delete" } condition { age = 7 } } }
gcloud CLIでTest Lab実行時、独自バケットへテスト保存は「gcloud firebase test android run」の--results-bucketで指定可能です。
サービスアカウントの作成
独自バケットを利用する場合、Firebase Test Lab用サービスアカウントに以下2つの事前定義ロールを付与する必要があります。
- Firebase Test Lab Admin(
roles/cloudtestservice.testAdmin) - Firebase Analytics Viewer(
roles/firebase.analyticsViewer)
詳細な権限設定については公式ドキュメントのIAM 権限リファレンス ガイドを参照してください。
サービスアカウントと、必要なIAMロールを付与するためのTerraform定義例は以下の通りです。
resource "google_service_account" "firebase_test_lab" { account_id = "firebase-test-lab" } resource "google_project_iam_member" "firebase_test_lab" { for_each = toset([ "roles/firebase.analyticsViewer", "roles/cloudtestservice.testAdmin", ]) project = <Google CloudのプロジェクトID> role = each.key member = "serviceAccount:${google_service_account.firebase_test_lab.email}" }
次の手順で行う「GitHub ActionsからGoogle Cloudへの認証」にて上記で作成したサービスアカウントを利用します。
3.GitHub ActionsからGoogle Cloudへの認証
gcloud CLIでテストを開始するではGoogle Cloud SDKをダウンロードしてgcloud CLIにログインするといった手順が記載されています。GitHub Actionsの場合は以下のyaml定義で、Google Cloudへの認証とCloud SDKのインストールをCIの処理で実行できます。
- name: Authenticate to Google Cloud uses: google-github-actions/auth@v0.4.0 with: # 上記で作成したサービスアカウントと連携済みのWorkload Identityプロバイダーを指定。 workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' # 上記で作成したサービスアカウントを指定。 service_account: 'my-service-account@my-project.iam.gserviceaccount.com' - name: Set up Google Cloud SDK uses: google-github-actions/setup-gcloud@v2
Google Cloudへの認証をGitHub Actionsで行う際は以下のアクションを利用できます。
google-github-actions/auth@v0.4.0google-github-actions/setup-gcloud@v2
以上でFirebase Test Labを使用する前準備が完了します。
マルチモジュールプロジェクトでFirebase Test Labを実行する
FAANS Androidは複数のGradleモジュールが存在するマルチモジュールプロジェクトです。
Firebase Test LabのInstrumentation Testは、モジュールごとに実行されます。
マルチモジュールプロジェクトの場合、全体をテストするには1度のCIの実行で全モジュールのテストを実施する必要があります。
しかし、毎回全モジュールをテストすると膨大なテストが走ってしまうため、テスト実行回数を抑制することにしました。
そのため、FAANS Androidでは差分のあるモジュールのみを対象にテストを実施しています。
以下の流れで実際にテスト実行回数を制御しています。

(1)差分モジュールを検出
次のコードを1つのステップで実施します。
1.ベースブランチのフェッチ
# Fetch the base branch echo "Fetching the base branch: refs/heads/$BASE_REF" git fetch origin refs/heads/$BASE_REF || git fetch --all
2.プロジェクトの全モジュール名を格納
# Get the list of modules echo "Getting the list of modules..." modules=$(./gradlew projects --quiet | grep -oP "^.*--- Project ':\K[^\']+") modules_list=$(echo "$modules" | sed 's/:/\//g' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
ここではfeature/mypageといったモジュールがあったときに、featureとfeature/mypageが取得されます。
親の階層のfeature自体は不要なので後続のステップで取り除く形としています。
3.ベースブランチと作成中のブランチの差分ファイルを検出
# Determine changed files echo "Getting the list of changed files..." changed_files=$(git diff --name-only origin/$BASE_REF $GITHUB_SHA) echo "Changed files:" echo "$changed_files" | tr ' ' '\n'
4.差分ファイルがあるモジュールの割り出し
# Identify changed modules echo "Identifying changed modules..." changed_modules="" for module in $modules_list; do if echo "$changed_files" | grep -q "^$module/"; then changed_modules="$changed_modules $module" fi done
5.余分な空白の削除
# Clean up leading/trailing spaces changed_modules=$(echo $changed_modules | sed 's/^ *//') echo "Changed modules detected: $changed_modules"
6.差分モジュールが存在するかの確認
# Set environment variable or skip tests if [ -z "$changed_modules" ]; then echo "No changed modules detected. Skipping instrumentation_test." echo "SKIP_TESTS=true" >> $GITHUB_ENV else echo "Tests are required for changed modules: $changed_modules" echo "changed_modules=$changed_modules" >> $GITHUB_ENV echo "SKIP_TESTS=false" >> $GITHUB_ENV fi
SKIP_TESTSフラグでテスト実施不要な場合の制御を行なっています。
changed modulesには、base domain domain/domain のようなモジュール名が格納されます。
(2)変更のあったモジュールごとにテストAPK作成
変更があったモジュール毎にassembleDebugAndroidTestを実行してテストAPKをそれぞれ作成します。
- name: Build test APK if: env.SKIP_TESTS == 'false' run: | for module in ${{ env.changed_modules }}; do echo "Building test APK for module $module" if [[ $module == */* ]]; then ./gradlew :${module//\//:}:assembleDebugAndroidTest || true else ./gradlew :$module:assembleDebugAndroidTest || true fi done
${module//\//:}は、モジュール名中のすべての / を : に置き換える処理:app:module:assembleDebugAndroidTestの形でテストを行う必要があるため、app/moduleをapp:moduleの形に変換する
|| trueを設定しているのはモジュールの階層の親でテストが存在しないモジュールも含まれてしまっているため、そのような場合のテストが失敗した時にテストが止まらないようにするため
(3)GitHub Actionsで利用できるように一時的にアップロードする
各モジュールのテストAPKは以下に格納されます。
/$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk
それぞれのモジュールに配置されたテストAPKを利用しやすい形にするため、GitHub Actions上の./test-apksというパスに作成されたテストAPKを配置するようにします。
- name: Archive assembled test APKs if: env.SKIP_TESTS == 'false' run: | mkdir -p ./test-apks for module in ${{ env.changed_modules }}; do apk_path=$(find . -type f -path "*/$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk") if [ -n "$apk_path" ]; then echo "Found test APK for module $module: $apk_path" cp "$apk_path" ./test-apks/ else echo "No test APK found for module $module" fi done - name: Check if test-apks directory has files if: env.SKIP_TESTS == 'false' run: | if [ -z "$(ls -A ./test-apks)" ]; then echo "No test APKs found. Setting SKIP_TESTS=true." echo "SKIP_TESTS=true" >> $GITHUB_ENV fi - name: Upload test APKs if: env.SKIP_TESTS == 'false' uses: actions/upload-artifact@v4 with: name: assembled-test-apks path: ./test-apks
(4)Google Cloud SDKのセットアップ
GitHub ActionsでFirebase Test Labを実行するセクションの「3.GitHub ActionsからGoogle Cloudへの認証」に記載した設定方法を実行します。
(5)テストAPKごとのFirebase Test Lab実行
./test-apksに格納されているテストAPKごとにInstrumentation Testを実行することで、差分があるモジュールの数だけFirebase Test Labが実行されます。
- name: Run Firebase Test Lab if: env.SKIP_TESTS == 'false' run: | for test_apk in ./test-apks/*.apk; do gcloud firebase test android run \ --type instrumentation \ --app app/build/outputs/apk/debug/app-debug.apk \ --test "$test_apk" \ --results-bucket=<作成したCloud Storageの独自バケットの名前> \ --device model=lynx,version=33,locale=ja_JP,orientation=portrait done
以上の設定を含むGitHub Actionsのワークフローファイルをリポジトリに配置することで、GitHub ActionsからFirebase Test Labを利用した自動テストを実行できます。
UIテストを導入してみて
テストケースの作成状況
現在のテストケース作成状況は以下のとおりです。
- テストが存在しているモジュールは18個
- プロジェクト全体で作成済みのテストケースは154個
FAANS Androidで最もテストケースを作成しているモジュールには現状79個のテストケースが存在します。

こちらのモジュールでテストを実施した際、Firebase Test Labの実行時間は40秒ほどでした。

差分を検出したモジュールが1つの場合一連のテスト実行時間は大体20分程度になっています。FAANS Androidで別途、並列実施しているCIと実行時間があまり変わらないため、現状は開発の生産性を落とすようなテストとはなっていません。

また、CI上でGoogle Cloudへの認証から、APKのビルド、そして全モジュールに対するInstrumentation Testの実施完了までにかかる合計時間は約40分となります。
(なお、この時間はFirebase Test Labの従量課金で計算される実行時間とは異なります)
1つのプルリクエストで全てのモジュールにコード差分が発生することは基本的にはないので目安程度の数字です。

導入後のQAにおける不具合の検出件数について
UIテスト導入前の開発案件(変更行数は約6万行、開発期間は約半年)では、166件の不具合検出がありました。
一方、UIテストを実装した案件(変更行数は約6万行、開発期間は約4か月)では、不具合検出が23件に減少しました。
また、QAチームからの品質報告会でもQA実施時に検出された不具合の内、QA実施前に検出されるべき不具合の件数が非常に下がったと共有を受けることができました。

UIテスト導入は、実装者とレビュアーが満たすべき仕様を正確かつ効率的に把握しやすくなった点やリグレッションを検知しやすくなった点で、品質向上に一定の効果が得られたと感じています。
終わりに
本記事ではFAANS Androidチームにおける、UIテストの自動化と現状のテスト運用について紹介しました。
UIテストの導入により、実装者とレビュアーは仕様誤認や実装漏れを防ぎやすくなり、開発プロセスの品質向上に大きく繋がりました。
しかし、現状は差分があるモジュールであれば実行対象になってしまうので明らかにUIに影響がないような差分でもテストが実行されてしまうなど、まだまだ改善の余地があります。
また、UIテストについても重要度が高いテスト内容とは何かなど、テストの質についても日々チームで議論を重ねています。
今後もチームとしてさらに品質向上に努めていければと思います。
Firebase Test Labの導入を検討している方がいれば、ぜひ参考にしてみてください。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。