ネイティブモバイルアプリのクロスプラットフォームE2Eテストガイド
この記事は、特にAndroidとiOSのネイティブモバイルアプリを含む、継続的インテグレーションと継続的デリバリー(CI/CD)のコンテキストで、モバイル自動化テストで頭痛を抱えている人にとって必読です。この特定のトピックを網羅する十分なリソースを見つけるのは非常に困難です。
この記事では、iOSとAndroidの両方のプラットフォームでGitHub Actionsを利用して、包括的なエンドツーエンドテストパイプラインを無料で作成する方法について、詳細な手順を説明します。チュートリアル全体を通して、私たちのお気に入りのWebdriverIOフレームワークを使用します。
課題
私たちの課題は、iOSとAndroidの両方のプラットフォームでネイティブモバイルアプリケーションのテストを可能にする統一されたパイプラインワークフローを確立することです。以前の記事では、GitHub Actionsを使用してエミュレーターでAndroidアプリのテストパイプラインを構築するプロセスについて徹底的に説明しました。AndroidコンポーネントのE2Eテストを処理するために、そのワークフローを再利用します。ただし、iPhone/iPadシミュレーターの個別のジョブを作成するという、残りの大きな課題に対処する必要があります。
調査中に、非常に便利だが見過ごされがちな機能、つまりmacOS GitHubランナーがXcodeと、iPhone iOSシミュレーターに必要なSDKを事前に搭載していることを発見しました。このことにより、Androidエミュレーターの場合と同様に、iOSシミュレーターに対してもプロセスを複製するというアイデアが浮かびました。これは、GitHub ActionsのiOSランナーが仮想化をサポートしているため、私たちの目的にとって実行可能なオプションであるため、特にエキサイティングです。
この機能により、追加費用なしで(最大2000分まで)パイプラインを構築できます。
ワークフロー構造
GitHub Actionsのワークフローは、基本的な概念ではAndroidと同じですが、技術的な違いが少しあります。
- シミュレーターの作成
- 依存関係のインストール
- テストの実行
- レポートの生成
非常に簡単そうですが、どのように行うのでしょうか?見てみましょう。
ステップ1
前述のように、GH Actionsランナーには、さまざまな利用可能なシミュレーターが搭載されています。これらの既存のシミュレーターの1つを使用することもできますが、deviceNameを使用する必要があり、その
UUIDは実行ごとにランダムに変更されます。ただし、シェルコマンドを使用して関連するUUIDを抽出することもできます。
プロセスを簡素化し、柔軟性を高めるために、独自のシミュレーターを作成します。Xcodeは既にインストールされているため、「xcrun」CLIを使用できます。ターミナルを使用してインストール済みのiOSバージョンからシミュレーターを作成するには、次のコマンドを実行するだけです。
xcrun simctl create "iPhone 14 Pro" "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro" "com.apple.CoreSimulator.SimRuntime.iOS-16-0"
このコマンドを実行すると、シミュレーターがすぐに作成され、そのUUIDが取得されます。
再利用性を高め、プロセスを最適化するために、このコマンドをシェルスクリプトにカプセル化できます。少し変更を加えることで、UUIDがGitHub Runnerの環境変数として保存されるようにすることができます。これは最終的にテスト機能に使用します。
#!/bin/bash
# Set iPhone model and iOS version
iphone_model="${IPHONE_MODEL// /-}"
ios_version="${IOS_VERSION//./-}"
simulator_name="${iphone_model}"
simulator_udid=$(xcrun simctl create "$IPHONE_MODEL" "com.apple.CoreSimulator.SimDeviceType.$iphone_model" "com.apple.CoreSimulator.SimRuntime.iOS-$ios_version")
# Export the simulator UDID as an environment variable
export SIMULATOR_UDID="$simulator_udid"
echo "SIMULATOR_UDID=$SIMULATOR_UDID" >> $GITHUB_ENV
# Boot the simulator
xcrun simctl boot "$simulator_udid"
上記のスクリプトを使用することで、デバイスモデルとiOSバージョンを環境変数として提供できます。これはワークフローの環境セクションに保存され、シミュレーターが作成され、そのUUIDがGITHUB_ENVに保存されます。このUUIDは、テストで必要な機能を構成するために不可欠です。
シェルスクリプトでIPHONE_MODELとIOS_VERSIONを環境変数として使用しているため、以下に示すように環境セクションに設定する必要があります。
ステップ2
前のステップでシミュレーターの作成と起動に成功したら、プロセスが問題なく完了し、デバイスが完全に使用できる状態になっていることを確認することが重要です。
テストの開始が成功することを確認するために、IOSが完全に起動したことを確認することが重要です。この目的のために、デバイスの状態を特定の出力が得られるまで継続的に監視するコードスニペットを作成しました。これは、シミュレーターの起動プロセスの完了を示しています。
#!/bin/zsh
function wait_for_boot() {
printf "${G}==> ${BL}Waiting for the simulator to boot...${NC}\n"
start_time=$(date +%s)
spinner=( "⠹" "⠺" "⠼" "⠶" "⠦" "⠧" "⠇" "⠏" )
i=0
# Get the timeout value from the environment variable or use the default value of 60 seconds
timeout=${BOOT_TIMEOUT:-60}
while true; do
output=$(xcrun simctl bootstatus "$SIMULATOR_UDID")
echo "${output}"
if [[ $output == *"Device already booted, nothing to do."* ]]; then
printf "\e[K${G}==> \u2713 Simulator booted successfully${NC}\n"
exit 0
else
printf "${YE}==> Please wait ${spinner[$i]} ${NC}\r"
i=$(( (i+1) % 8 ))
fi
elapsed_time=$(( $(date +%s) - $start_time ))
if [[ $elapsed_time -ge $timeout ]]; then
printf "${RED}==> Timeout waiting for simulator to boot 🕛${NC}\n"
exit 1
fi
sleep 1
done
}
# Call the wait_for_boot function
wait_for_boot
ステップ3
さらに進んで、テストを実行するために必要なステップと依存関係について説明します。これには、Appium、XCUITestドライバ、および必須のNode.jsライブラリのインストールが含まれます。
"devDependencies": {
"@wdio/allure-reporter": "^8.10.4",
"@wdio/appium-service": "^8.10.5",
"@wdio/cli": "^8.10.5",
"@wdio/local-runner": "^8.10.5",
"@wdio/mocha-framework": "8.10.4",
"@wdio/spec-reporter": "8.8.7",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"dependencies": {
"allure-commandline": "^2.22.1",
"appium": "2.0.0-beta.71",
"appium-uiautomator2-driver": "*",
"appium-xcuitest-driver": "*"
}
パズルのピースをつなげる
これで、iOSシミュレーターでモバイル自動化テストを実行するための環境をセットアップするために必要な主要な要素が準備できました。それらをすべてGH Actionsの単一のYAMLファイルにまとめましょう。
name: Wdio-x-native
on:
workflow_dispatch:
env:
IPHONE_MODEL: iPhone 8
IOS_VERSION: 16.2
BOOT_TIMEOUT: 700
jobs:
ios:
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Export environment variables
run: |
export IPHONE_MODEL=$IPHONE_MODEL
export IOS_VERSION=$IOS_VERSION
- name: Start simulator
run: |
chmod a+x ./sscript/start_simu.sh
./sscript/start_simu.sh
- name: Install dependencies
run: |
npm i
- name: Check simulator booting status
run: |
chmod a+x ./check_simu.sh
./check_simu.sh
- name: Execute the test
run: |
npm run ios
シミュレーターの状態チェックとシミュレーターの起動を単一のシェルスクリプトにまとめることも可能でした。しかし、私はそれらを個別に実行するために意図的に分離しました。これにより、シミュレーターの起動と残りの依存関係のインストールにかかる時間を利用できます。その後、シミュレーターの状態を確認できます。同様に、Androidエミュレーターにも同じアプローチを適用します(以前の記事を参照)。
クロスプラットフォームワークフローの構築
今度は、以前の記事のAndroidワークフローとIosワークフローを、次のようにマトリックス戦略を使用して単一のワークフローに組み合わせる時間です。
name: Wdio-x-native
on:
workflow_dispatch:
env:
IPHONE_MODEL: iPhone 8
IOS_VERSION: 16.2
API_LEVEL: 32
EMULATOR_NAME: Nexus
EMULATOR_DEVICE: Nexus 5
EMULATOR_VERSION: 12
ANDROID_ARCH: x86_64
ANDROID_TARGET: google_apis
ANDROID_BUILD_TOOLS_VERSION: 34.0.0-rc4
ANDROID_SDK_PACKAGES: system-images;android-32;google_apis;x86_64 platforms;android-32 build-tools;34.0.0-rc4 platform-tools emulator
EMULATOR_TIMEOUT: 350
BOOT_TIMEOUT: 700
jobs:
ios:
runs-on:
- macos-13
strategy:
matrix:
os: [IOS]
device: [$IPHONE_MODEL]
version: [$IOS_VERSION]
steps:
- uses: actions/checkout@v3
- name: Export environment variables
run: |
export IPHONE_MODEL=$IPHONE_MODEL
export IOS_VERSION=$IOS_VERSION
# ...
# find the full workflow at the end of the article
# ...
android:
runs-on: macos-13
strategy:
matrix:
os: [Android]
emulator_name: [$EMULATOR_NAME]
steps:
- uses: actions/checkout@v3
- name: Add avdmanager and sdkmanager to system PATH
run: |
echo "$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/${{ env.ANDROID_BUILD_TOOLS_VERSION }}" >> $GITHUB_PATH
- name: Install Sdk
run: |
yes Y | sdkmanager --licenses
sdkmanager --install ${ANDROID_SDK_PACKAGES}
- name: Build emulator
# ...
# find the full workflow at the end of the article
# ...
上記の例では、先に説明したIOSワークフローを、以前の記事で説明したAndroidエミュレーターワークフローと統合しています。記事.
これらは、AndroidエミュレーターとiPhoneシミュレーターの両方で必要になる可能性のある推奨構成です。deviceName、platformVersion、およびUUIDはオブジェクトにハードコードされていないことに注意することが重要です。この柔軟性により、必要に応じて異なるバージョンとデバイスモデル間を簡単に切り替えることができます。
const emulator = [{
platformName: 'android',
'appium:options': {
deviceName: process.env.CI ? process.env.EMULATOR_NAME : 'Nexus',
platformVersion: process.env.CI ? process.env.EMULATOR_VERSION : '13',
automationName: 'uiautomator2',
appPackage: 'com.wdiodemoapp',
appWaitPackage: 'com.wdiodemoapp',
appActivity: 'com.wdiodemoapp.MainActivity',
appWaitActivity: 'com.wdiodemoapp.MainActivity',
uiautomator2ServerLaunchTimeout: 200000,
uiautomator2ServerInstallTimeout: 200000,
appWaitForLaunch: true,
autoGrantPermissions: true,
adbExecTimeout: 200000,
androidInstallTimeout: 150000,
ignoreHiddenApiPolicyError: true,
noReset: true,
fullReset: false
}
}]
const simulator = [{
platformName: 'iOS',
'appium:options': {
deviceName: process.env.CI ? process.env.IPHONE_MODEL : 'Iphone-13',
platformVersion: process.env.CI ? process.env.IOS_VERSION : '15.5',
automationName: 'XCUITest',
bundleId: 'org.wdioNativeDemoApp',
app: 'iOS-Simulator-NativeDemoApp-0.4.0.app.zip',
udid: process.env.CI ? process.env.SIMULATOR_UDID : '15A098DB-B8A0-4D6A-9057-23FF1F0F0D9B',
useNewWDA: true,
usePrebuiltWDA: false,
wdaConnectionTimeout: 180000,
appWaitForLaunch: true,
noReset: true,
fullReset: false
}
}]
最初の実行
朗報です。ワークフローは正しく構成されており、IOSアプリのe2eテストが正常に実行されました。
iPhoneシミュレーターのエンドツーエンドテストは合格しましたが、Androidエミュレーターのテストは不安定であることが観察されました。
デバッグ
システムUIクラッシュ
Androidをヘッドレスモードで初めて実行すると、ランダムにシステムUIが応答しなくなる問題が発生することがあります。残念ながら、この問題により、UIシステムが応答しないため、テストを実行できません。その結果、Appiumがアプリと適切にやり取りできなくなります。
この問題は、allureレポートのスクリーンショットを確認したときに確認されました。
これは、ターゲットアプリがバックグラウンドで起動されているにもかかわらず、Appiumが現在の実行中のアクティビティである`.systemui`で目的の要素を見つけようとしているため、ターミナルログにAppiumが要素を見つけられなかったことが表示された理由を説明しています。
これは、Appiumが目的の要素を見つけようとしていますが、現在の実行中のアクティビティは` .systemui`であり、ターゲットアプリはバックグラウンドで起動されているため、理にかなっています。
接続タイムアウト
Appiumによるテスト開始時に、接続の再試行すべてが失敗する事例が複数確認されました。しかし、徹底的な調査の結果、`"./test.apk"` のcapability を使用したAndroidエミュレータへのApkファイルのインストールに異常に時間がかかっていることが判明しました。そのため、インストールの成功を保証するために接続タイムアウトを大幅に延長する必要がありましたが、これは最適な解決策ではありません。
問題とその根本原因を特定したので、これに対処し解決します。
解決策
システムUIクラッシュ
幸いにも、Androidデバイス上で現在実行中のアクティビティをgrepできるという利点を活用できます。この権限により、システムUIや同様のAndroidサービスがクラッシュするのか、正常に機能するのかを検出できます。これは、次のadbシェルコマンドを実行することで実現できます。
adb shell dumpsys window 2>/dev/null | grep -i mCurrentFocus
現在の実装では、Androidデバイスでこの問題が発生した場合の自然な動作を模倣します。具体的には、問題が解決されるまで、ホームボタンを連続してクリックします。問題が解決し、Androidシステムが正常に機能したら、現在実行中のメインアクティビティとして「.NexusLauncherActivity」が表示されると予想されます(「Nexus」はAndroidデバイスを表します)。
これを達成するために、次のシェルスクリプトを作成しました。
#!/bin/bash
function check_current_focus() {
printf "==> Checking emulator running activity \n"
start_time=$(date +%s)
i=0
timeout=20
target="com.google.android.apps.nexuslauncher.NexusLauncherActivity"
while true; do
result=$(adb shell dumpsys window 2>/dev/null | grep -i mCurrentFocus)
if [[ $result == *"$target"* ]]; then
printf "==> Activity is okay: \n"
printf "$result\n"
break
else
adb shell input keyevent KEYCODE_HOME
printf "==> Menu button is pressed \n"
i=$(( (i+1) % 8 ))
fi
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
if [ $elapsed_time -gt $timeout ]; then
printf "==> Timeout after ${timeout} seconds elapsed 🕛.. \n"
return 1
fi
sleep 4
done
}
check_current_focus
上記の関数は連続ループします。メインアクティビティ(NexusLauncherActivity)が見つからない場合、ホームボタンイベントを送信し、見つかるかタイムアウトに達するまでプロセスを繰り返します。
接続タイムアウト
Appiumの接続タイムアウトを大幅に延長するのではなく、APKのインストールをメインアクティビティのチェックと合わせて別々のステップで処理します。
- name: Install APK
run: |
adb install Android-NativeDemoApp-0.4.0.apk
chmod a+x ./mainActivityCheck.sh
./mainActivityCheck.sh
素晴らしい!APKが正しくインストールされ、解決策が正常に実行されました。予想通り、システムUIは応答せず、シェルスクリプトが状況を効果的に管理・処理しました。
ワークフローの最適化と強化
iOS、Android、クロスプラットフォームのいずれでテストを実行する場合でも、プラットフォームの制御を向上させるためにワークフロースパッチを改善しました。
name: Wdio-x-native
on:
workflow_dispatch:
inputs:
e2e:
type: choice
description: Select a platform
required: true
options:
- xplatform
- ios
- android
default: xplatform
したがって、ジョブを調整する必要があります。
name: Wdio-x-native
on:
workflow_dispatch:
inputs:
e2e:
type: choice
description: Select a platform
required: true
options:
- xplatform
- ios
- android
default: xplatform
permissions:
contents: write
pages: write
id-token: write
env:
IPHONE_MODEL: iPhone 8
IOS_VERSION: 16.2
API_LEVEL: 32
EMULATOR_NAME: Nexus
EMULATOR_DEVICE: Nexus 5
EMULATOR_VERSION: 12
ANDROID_ARCH: x86_64
ANDROID_TARGET: google_apis
ANDROID_BUILD_TOOLS_VERSION: 34.0.0-rc4
ANDROID_SDK_PACKAGES: system-images;android-32;google_apis;x86_64 platforms;android-32 build-tools;34.0.0-rc4 platform-tools emulator
EMULATOR_TIMEOUT: 350
BOOT_TIMEOUT: 700
jobs:
ios:
runs-on:
- macos-13
if: ${{ contains(github.event.inputs.e2e, 'ios') || contains(github.event.inputs.e2e, 'xplatform') }}
strategy:
matrix:
os: [IOS]
device: [$IPHONE_MODEL]
version: [$IOS_VERSION]
steps:
- uses: actions/checkout@v3
# find the full workflow at the end of the article
android:
runs-on: macos-13
if: ${{ contains(github.event.inputs.e2e, 'android') || contains(github.event.inputs.e2e, 'xplatform') }}
strategy:
matrix:
os: [Android]
emulator_name: [$EMULATOR_NAME]
steps:
- uses: actions/checkout@v3
# find the full workflow at the end of the article
最後に、レポートを生成し、すぐにGitHubページにデプロイします。
- name: Generate report
if: always()
run: |
npx allure generate report/allure-results
- name: Setup Pages
if: always()
uses: actions/configure-pages@v3
- name: Upload artifact
if: always()
uses: actions/upload-pages-artifact@v1
with:
path: './allure-report'
- name: Deploy to GitHub Pages
if: always()
id: deployment
uses: actions/deploy-pages@v2
ワークフローの実行
素晴らしいニュースです!ワークフローは完全に機能し、完全な安定性を示しています。ワークフローは、AndroidやiOSなど単一のプラットフォームに対してトリガーすることも、両方のプラットフォームに対して同時に並列でトリガーすることもできます。
loading...
結論
GitHub Actionsによって提供されるAndroidとiOSの両方のすぐに使えるSDKを活用することで、大きなメリットが得られます。これにより、費用をかけずに、モバイルデバイスファームのクラウドサービスに依存することなく、効率的なエンドツーエンドテストパイプラインを構築できます。特にAndroidでは実機でのテストが好ましいですが、このアプローチは無料であるため、満足のいく妥協点となります。
本稿では、ネイティブモバイルアプリ向けにGitHub Actionsパイプラインを使用してクロスプラットフォームのエンドツーエンドテストを構築する手順を段階的に説明しました。さまざまな課題、障害、問題に対処し、プロセス全体を徹底的に理解できるようにしました。この知識があれば、独自の要件に合わせてカスタマイズされたパイプラインを構築することが容易になります。