AWS Device Farm、Appium、CircleCIでモバイルアプリの実機E2EテストをCIとして回す

# はじめに
AWSのDevice Farmを使ってE2Eテストを行うようにしました。
このテストは「実際のモバイルデバイス」[^aws1]で行うことができるため、問題の再現性や問題がないことの確認を実機レベルで確認することができます。便利ですね。

[^aws1]: https://aws.amazon.com/jp/device-farm/
## 準備
[AWS-CLI](https://aws.amazon.com/jp/cli/)の他に[jq](http://stedolan.github.io/jq/)をインストールしておくと良いです。

参考: [jq コマンドを使う日常のご紹介 - Qiita](https://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af "jq コマンドを使う日常のご紹介 - Qiita")

# Device FarmとAppiumについて
Device FarmはAWSが提供している実機テストを行うためのサービスです。Device Farmを使うことで実機（とデスクトップも）でE2Eテストを行うことができます。
E2Eテスト自体は様々なテストフレームワークを利用できます。JUnitやRspecといったメジャーなものから、JS系も利用が可能です。
今回はNode.js製のテストフレームワークである[Appium](http://appium.io/)を利用します。

## ビルドとテストコードについて

今回は「E2EテストをCIに組み込む」ことに焦点を当てているため、ビルドについては行っていません。愚直な方法ですが、ビルド済みバイナリをリポジトリに入れ、そのファイルを更新することでCIに取り入れる方法をとっています。（今後はアプリのビルドも是非対応していきたいです。）

## Device Farmの用語について
Device FarmをAWS-CLIで動作させるために用語を理解する必要があります。

用語 | 意味
---|---
プロジェクト | E2Eを動かすためのプロジェクトの単位。ARNを取得できる。
デバイスプール | テストを行うモバイル実機群。
ラン run | 実行されたテスト。
アップロード | テストコードやアプリ本体のapkファイルなど。


# AWSのコンソールで手動動作させる

まずはDevice Farm上でテストを動作させます。
下記の記事に沿ってAppiumでテストを走らせてみます。

[Testing mobile apps across hundreds of real devices with Appium, Node.js, and AWS Device Farm | AWS Mobile Blog](https://aws.amazon.com/jp/blogs/mobile/testing-mobile-apps-across-hundreds-of-real-devices-with-appium-node-js-and-aws-device-farm/ "Testing mobile apps across hundreds of real devices with Appium, Node.js, and AWS Device Farm | AWS Mobile Blog")

流れとしては下記の通りです。

1. デバイスプールの作成
2. アプリ本体のアップロード
3. テストフレームワーク（Appiumu Node.js）の選択
4. テストファイルをアップロード
5. テスト実行用YAMLファイルの書き換え
6. テストの実行

上記が問題なく実行されるとモバイル実機の用意が始まります。
動作についてはコンソール上で実機の画面を確認することができるため、どのように進んでいるか確認することができます。

# 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を変数に取っておきます。

```bash
$ 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の指定を入れておきます。

```bash
$ export AWSCLI_OPTIONS='--region=us-west-2 --profile=yousan@l2tp.org'
```


うまく実行できるかコマンドのテストをしておきます。

```bash
$ aws ${AWSCLI_OPTIONS} devicefarm list-runs --arn ${PROJECT_ARN}
```

オプションが正しく設定されていれば手動で動作させたテストの結果が出力されます。

ついでにその手動で実行した過去のテスト結果を取り出してみます。時間と結果を取り出してみます。

```bash
$ aws ${AWSCLI_OPTIONS} devicefarm list-runs --arn ${PROJECT_ARN} | jq '.runs | map({created, result})'
```

```json
[
  {
    "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"
  },
...
```

[jq コマンドを使う日常のご紹介 - Qiita](https://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af#map "jq コマンドを使う日常のご紹介 - Qiita")

## ファイルのアップロード
AWS Device Farmのファイルアップロードはその仕組みにクセがあり、なれるまで大変でした。

ファイルアップロードのポイントです。

- アプリ本体、テストファイルの同じ一つの「アップロード」として扱われる
- AWS-CLIで「このファイルをアップロードする」（アップロードを作成する）とし、アップロード用のURLを発行する

手順を確認します。

### アップロードの作成

Androidアプリのアップロードを作成します。

```bash
$ aws ${AWSCLI_OPTIONS} devicefarm create-upload --project-arn ${PROJECT_ARN} --name test.apk --type ANDOID_APP
```

結果がJSONで返却されます。

```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`に保存しています。

```bash
$ 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を取得し、そこに対してファイルをアップロードします。

```bash
$ curl -T test.apk $(jq -r .upload.url <  upload.json )
```

アップロードが正常に終了しているかの確認は`get-upload`を利用します。

```bash
$ aws ${AWSCLI_OPTIONS} devicefarm get-upload --arn $(jq -r .upload.arn <  bin/app_upload.json )
```

`status`が`SUCCEEDED`になっていれば成功です。

```bash
$ aws ${AWSCLI_OPTIONS} devicefarm get-upload --arn $(jq -r .upload.arn <  upload.json ) | jq .upload.status

"SUCCEEDED"
```

# スクリプト化する

CIとして組み込むため、上記のCLIコマンドを元にスクリプト化を行います。

色々あるのですが出来上がったスクリプトです。

```bash:e2e.sh
#!/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}
```

上記のスクリプトは下記のような流れです。

1. 事前準備
2. アプリバイナリのアップロード準備
3. テストコードのアップロード準備
4. 並列でアップロード
5. runをスケジュール予約する

ファイルのアップロードに時間がかかるため、先にアップロードを準備しておき、平行してアップロードします。こうすることでCIの実行時間を短縮することができます。
上記のスクリプトが手元で正しく動作することを確認しておきます。

# GitとCircleCIに組み込む

スクリプトが正しく動くようになったら上記のファイル群をCircleCIに組み込みます。

上記のファイルなどは`appium`ディレクトリに入れています。

```bash
$ tree
├── MyTest.zip
├── README.md
├── bin
│   └── e2e.sh
├── test.apk
├── e2e_android.js
├── node_modules/
├── package-lock.json
├── package.json

...略
```

```yaml:.circleci/config.yaml
# 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の導入では橋本くんの残した検討資料を参考にしました。ありがとうございます！

# 参考記事、サイト

公式サイトでのチュートリアル。手順通りに進めることができ、思ったより簡単でおすすめです。

[Testing mobile apps across hundreds of real devices with Appium, Node.js, and AWS Device Farm | AWS Mobile Blog](https://aws.amazon.com/jp/blogs/mobile/testing-mobile-apps-across-hundreds-of-real-devices-with-appium-node-js-and-aws-device-farm/ "Testing mobile apps across hundreds of real devices with Appium, Node.js, and AWS Device Farm | AWS Mobile Blog")

[[AWS] Device FarmをCLIで実行してみた - Qiita](https://qiita.com/VA_nakatsu/items/d74814cfcbd8975cbb11 "[AWS] Device FarmをCLIで実行してみた - Qiita")

[Device Farmでリモート操作できるようになりました。 - Qiita](https://qiita.com/okamura-s/items/8393039cf447244dac48 "Device Farmでリモート操作できるようになりました。 - Qiita")


[AWS Device FarmでAndroidのアプリをテストしてみたよ！ - Qiita](https://qiita.com/flexfirm/items/45700d48b49826e6d31b "AWS Device FarmでAndroidのアプリをテストしてみたよ！ - Qiita")


[Getting Started with AWS Device Farm - AWS Device Farm](https://docs.aws.amazon.com/devicefarm/latest/developerguide/getting-started.html "Getting Started with AWS Device Farm - AWS Device Farm")