本記事は、ソフトウェアテストの小ネタ Advent Calendar 2023 20日目の記事です。
概要
AWS Device FarmでAppiumをPytestでE2Eテストするにあたって基本的な情報をまとめています。
導入
まずは、一般的な使い方やハマりやすいポイントを解説します。
Console
・プロジェクトを用意後に新しく実行を設定する際に新たにアプリファイルをアップロードかすでにアップロード済みのアプリを指定
・テストの種類を指定するセレクトから Appium Python
を選んで公式にある通り作成した test_bundle.zip
をアップロードかすでにアップロード済みのZipファイルを指定
・TestSpecの設定はConfigure画面上で表示されているテキストを編集しても適応されないためEdit
ボタンからEdit your YAML
上で編集してSave as New
ボタンで保存してから適応
・テスト対象のデバイスの指定はデフォルトでも問題ないがOSのバージョンを別々に実行したいテストや特定のデバイスを限定して実行したいテストがある場合は前もってdevice pool
を作成して実行する
・デフォルトのTestSpecに設定されている$DEVICEFARM_LOG_DIR
にテストスクリプトで生成したログやスクショのファイルを保存しておくと実行履歴からテストのFilesタブにあるCustomer Artifacts
のリンクからファイルをダウンロードすることができる
requirements.txt
公式で
WorkSpace で次のコマンドを実行して、requirements.txt ファイルを生成します。
$ pip freeze > requirements.txt
と解説があるものの補足がないため初見ではわかりにくい。
すでにローカルでAppiumをPytestで実装済みであれば、このコマンドでインストール済みのpip群を出力できるもののDevice Farmがサポートする環境に適していないpipやバージョンが多く、例えば以下のようなエラーが発生する。
- ERROR: Could not find a version that satisfies the requirement numpy==1.22.3
- ERROR: Failed building wheel for PyNaCl
- CMake Error: CMake was unable to find a build program corresponding to "Ninja". CMAKE_MAKE_PROGRAM is not set. You probably need to select a different build tool.
そのため必要最低限のpip群だけ記述したrequirements.txt
を出力するため、PytyonのDockerコンテナのイメージを作ってAppium-Python-Client
をインストールした余計なものが入っていない環境下でpip freeze > requirements.txt
を実行してDevice Farmにアップロードしたところ、Device Farm側で用意されるiOS向けの環境では問題なかったもののAndroid向けの環境では上記のCMake Error
が起きて、このエラーの解消はどうしても対応できませんでした。
結論としては、pip freeze > requirements.txt
はやらずにPytestのコード上でimportしているライブラリのみ指定すればよく、あとはよしなにDevice Farm側で必要なpip群を自動でインストールしてくれる仕組みになっている。
そのため、Appiumのみであれば
Appium-Python-Client==2.6.0
pytest==7.1.2
を記載(バージョンはよしなに調整)したrequirements.txt
をアップロードすれば、問題なくPytestでAppiumのテストがiOS/Androidの両方で問題なく正常動作しました。
test_bundle.zip
の作り方
応用
ここからは、Device Farmを活用したトピックを解説します。
Airtest
ネイティブアプリだけでなくUnityやCocos2dxなどの3DCG APIを用いて作られたアプリにもE2EテストができるAirtestをDevice FarmのAppiumサポートを通して実行することもできました。
Android
参考にした上記のアカツキさんの記事のまま対応してみたところ、TestSpecの$adb_path=$(which adb)
で
[DeviceFarm] $adb_path=$(which adb)
/tmp/scratchKdJ6cj.scratch/shell-script-ZUpYYv/shell_script.sh: 125: /tmp/scratchKdJ6cj.scratch/shell-script-ZUpYYv/shell_script.sh: =/opt/dev/android-sdk-linux/platform-tools/adb: not found
とエラーになってしまったため、確実に取得できるよう以下のように調整することで回避できました。
- ls -l ./lib/python3.7/site-packages/airtest/core/android/static/
- rm ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb
- adb_path=`which adb`
- echo $adb_path # 確認できるよう念のため出力しておく
- cp $adb_path ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb
- sed -i -e s#^\$ADB#$adb_path.orig#g ./lib/python3.7/site-packages/airtest/core/android/static/adb/linux/adb
iOS
Airtestの公式には詳細がないため、iOSサポートの実装が似ているAltUnityTesterの公式ドキュメント
で解説されている内容がAirtestでもおそらく同様の問題で対応できない模様。
We encountered some problems forwarding the port on iOS devices. This why we only talk about running tests on Android devices. We will update this page and the sample project once we have a solution for iOS.
[翻訳: DeepL]
iOSデバイスでポートの転送に問題が発生しました。このため、Androidデバイスでのテスト実行についてのみ説明しています。iOS用のソリューションができ次第、このページとサンプルプロジェクトを更新します。
そのサンプルプロジェクト
を確認するとローカルで実装する際に参考になるサンプルは豊富で
のプロジェクトでDevice Farmのサンプルとしては不足していてこの内容では実装できなかった。
ただし
などのサンプルでシェルスクリプトでの実行方法や
iproxy 13000 13000 $DEVICE_UDID &
でiproxy
をデーモン起動できることなどが知れたので参考になる情報が多くありました。
AWS CLI: $ aws devicefarm
こちらの記事で、device farmのコマンドについてわかりやすくまとめられていてとても参考になった。
$ aws devicefarm get-run
でコンソール上から操作して実行したテストのリストを取得して実行時間をざっと算出したが、なぜかAn error occurred (ArgumentException) when calling the GetRun operation: Invalid arn ...
になってしまってできなかったため残念・・・
テストの実行
schedule-run
でテストを実行することができる。
すでにアップロードしているファイルを指定するケースの場合、以下のように各arnを取得してJsonを準備して--cli-input-json
オプションにセットする。
# すでにアップロードしているアプリファイルのarnを取得する
% aws devicefarm list-uploads --arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) --region=us-west-2 --type=IOS_APP
{
"uploads": [
{
"arn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(APP_UPLOAD_ID)",
"name": "PageBasedSample.ipa",
"created": "2022-10-04T17:23:33.510000+09:00",
"type": "IOS_APP",
"status": "SUCCEEDED",
"url": "https://prod-us-west-2-uploads.s3-us-west-2.amazonaws.com/arn%3Aaws%3Adevicefarm%3Aus-west-2%3A$(ACCOUNT_ID)%3Aproject%3A$(PROJECT_ID)/uploads/arn%3Aaws%3Adevicefarm%3Aus-west-2%3A$(ACCOUNT_ID)%3Aupload%3A$(PROJECT_ID)/$(APP_UPLOAD_ID)/PageBasedSample.ipa?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20221011T043929Z&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Expires=86400&X-Amz-Credential=AKIAUJHLTYS5AWNTRO6L%2F20221011%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Signature=$(SIGNATURE_ID)",
"metadata": "{\"activity_name\":\"\",\"minimum_arm\":6,\"error_type\":null,\"package_name\":\"net.gremito.app.ios.PageBasedSample\",\"sdk_version\":1210,\"files\":{},\"warning_type\":null,\"supported_os\":\"12.1\",\"executable\":\"PageBasedSample\",\"platform\":[\"iPhoneOS\"],\"form_factor\":[1,2]}",
"contentType": "application/octet-stream",
"category": "PRIVATE"
}
]
}
# すでにアップロードしている`test_bundle.zip`のarnを取得する
% aws devicefarm list-uploads --arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) --region=us-west-2 --type=APPIUM_PYTHON_TEST_PACKAGE
{
"uploads": [
{
"arn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(TEST_PACKAGE_UPLOAD_ID)",
"name": "test_bundle.zip",
"created": "2022-10-04T17:23:41.907000+09:00",
"type": "APPIUM_PYTHON_TEST_PACKAGE",
"status": "SUCCEEDED",
"url": "https://prod-us-west-2-uploads.s3-us-west-2.amazonaws.com/arn%3Aaws%3Adevicefarm%3Aus-west-2%3A$(ACCOUNT_ID)%3Aproject%3A$(PROJECT_ID)/uploads/arn%3Aaws%3Adevicefarm%3Aus-west-2%3A$(ACCOUNT_ID)%3Aupload%3A$(PROJECT_ID)/$(TEST_PACKAGE_UPLOAD_ID)/test_bundle.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20221011T043947Z&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Expires=86400&X-Amz-Credential=AKIAUJHLTYS5AWNTRO6L%2F20221011%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Signature=$(SIGNATURE_ID)",
"metadata": "{\"valid\":true}",
"contentType": "application/octet-stream",
"category": "PRIVATE"
}
]
}
# すでにアップロードしているTestSpecのarnを取得する。
% aws devicefarm list-uploads --arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) --region=us-west-2 --type=APPIUM_PYTHON_TEST_SPEC
{
"uploads": [
...
{
"arn": "arn:aws:devicefarm:us-west-2::upload:$(TEST_SPEC_UPLOAD_ID)",
"name": "TestSpec v7.0 for iOS Appium Python (sets Python version 3 as the default)",
"created": "2021-10-26T00:37:02.386000+09:00",
"type": "APPIUM_PYTHON_TEST_SPEC",
"status": "SUCCEEDED",
"url": "https://prod-us-west-2-uploads-testspec.s3-us-west-2.amazonaws.com/public-yaml-files/appium_119_python_3_ios.yml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20221011T044001Z&X-Amz-SignedHeaders=host&X-Amz-Expires=86400&X-Amz-Credential=AKIAUJHLTYS5AWNTRO6L%2F20221011%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Signature=$(SIGNATURE_ID)",
"category": "CURATED"
},
...
]
}
# すでに作成しているDevice Poolのarnを取得する
% aws devicefarm list-device-pools --arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) --region=us-west-2
{
"devicePools": [
...
{
"arn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):devicepool:$(PROJECT_ID)/$(DEVICEPOOL_ID)",
"name": "iphones",
"type": "PRIVATE",
"rules": [
{
"attribute": "ARN",
"operator": "IN",
"value": "[\"arn:aws:devicefarm:us-west-2::device:$(DEVICE_ID)\"]"
}
]
}
]
}
# `schedule-run`にセットするJsonを作成する
% cat <<EOF > ~/execTest.json
> {
"projectArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID)",
"appArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(APP_UPLOAD_ID)",
"devicePoolArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):devicepool:$(PROJECT_ID)/$(DEVICE_POOL_ID)",
"name": "202210111605",
"test": {
"type": "APPIUM_PYTHON",
"testPackageArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(TEST_PACKAGE_UPLOAD_ID)",
"testSpecArn": "arn:aws:devicefarm:us-west-2::upload:$(TEST_SPEC_UPLOAD_ID)"
},
"configuration": {
"locale": "ja_JP"
}
}
EOF
# テストを実行する
% aws devicefarm schedule-run --region=us-west-2 --cli-input-json file:///Users/gremito/execTest.json
{
"run": {
"arn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):run:$(PROJECT_ID)/$(RUN_ID)",
"name": "202210111605",
"type": "APPIUM_PYTHON",
"platform": "IOS_APP",
"created": "2022-10-11T16:06:44.681000+09:00",
"status": "SCHEDULING",
"result": "PENDING",
"started": "2022-10-11T16:06:44.681000+09:00",
"counters": {
"total": 0,
"passed": 0,
"failed": 0,
"warned": 0,
"errored": 0,
"stopped": 0,
"skipped": 0
},
"totalJobs": 1,
"completedJobs": 0,
"billingMethod": "METERED",
"appUpload": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(APP_UPLOAD_ID)",
"jobTimeoutMinutes": 150,
"devicePoolArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):devicepool:$(PROJECT_ID)/$(DEVICEPOOL_ID)",
"locale": "ja_JP",
"radios": {
"wifi": true,
"bluetooth": false,
"nfc": true,
"gps": true
},
"testSpecArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(TEST_SPEC_UPLOAD_ID)"
}
}
テスト実行結果の取得
get-run
は単一のテスト実行結果の詳細を取得したいときのAPIで、実行結果の一覧を取得するAPIはlist-runs
を指定することで取得できた。
% aws devicefarm list-runs --arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) --region=us-west-2
{
"runs": [
{
"arn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):run:$(PROJECT_ID)/$(RUN_ID)",
"name": "202210111605",
"type": "APPIUM_PYTHON",
"platform": "IOS_APP",
"created": "2022-10-11T16:06:44.681000+09:00",
"status": "COMPLETED",
"result": "FAILED",
"started": "2022-10-11T16:06:44.681000+09:00",
"stopped": "2022-10-11T16:20:14.277000+09:00",
"counters": {
"total": 3,
"passed": 2,
"failed": 1,
"warned": 0,
"errored": 0,
"stopped": 0,
"skipped": 0
},
"totalJobs": 1,
"completedJobs": 1,
"billingMethod": "METERED",
"deviceMinutes": {
"total": 11.22,
"metered": 4.49,
"unmetered": 0.0
},
"appUpload": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(APP_UPLOAD_ID)",
"jobTimeoutMinutes": 150,
"devicePoolArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):devicepool:$(PROJECT_ID)/$(DEVICEPOOL_ID)",
"locale": "ja_JP",
"radios": {
"wifi": true,
"bluetooth": false,
"nfc": true,
"gps": true
},
"testSpecArn": "arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(TEST_SPEC_UPLOAD_ID)"
},
{
...
Jnekins CI
上記の記事などを参考に次の設定でJenkinsからDvice Farmと連携してCI対応できる。
- Device Farmのアクセスを許可するIAM(Identity and Access Management)を準備
- JnekinsでDevice Farmのプラグインをインストール
- 設定画面に入るとDevice Farmの設定フォームが追加されているため、ダウンロードした
user_credentials.csv
からAKID(Access key ID)とSKID(Secret access key ID)を取得してその設定フォームに記入する - ジョブの設定から作成済みのDevice Farmプロジェクトを選択できるようになる。
注意1: セキュリティ問題
やJnekins上で警告が出ているとおり、現在2022/10/01時点でDevice FarmのJnekinsプラグインがunresolved security vulnerability affecting
となっているため、使用する際には十分検討すること。
対策としては、Jenkinsを起動しているマシンにアクセス制限をかけたセキュアなネットワーク環境にする。
または、一旦プラグインの使用を止めてAWS CLIをJnekinsマシンにインストールして、ジョブのビルド設定でシェルの実行からaws devicefarmを使って連携することでセキュアなCI対応ができる。
注意2: Locale設定問題
Device FarmのコンソールからだとDevice Locale(端末の国籍設定)を変更できるもののJenkinsプラグインの設定ではLocaleの変更が用意されていない。
上記のIssueがあったため試しに手元にMavenビルドできる環境を準備してhardcodeされてる箇所を変更して、make clean compile
してできたプラグインをJenkinsに入れて使ってみても変更されなかった。
そのためデフォルトのen_US
以外を設定してテストを実行したい場合は、プラグインの使用をやめてシェルスクリプトで実行に変更して、上記の$ aws devicefarm schedule-run
を用いることで実現できる。
が、アプリファイル・test_bundle.zip
・TestSpecをアップロードするcreate-upload
の実行設定とそのレスポンスに入っているarnを以下のように取得して、schedule-run
で指定するJsonに設定するshell操作が必要になるため少し手間がかかる。
% aws devicefarm create-upload \
--region=us-west-2 \
--project-arn arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):project:$(PROJECT_ID) \
--name build/PageBasedSample.ipa \
--type IOS_APP \
| jq '.upload.arn'
"arn:aws:devicefarm:us-west-2:$(ACCOUNT_ID):upload:$(PROJECT_ID)/$(IOS_APP_UPLOAD_ID)"
料金
7, 8月で多く使用したことで途中から1000分無料枠を使い切って従量課金対象に切り替わったのでざっくり計算してみた。今回の他に前にもDevice Farmを別件で少し使ったことがあるため誤差はその分なのか。
Airtestの対応でテストが正常に終わらず実行前に設定したタイムアウトも機能せず実行されたままの状態になるケースがあり、その場合無駄に使用時間が増えて料金も増えてしまうことがあった。
また、device poolで設定したデバイス数 × 1テスト実行の使用時間が計算されるため、調査や初回の導入対応の際は1,2つのデバイス数を設定とよいだろう。
従量課金に切り替わった次の月でメンテナンスのため少し使用してみたところ上記のような料金結果になっていた。
使用時間が約5分近いものの実際には約3分使用した分の料金が計算されており、おそらく実行したテストの時間分ではなく、そのテスト内でデバイスを使用した時間を裏でカウントされたものが約3分だったのではないかと考えられる。