💡 はじめに
こんにちは。ニフティ株式会社のLinです。
台湾出身のモバイルアプリエンジニアとして、社内で「マイ ニフティ(Android・iOS)」の開発を担当しております。
今回は、マイニフティ Android 2.8.1 から導入したMaestroとGitHub Actionsによるテスト自動化についてご紹介します。
🪄 Maestroについて
Maestroは、Android・iOS・React Native・Flutterなどの各種モバイルプラットフォームに対応した、オープンソースの軽量UIテストフレームワークです。
基本利用は無料ですが、CIを導入するとMaestro Cloudは有料サービスです。ただし、GitHub Actionsを利用すれば無料で同様の機能を実現できます。
(節約できる費用はこちらで計算できます)
今回はこの裏技を紹介します。
💭 事前準備
まずは下記のコマンドを使ってMaestroをインストールしましょう。
1 |
curl -fsSL "https://get.maestro.mobile.dev" | bash |
そして、やりたいことを整理しましょう。
実現できたらいいなと思うことを下記の通り並べてみました:
- GitHub Actionsのプルリクで変更があれば自動的にテストフローを実行
- 複数端末のテスト結果を確認できる
- テスト結果をGitHubのコメントに自動投稿
- エビデンスとしてテストの録画を確認できる
こういうことは本当にできるのか?!
答えはまさかのYES!
どうやって実現するのか、一緒にやってみましょう! 🙌
💻 ローカルでの実装
CIによるテスト自動化を実現するため、下記の三つのものが必要です。
- テストしたいアプリ
- Maestroテストフロー
- Github Actionsワークフロー
今回の例は弊社の会員アプリの「マイ ニフティ」で実装しました。
最初にアプリのbuild.gradleにアプリバージョンを取得するタスクを追加します。
(これは後のGitHub Actionsコメント機能で使われています)
1 2 3 4 5 6 7 |
// build.gradle.kts (Module :app) ... tasks.register("printVersionName") { println(project.android.defaultConfig.versionName) } |
Maestroテストフローは下記の通り追加していきます。
重複操作はoperationsにまとめ、テストフローはtestsに集約します。
1 2 3 4 5 6 7 8 |
. └── maesto/ ├── operations/ │ ├── init.yaml │ ├── login.yaml │ └── skip_tutorial.yaml └── tests/ └── test_flow.yaml |
使われたoperationsフローは下記の通り:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// init.ymal appId: ... --- - clearState - launchApp: permissions: notifications: allow - runFlow: when: visible: "ログイン" file: "../operations/login.yaml" - runFlow: when: visible: "マイ ニフティへようこそ" file: "../operations/skip_tutorial.yaml" |
1 2 3 4 5 6 7 8 9 10 11 |
// login.ymal appId: ... --- - tapOn: "@nifty ID または @niftyユーザー名" - inputText: ${USER_ID} - tapOn: "パスワード" - inputText: label: Enter password text: ${PASSWORD} - tapOn: "ログイン" |
1 2 3 4 5 |
// skip_tutorial.ymal appId: ... --- - tapOn: "チュートリアルをスキップ" |
テスト用のtestsフロー例は下記の通り:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// test_flow.yaml appId: ... env: USER_ID: ${USER_ID} # init.yamlから上書きが必要な場合は設定する PASSWORD: ${PASSWORD} onFlowStart: - runFlow: file: "../operations/init.yaml" --- - startRecording: test_flow_recording # ここにテストを書く - stopRecording |
テストフローの作成について、一つ簡単な方法があります。
下記のコマンドを実行すると、Maesteo Studioを呼び出せて、画面をクリックしてテストフローを作成できます。
1 |
maestro studio |
そして、テストフローが完成したら、ローカルで下記のコマンドを実行すると
1 |
maestro test maestro/tests/test_flow.yaml -e USER_ID="XXX" -e PASSWORD="XXX" |
テストフローが自動的に動き始めます。
startRecordingとstopRecordingが設定されているので、実行画面の録画も保存されています。
🐱 GitHub Actionsでの実装
動作できるMaestroフローがあれば、次はGitHub Actionsのワークフローを作りましょう。
.githubのworkflowsでandroid_maestro_tests.ymlを追加しよう。
1 2 3 4 |
. └── .github/ └── workflows/ └── android_maestro_tests.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
// android_maestro_tests.yml name: Maestro Test on: pull_request: branches: - master paths-ignore: - "**.md" jobs: test: name: Run Maestro Tests (API ${{ matrix.api-level }}) runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: api-level: [ 28, 33, 34 ] include: - target: default arch: x86_64 profile: pixel_6 ram-size: 8192M disk-size: 4096M heap-size: 4096M steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: java-version: "17" distribution: "zulu" - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: AVD cache uses: actions/cache@v4 id: avd-cache with: path: | ~/.android/avd/* ~/.android/adb* key: avd-${{ matrix.api-level }} - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: ${{ matrix.arch }} profile: ${{ matrix.profile }} ram-size: ${{ matrix.ram-size }} disk-size: ${{ matrix.disk-size }} heap-size: ${{ matrix.heap-size }} emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none force-avd-creation: false script: echo "Generated AVD snapshot for caching." - name: Build app with Gradle (Production) run: ./gradlew assembleProductionDebug - name: Get apk path id: apk-path run: echo "path=$(find . -regex '^.*/build/outputs/apk/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT - name: Get app version run: | VERSION_NAME=$(${{ github.workspace }}/gradlew -q printVersionName) echo "APP_VERSION=${VERSION_NAME}" >> $GITHUB_ENV - name: Set up Maestro run: | curl -fsSL "https://get.maestro.mobile.dev" | bash ~/.maestro/bin/maestro --version || echo "Failed to execute maestro" - name: Run Maestro tests id: run-maestro-tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: ${{ matrix.arch }} profile: ${{ matrix.profile }} ram-size: ${{ matrix.ram-size }} disk-size: ${{ matrix.disk-size }} heap-size: ${{ matrix.heap-size }} emulator-options: -no-window -no-snapshot-save -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none script: | $ANDROID_HOME/platform-tools/adb install ${{ steps.apk-path.outputs.path }} ~/.maestro/bin/maestro test --format junit maestro/tests/ -e USER_ID="${{ secrets.TEST_USER_ID }}" -e PASSWORD="${{ secrets.TEST_PASSWORD }}" -e APP_VERSION=${{ env.APP_VERSION }} - name: Upload test record id: upload-test-record if: ${{ steps.run-maestro-tests.outcome == 'success' }} uses: actions/upload-artifact@v4 with: name: Android_SDK_API_${{ matrix.api-level }}(${{ env.APP_VERSION }}_Succeeded) path: "**.mp4" - name: Add test record to PR comment if: ${{ steps.upload-test-record.outcome == 'success' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Prepare GitHub CLI if ! command -v gh &> /dev/null; then sudo apt-get install gh -y fi # Prepare PR comment comment="### ${{ env.APP_VERSION }}テスト成功(Android SDK API ${{ matrix.api-level }}) : [レコードをダウンロードする](${{ steps.upload-test-record.outputs.artifact-url }})" # Post PR comment gh pr comment ${{ github.event.pull_request.number }} --body "$comment" - name: Upload test report id: upload-test-report if: ${{ steps.run-maestro-tests.outcome == 'failure' }} uses: actions/upload-artifact@v4 with: name: Android_SDK_API_${{ matrix.api-level }}(${{ env.APP_VERSION }}_Failed) path: | **.mp4 report.xml - name: Add test report to PR comment if: ${{ steps.upload-test-report.outcome == 'success' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Prepare GitHub CLI if ! command -v gh &> /dev/null; then sudo apt-get install gh -y fi # Prepare PR comment comment="### ${{ env.APP_VERSION }}テスト失敗(Android SDK API ${{ matrix.api-level }}) : [レポートをダウンロードする](${{ steps.upload-test-report.outputs.artifact-url }})" # Post PR comment gh pr comment ${{ github.event.pull_request.number }} --body "$comment" |
このワークフローで下記の機能が実装されました:
- エミュレーターはテストしたいAndroid SDKバージョンのPixel 6を指定、実行安定性を保証するためにram-size、disk-sizeとheap-sizeもデフォルト値より高い数値に設定しました
- エミュレーターを利用するためKVMを有効化し、Productionのapkをビルドしてインストールパスを記録し、エミュレーターにapkをインストールしてMaestroテストフローを実行します
- 実行成功したら、エミュレーターのキャッシュが保存され、次回実行する時に再利用されます
- 先ほど追加したprintVersionNameタスクによりテスト対象アプリのバージョンを自動取得し、実行録画もartifactに自動保存、ダウンロードリンクを自動生成して、コメントにエビデンスを残します
- 成功したら成功コメントを自動投稿、失敗したら失敗コメントを自動投稿
ワークフローで使われたIDとパスワードもGitHub → Settings → Secrets and variables → Actions
のsecretsに登録しましょう。
そして、自動テスト君が爆誕。
😎 成果
次回、リリースプルリクが作成された際に、呪文を詠唱すれば自動テスト君を召喚できます。
集いしテストフローが新たな力を呼び起こす!自動化の道となれ!
いでよ、自動テスト君!
冗談です (・ω・)
何もしなくても自動テスト君が自動的に召喚され動作します。
テスト成功・失敗した場合、実行エビデンスが自動生成され、ダウンロードして確認できます。
素晴らしいですね!
テスト結果は一目で確認でき、レコードをダウンロードすると実行録画も確認できます。
💁♂️ 補足
Maestroは便利ですが、よくある落とし穴もいくつかあります:
- Maestro 1.33.1以上では、度々「emulator not found」の問題が発生します
- 詳細
- ram-size、disk-sizeとheap-sizeを高めに設定するか、GitHub Actions Runners のスペックを増強することで、ある程度改善できます
- MaestroはWebViewに弱く、assertVisibleは度々失敗します
- extendedWaitUntil を追加すると、ある程度改善できます
- 度々ボタン・文字を認識できない問題があります
- 対象の contentDescription を追加したら改善できます(null にしないでください)