Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

CircleCI + Android UI Test スクリーンショットの確認仕組み

こんにちは、グノシー Android アプリの開発担当の Liang です。

この記事は Gunosy Advent Calendar 2023 の23日目の記事です。前回の記事は Naoto Koizumi さんの「CircleCIからGithub Actionsに大引っ越しした話」でした。

今回は CircleCI での自動化テスト:Android Instrumentation Test において、スクリーンショットを確認出来る仕組みを紹介したいと思います。

背景

グノシー Android アプリでは、CircleCI を用いた自動 UI テストを導入しています。テスト失敗の原因をログだけで判断していたので、十分な情報が足りないため、対応に時間がかかっていました。そこでテスト失敗時のスクリーンショットを見れるようにして、確認・修正の効率を向上させようと考えました。

CircleCI TESTS

前提対応:CI/CD での自動化テスト

スクリーンショットを撮る UI テストを導入するにあたり、エミュレーターを立ち上げられる Virtual Machine が必要になります。下記を参考に、Github Actions 等の CI / CD ツールで Android System Image を導入してください。 circleci.com

テストケースの実行結果を検知

グノシー Android アプリの運用では、テストケースが失敗の時だけ、スクリーンショットを撮るので、junit のTestWatcherを使って、各ケースのRuleとして失敗のコールバックを検知します。

  • FailedTestWatcher
class FailedTestWatcher : TestWatcher() {
    override fun succeeded(description: Description?) {
        super.succeeded(description)
    }    
    override fun failed(e: Throwable?, description: Description?) {
        super.failed(e, description)
        // take a screenshot
    }
}
  • Test Class
@Rule
val ruleChain = RuleChain
        .emptyRuleChain()
        .around(FailedTestWatcher())

スクリーンショットを取得

テストが失敗したタイミングFailedTestWatcher.failedで、まずuiAutomation.takeScreenshotを通じて今のスクリーンショットであるBitmapを取得します。 実行したエミュレーターのストレージに画像を書き込むため、ContentResolver.insertUriMediaStore.VOLUME_EXTERNAL_PRIMARYに指定して、書き込む用のパスUriを取得します。 書き込む内容ContentValuesには下記のカラムを記入し、外部から画像をダウンロード出来るようにします

  • Media.DISPLAY_NAME画像ファイル名
    • {TestClass}_{TestMethod}となるように指定しています。
  • Media.MIME_TYPE: image/pngファイルタイプ
  • MediaColumns.RELATIVE_PATHディレクトリ名
    • Environment.DIRECTORY_PICTURESメディアコンテンツとして保存します。

最後にcontentResolver.openOutputStreamを経由し、Bitmapを PNG フォマットのInputStreamとして、画像ファイルに転換します。

val bitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot()
val contentResolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver
val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val contentValues = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "${description?.testClass?.simpleName}_${description?.methodName}")
    put(MediaStore.Images.Media.MIME_TYPE, "image/png")
    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val uri = contentResolver.insert(contentUri, contentValues) ?: return
contentResolver.openOutputStream(uri)?.use {
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}

CI/CD の artifacts に保存

CircleCI では、ARTIFACTS を通じてスクリーンショットである画像ファイルを確認するため、該当Jobには保存用adbコマンドの Bash を実行させます。adb shell findを経由し、エミュレーターのstorage/emulated/0/Picturesに保存した PNG 画像ファイルのパスを探し出します。 次にadb shell catから、指定したフォルダーに各画像をコピーして保存します。

mkdir -p failed_screenshots
screenshots = $(adb shell find storage/emulated/0/Pictures -name "*.png")
for screenshot in screenshots; do \
  adb shell cat "${screenshot}" > "failed_screenshots/${screenshot##*/}"; \
done

これで CircleCI Dashbord のARTIFACTS上でテスト失敗のスクリーンショットを確認出来るようになりました。

CircleCI ARTIFACTS

感想

Android 開発においての UI Test は、異なる端末環境での実行結果も変わる可能性が大いにあり、レアケースを含めた不具合を解消するには、やはりテスト過程の確認が必要不可欠です。動画としてレコードしたい場合もTestWatcheradb shell screenrecordを通じで、実現が可能でしょう。

明日は Hironori Yamamoto さんの「大規模データ基盤における dbt のオーケストレーション」です。お楽しみに!