はじめに
AWSのDevice Farmを使ってE2Eテストを行うようにしました。
このテストは「実際のモバイルデバイス」1で行うことができるため、問題の再現性や問題がないことの確認を実機レベルで確認することができます。便利ですね。
準備
Device FarmとAppiumについて
Device FarmはAWSが提供している実機テストを行うためのサービスです。Device Farmを使うことで実機(とデスクトップも)でE2Eテストを行うことができます。
E2Eテスト自体は様々なテストフレームワークを利用できます。JUnitやRspecといったメジャーなものから、JS系も利用が可能です。
今回はNode.js製のテストフレームワークであるAppiumを利用します。
ビルドとテストコードについて
今回は「E2EテストをCIに組み込む」ことに焦点を当てているため、ビルドについては行っていません。愚直な方法ですが、ビルド済みバイナリをリポジトリに入れ、そのファイルを更新することでCIに取り入れる方法をとっています。(今後はアプリのビルドも是非対応していきたいです。)
Device Farmの用語について
Device FarmをAWS-CLIで動作させるために用語を理解する必要があります。
用語 | 意味 |
---|---|
プロジェクト | E2Eを動かすためのプロジェクトの単位。ARNを取得できる。 |
デバイスプール | テストを行うモバイル実機群。 |
ラン run | 実行されたテスト。 |
アップロード | テストコードやアプリ本体のapkファイルなど。 |
AWSのコンソールで手動動作させる
まずはDevice Farm上でテストを動作させます。
下記の記事に沿ってAppiumでテストを走らせてみます。
流れとしては下記の通りです。
- デバイスプールの作成
- アプリ本体のアップロード
- テストフレームワーク(Appiumu Node.js)の選択
- テストファイルをアップロード
- テスト実行用YAMLファイルの書き換え
- テストの実行
上記が問題なく実行されるとモバイル実機の用意が始まります。
動作についてはコンソール上で実機の画面を確認することができるため、どのように進んでいるか確認することができます。
AWS-CLIで動作を確認する
この実機テストを自動化するために、AWS-CLIで動作できるようにします。
(恐らく)Device Farmのすべての動作をAWS-CLIで行うことができますが、「CIとして組み込む」という観点から下記のようにCLIでの動作を行います。
CLIで行うこと
- アプリ本体のファイルアップロード
- テストファイルのアップロード
- テストの実行(スケジュール)
CLIで行わないこと
- プロジェクトの作成
- デバイスプールの作成
プロジェクトとデバイスプールについては作成、変更の頻度が少ないためマネジメントコンソールから設定するようにします。
CLI実行のための準備
AWS-CLIでの実行時によく使う変数を用意しておくと便利です。Device Farmはus-west-2でしか提供されていないため、日本からはap-northest-1を普段は利用していますが、us-west-2の指定が必要です。
また上述のようにプロジェクトは一度作成したらそのまま利用するため、そのARNを変数に取っておきます。
$ export AWSCLI_OPTIONS='--region=us-west-2'
$ export PROJECT_ARN="arn:aws:devicefarm:us-west-2:210389843340:project:3302xxxx-0000-0000-0000-xxxxxxxxxxxxxx"
AWSCLI_OPTIONS
は必要に応じてprofileの指定を入れておきます。
$ export AWSCLI_OPTIONS='--region=us-west-2 --profile=yousan@l2tp.org'
うまく実行できるかコマンドのテストをしておきます。
$ aws ${AWSCLI_OPTIONS} devicefarm list-runs --arn ${PROJECT_ARN}
オプションが正しく設定されていれば手動で動作させたテストの結果が出力されます。
ついでにその手動で実行した過去のテスト結果を取り出してみます。時間と結果を取り出してみます。
$ aws ${AWSCLI_OPTIONS} devicefarm list-runs --arn ${PROJECT_ARN} | jq '.runs | map({created, result})'
[
{
"created": 1577439729.014,
"result": "FAILED"
},
{
"created": 1577439320.224,
"result": "FAILED"
},
{
"created": 1577438701.948,
"result": "FAILED"
},
{
"created": 1577178528.088,
"result": "FAILED"
},
{
"created": 1577178429.833,
"result": "STOPPED"
},
...
ファイルのアップロード
AWS Device Farmのファイルアップロードはその仕組みにクセがあり、なれるまで大変でした。
ファイルアップロードのポイントです。
- アプリ本体、テストファイルの同じ一つの「アップロード」として扱われる
- AWS-CLIで「このファイルをアップロードする」(アップロードを作成する)とし、アップロード用のURLを発行する
手順を確認します。
アップロードの作成
Androidアプリのアップロードを作成します。
$ aws ${AWSCLI_OPTIONS} devicefarm create-upload --project-arn ${PROJECT_ARN} --name test.apk --type ANDOID_APP
結果がJSONで返却されます。
{
"upload": {
"arn": "arn:aws:devicefarm:us-west-2:210389843340:upload:xxxxxxxxxxxxxx",
"name": "test.apk",
"type": "ANDROID_APP",
"status": "INITIALIZED",
"url": "https://prod-us-west-2-uploads.s3-us-west-2.amazonaws.com/arnxxxxxxxxxxxxxxxxxx",
"category": "PRIVATE"
}
}
ARNとURLを利用したいので、アップロードを扱うときにはcreate-upload
した結果をファイルに保存しておくと良いです。下記ではupload.json
に保存しています。
$ aws ${AWSCLI_OPTIONS} devicefarm create-upload --project-arn ${PROJECT_ARN} --name test.apk --type ANDOID_APP > upload.json
この保存されたURLに対してファイルをアップロードします。
jq -r .upload.url < upload.json
でURLを取得し、そこに対してファイルをアップロードします。
$ curl -T test.apk $(jq -r .upload.url < upload.json )
アップロードが正常に終了しているかの確認はget-upload
を利用します。
$ aws ${AWSCLI_OPTIONS} devicefarm get-upload --arn $(jq -r .upload.arn < bin/app_upload.json )
status
がSUCCEEDED
になっていれば成功です。
$ aws ${AWSCLI_OPTIONS} devicefarm get-upload --arn $(jq -r .upload.arn < upload.json ) | jq .upload.status
"SUCCEEDED"
スクリプト化する
CIとして組み込むため、上記のCLIコマンドを元にスクリプト化を行います。
色々あるのですが出来上がったスクリプトです。
#!/bin/bash
# CLIでの実行方法について @see https://docs.aws.amazon.com/devicefarm/latest/developerguide/how-to-create-test-run.html
# チュートリアル @see https://aws.amazon.com/jp/blogs/mobile/testing-mobile-apps-across-hundreds-of-real-devices-with-appium-node-js-and-aws-device-farm/
# @see https://qiita.com/VA_nakatsu/items/d74814cfcbd8975cbb11
set -xe # エラーがあったら止める
cd -- "$(dirname "$BASH_SOURCE")" # スクリプトがある場所に移動する
# 1. 事前準備
CLI_OPTION=" --region=us-west-2 "
NOW=$(date +%Y%m%d%H%M%S)
# リポジトリで固定されているARN
PROJECT_ARN="arn:aws:devicefarm:us-west-2:210389843340:project:xxxx"
TEST_YAML_ARN="arn:aws:devicefarm:us-west-2:210389843340:upload:xxxx"
# Top devices(3台)
# DEVICE_POOL_ARN="arn:aws:devicefarm:us-west-2::devicepool:xxxxx"
# Only 1 device (Pixel 3のみ)(節約用)
DEVICE_POOL_ARN="arn:aws:devicefarm:us-west-2:210389843340:devicepool:xxxx"
# デプロイごとに変わるもの(アプリ、テストコード)(下記は成功した際のサンプルARN)
# TEST_PACKAGE_ARN="arn:aws:devicefarm:us-west-2:210389843340:upload:xxxx"
# APP_ARN="arn:aws:devicefarm:us-west-2:210389843340:upload:xxxx"
# 2. アプリバイナリのアップロード準備
# アップロードするとファイルの識別ができなくなるため、ファイルに日付を付けた名前としてコピー e.g. test_20191224011223.apk
#cd ../ && cp test.apk tmp/test_${NOW}.apk && cd bin/
aws ${CLI_OPTION} devicefarm create-upload \
--project-arn ${PROJECT_ARN} \
--name test_${NOW}.apk --type ANDROID_APP > ../tmp/app_upload.json
# アップロード用のURLを取得
APP_UPLOAD_URL=$(jq -r .upload.url < ../tmp/app_upload.json)
# アプリケーションバイナリのアップロード
APP_UPLOAD_ARN=$(jq -r .upload.arn < ../tmp/app_upload.json)
# 確認するには下記のコマンドを実行
# aws ${CLI_OPTION} devicefarm get-upload --arn $(jq -r .upload.arn < tmp/app_upload.json)
# 3. テストコードのアップロード準備
cd ../
rm -f MyTest.zip; # 前処理としてファイルを消しておく。消さないと二重でbundleされてtgzのサイズが大きくなる
# npm-bundleが作成するpachage.jsonにあるパッケージ名のtgzファイルを消す e.g. cc_e2etest-1.0.0.tgz
rm -f $(node -p "try { require('../package.json').name } catch(e) {}")*.tgz
# npm-bundleで現在ディレクトリ(appium/)をtgzにまとめ、圧縮形式をzipに変更する
$(npm bin)/npm-bundle && zip -r MyTest.zip *.tgz
cd bin/
# アップロードするとファイルの識別ができなくなるため、ファイルに日付を付けた名前としてコピー e.g. test_20191224011223.apk
#cd ../ && cp MyTest.zip tmp/MyTest_${NOW}.apk && cd bin/
# アップロード準備
aws ${CLI_OPTION} devicefarm create-upload \
--project-arn ${PROJECT_ARN} \
--name MyTest_${NOW}.zip --type APPIUM_NODE_TEST_PACKAGE > ../tmp/testpackage_upload.json
# アップロード用のURLを取得
TEST_PACKAGE_UPLOAD_URL=$(jq -r .upload.url < ../tmp/testpackage_upload.json)
# アプリケーションバイナリのアップロード
TEST_PACKAGE_UPLOAD_ARN=$(jq -r .upload.arn < ../tmp/testpackage_upload.json)
# 確認するには下記のコマンドを実行
# aws ${CLI_OPTION} devicefarm get-upload --arn $(jq -r .upload.arn < tmp/testpackage_upload.json)
# 4. 並列でアップロード
curl -T ../test.apk ${APP_UPLOAD_URL} &
curl -T ../MyTest.zip ${TEST_PACKAGE_UPLOAD_URL} &
wait; # アップロードが終わるのを待つ
sleep 3; # アップロードが正常に完了していないことがあるため、3秒待つ
# 5. runをスケジュール予約する
aws ${CLI_OPTION} devicefarm schedule-run --project-arn ${PROJECT_ARN} \
--app-arn ${APP_UPLOAD_ARN} \
--device-pool-arn ${DEVICE_POOL_ARN} \
--name test_${NOW} \
--test type=APPIUM_NODE,testSpecArn=${TEST_YAML_ARN},testPackageArn=${TEST_PACKAGE_UPLOAD_ARN}
上記のスクリプトは下記のような流れです。
- 事前準備
- アプリバイナリのアップロード準備
- テストコードのアップロード準備
- 並列でアップロード
- runをスケジュール予約する
ファイルのアップロードに時間がかかるため、先にアップロードを準備しておき、平行してアップロードします。こうすることでCIの実行時間を短縮することができます。
上記のスクリプトが手元で正しく動作することを確認しておきます。
GitとCircleCIに組み込む
スクリプトが正しく動くようになったら上記のファイル群をCircleCIに組み込みます。
上記のファイルなどはappium
ディレクトリに入れています。
$ tree
├── MyTest.zip
├── README.md
├── bin
│ └── e2e.sh
├── test.apk
├── e2e_android.js
├── node_modules/
├── package-lock.json
├── package.json
...略
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2.1
jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:8.15
working_directory: ~/repo
steps:
- checkout
# Install jq and aws cli to parse AWS outputs in api/bin/deploy_branch.sh
- run:
name: Set PATH
command: |
echo 'export PATH=$PATH:/home/circleci/.local/bin/' >> $BASH_ENV
source /home/circleci/.bashrc
- run:
name: install jq
command: |
sudo sh -c 'echo "deb http://deb.debian.org/debian jessie main" > /etc/apt/sources.list'
sudo sh -c 'echo "deb http://security.debian.org jessie/updates main" >> /etc/apt/sources.list'
sudo apt-get update && sudo apt-get install -y jq
- run:
# ref https://github.com/TheDesignium/LD_Satellite/blob/develop/.circleci/config.yml
name: install awscli
command: |
sudo apt-get -y -qq install python3
curl -O https://bootstrap.pypa.io/get-pip.py
python3 get-pip.py --user
pip install awscli --upgrade --user
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: setting npm config
command: npm config set "@fortawesome:registry" https://npm.fontawesome.com/ && npm config set "//npm.fontawesome.com/:_authToken" $FONTAWESOME_TOKEN
- run: npm config get
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
# Appium用のnpm install
- restore_cache:
keys:
- v1-dependencies-{{ checksum "appium/package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: cd appium; npm install
- save_cache:
paths:
- appium/node_modules
key: v1-dependencies-{{ checksum "appium/package.json" }}
# run tests!
- run: yarn test
- run: cd appium; $(npm bin)/sls config credentials -k ${AWS_ACCESS_KEY_ID} -s ${AWS_SECRET_ACCESS_KEY} --provider aws -n default
- run: appium/bin/e2e.sh
これでコミットごとにE2Eテストが走るようになりました!
まとめと所感
Device FarmでAppiumのテストが通るようになったため、面倒な実機テストを自動化することができました。
実はローカルではAndroidエミュレータでAppiumを動かし、その後実機でテストを行いました。実機ではエラーになってしまいました。
このエラーがDevice Farmでは検出することができました。Device Farmが実機を謳っているだけあり、そういったエラーを早期に検出できそうです。
またアプリのビルド、iOSへの対応については未実装なので進めていきたいです。
謝辞
Appiumの導入についてはインターン生の曽我くんが作成を行い、Device Farmの導入では橋本くんの残した検討資料を参考にしました。ありがとうございます!
参考記事、サイト
公式サイトでのチュートリアル。手順通りに進めることができ、思ったより簡単でおすすめです。
[AWS] Device FarmをCLIで実行してみた - Qiita
Device Farmでリモート操作できるようになりました。 - Qiita