MoneyEasyのAndroidアプリでVisual Regression Testを始めた話(CI/CD編)

こんにちは、株式会社フィノバレーでAndroidエンジニア兼UXリサーチャーをやっております、島本(id: yoi_ko)です。
なお、こちらの記事は弊社の親会社であるiRidgeアドベントカレンダー6日目の記事です。

qiita.com

MoneyEasyのAndroidアプリでは、デザインリニューアルしたver2系のリリースに伴い、ver1系で実装したUI TestからVisual Regression Testへの移行を行いました。 現状、AndroidアプリにおけるVisual Regression Test=VRTの導入事例はあまり多くは見かけません。ですが、CI/CDと組み合わせることでテストのリソースを大幅に削減しつつ品質を担保することができるため、特に小規模な開発組織においては大変有効なテスト方法であると思います。

そんなVRTについて、2回に分けて紹介します。今回の記事は、後編:CI/CD編です。

  • 前編:実装編 VRT導入の経緯、想定ゴール、実装について
  • 後編:CI/CD編 GitLab CIへの取り込み方法について

前編:実装編の記事はこちらです。合わせてご覧ください💁‍♀️

finnovalley.hatenablog.com

スクリーンショットの比較を行いたい:reg-suitの導入

さて、前回の記事でご紹介した通り、約1ヶ月ほどかけてスクリーンショット(SS)を全画面分撮るテストコードの実装を終えました。テスト効率化に最低限必要なことはクリアできました🎉。
とはいえ、やはりせっかくだからすべて自動でやってしまいたいわけです。

f:id:yoi_ko:20201204115358p:plain
←これを こうしたい→

そこで次に着手したのが、reg-suitの導入でした。

reg-viz.github.io f:id:yoi_ko:20201120155556p:plain

reg-suitとは、変更前と変更後のスクリーンショットを比較し、差分を検出してHTML形式のレポートを出力するツールです。上記の画像にある通り、reg-suitはコマンドラインツールなので、CI環境およびローカルで実行することができます。万が一CIに組み込むところまでいけなくても、ローカルで動かせるならそれでもよさそう、ということで選定しました。

事前準備

  1. SSの保存先を決める
    reg-suitは、変更前のSSをAWSあるいはGCSに保存しておくことで比較を可能にしています。MoneyEasyではGCSに専用のバケットを用意しました。reg-suitの設定時に自動で生成されますが、バケット名が reg-publish-bucket-{ランダムな文字列} になるので、指定するほうがベターだと思います。

  2. SlackのWebhookを発行する
    終了時にSlackに通知させたい場合、Webhookを発行しておく必要があります。実際はこんな感じで通知が来ます。便利なので設定しておくことをオススメします。 f:id:yoi_ko:20201120163901p:plain

環境構築

以下の公式ドキュメント通りにコマンドを実行します。
GitHub - reg-viz/reg-suit: Visual Regression Testing tool

reg-suit init を実行した際に、追加するプラグインを選択します。プラグインは大きく3つにカテゴライズできるので、各々の環境に合わせてそれぞれ適当なものを選択します。

  • key-generator plugin
    比較対象のkeyを識別するためのプラグイン。 2つあるうちのどちらか1つを選ぶ

  • publisher plugin
    比較対象(変更前)のSSの取得、(比較実行後)変更後のSSと比較結果を保存するためのプラグインAWS or GCS。

  • notifier plugin
    比較結果を通知するためのプラグインGitHub、GitLab、Slackとの連携が可能。

MoneyEasyでは、以下のプラグインを使っています。

  • reg-simple-keygen-plugin
  • reg-publish-gcs-plugin
  • reg-notify-slack-plugin

以上を実行すると、 regconfig.json という設定ファイルが生成されます。

{
  "core": {
    "workingDir": ".reg",
    "actualDir": "screenshots", // 比較対象(変更後)のSSを配置するフォルダ
    "thresholdRate": 0,
    "ximgdiff": {
      "invocationType": "client"
    }
  },
  "plugins": {
    "reg-simple-keygen-plugin": {
      "expectedKey": "${EXPECTED_KEY}", // 環境変数で指定できるようにしておく
      "actualKey": "${ACTUAL_KEY}" // 環境変数で指定できるようにしておく
    },
    "reg-notify-slack-plugin": {
      "webhookUrl": "指定したWebhookURL"
    },
    "reg-publish-gcs-plugin": {
      "bucketName": "指定したバケット名"
    }
  }
}

reg-suitの環境構築はこれで終わりです。

ローカルで動かす場合

CI/CDにreg-suitの実行を組み込まず、ローカルで動かしたい場合は以下の手順で行います。

  1. Firebase TestLabでSS取得処理実行
  2. テスト完了後、テスト結果の格納されているストレージからSSをローカルにDL
  3. DLしたSSをプロジェクトリポジトリ直下の screenshots フォルダ( actualDir に指定したもの)に格納
  4. SSをリネーム&リサイズ(必要であれば)
  5. reg-suit実行

これで比較結果のレポートがHTML形式で吐き出されます 👏
実際のレポート画面はこんな感じ👇 f:id:yoi_ko:20201203205806p:plain
diffのあった画面は個別に確認できます。

f:id:yoi_ko:20201203210008p:plain
画面タイトル文字列の位置がズレている

しかしこの手順をいちいち踏むのはやっぱりめんどくさいし、いずれ実行しなくなることは目に見えていますね。特に私は短気かつ面倒くさがりなので、耐えられるはずもないわけです。
ということで、速攻これをCI/CDへ組み込む作業へ移りました。

全部自動でスクリーンショットの比較まで終わらせたい:reg-suitの実行をCI/CDへ組み込む

以前の記事でも紹介した通り、MoneyEasyのAndroidアプリはGitLab CIを使ってCI/CDを回しています。
GitLab CIはyaml形式でJobを定義します。VRTジョブは下記のような記述になりました。

# Visual Regression test
vrt:
  stage: acceptance
  # 実行環境にCloud SDK DockerベースのDockerイメージを指定 ※1
  image: <Container RegistryにアップしたDockerImage>
  script:
    # GCSの読み取り/書き込み権限のあるサービスアカウントで認証
    - echo $SERVICE_ACCOUNT > tmp.json
    - base64 -d tmp.json > service-account.json
    - gcloud auth activate-service-account --key-file service-account.json
    - gcloud config set project $CLOUD_PROJECT_ID

    # Firebase TestLabの実行 ※2
    - gcloud firebase test android run --type instrumentation --app <デバッグ用apkのパス> --test <AndroidTest用apkのパス> --device model=blueline,version=28,locale=ja_JP,orientation=portrait --timeout 30m --use-orchestrator --results-bucket="<実行結果を格納するバケット名>" --results-dir="<実行結果を格納するディレクトリ名>"

    # Firebase TestLabで取得したSSをDL
    - gsutil cp -r gs://<実行結果を格納するバケット名>/<実行結果を格納するディレクトリ名>/blueline-28-ja_JP-portrait/artifacts/sdcard/screenshots .

    - npm install
    - export PATH=$PATH:./node_modules/.bin
    - export GOOGLE_APPLICATION_CREDENTIALS="service-account.json"
    - cd screenshots

    # SSの一括リネーム&リサイズ
    - for f in `ls`; do mv $f ${f/UnknownTestClass-unknownTestMethod-/}; done
    - mogrify -format png -resize 480x *.jpg && rm -rf *.jpg && cd ..

    # 比較対象のコミットハッシュを指定し、reg-suit実行 ※3
    - git checkout $CI_COMMIT_REF_NAME || git checkout -b $CI_COMMIT_REF_NAME && git pull
    - export EXPECTED_KEY=$(git rev-parse HEAD~1)
    - export ACTUAL_KEY=$CI_COMMIT_SHA
    - reg-suit run

※1 実行環境にCloud SDK DockerベースのDockerイメージを指定

Firebase TestLabとの連携&reg-suitのSS保存先をGCSにしているため、ジョブの実行環境としてCloud SDK DockerベースのカスタムDockerイメージを指定しています。
Cloud SDK Dockerベースにしておくと、Google Cloud SDKをインストールする手順をまるっと省略できるのでオススメです。
Dockerfileはこんな感じです。nodejsとリサイズ用のImageMagickを予めインストールしているだけですね。

FROM gcr.io/google.com/cloudsdktool/cloud-sdk:latest

RUN apt-get update && apt-get install nodejs npm imagemagick -y

※2 Firebase TestLabの実行

gcloud CLIでTestLabを実行しています。詳細は公式ドキュメントを参考にしていただければと思いますが、色々設定しているので補足します。

  • --type instrumentation : インストゥルメンテーションテストの実施を指定
  • --app <デバッグ用apkのパス> --test <AndroidTest用apkのパス> : インストゥルメンテーションテスト実行に必要なapkを指定
    ※MoneyEasyの場合、VRTより前に実行している unit-test ジョブでapkを作成しているので、その保存先パスを指定しています
  • --device model=blueline,version=28,locale=ja_JP,orientation=portrait : テストを実行する端末を指定(この場合は物理のPixel3、API28、日本語、縦向き)
  • --results-bucket="<実行結果を格納するバケット名>" --results-dir="<実行結果を格納するディレクトリ名>" : TestLabの実行結果を格納する場所を指定
    ※実行完了後にSSをDLするため、ここは必ず指定してください。(指定がない場合、ランダム文字列のバケットが自動生成されます)

Orchestratorは有効にしたほうが良い

--use-orchestrator をつけることで、Android Test Orchestrator を使用することができます。
そもそもOrchestratorってなんぞや?という方向けに簡単に説明すると、要はテストの独立性を高めて、仮に1つのテストケースがコケても他のテストに影響が出ないようにする機能です。
詳しい説明や有効化の方法については、以下の公式ドキュメントをご覧ください。
AndroidJUnitRunner  |  Android デベロッパー  |  Android Developers

MoneyEasyの場合は100画面を超えるSSを取得する都合上、1つのテストが失敗して実行が中断されると影響が大きいため、Orchestratorを有効化しています。
Firebase TestLabでも推奨されているので、規模の大きいアプリの場合は設定しましょう。

f:id:yoi_ko:20201203192052p:plain
Firebase TestLabのコンソール画面(インストゥルメンテーションテスト実行)

※3 比較対象のコミットハッシュを指定し、reg-suit実行

まず前提を説明すると、MoneyEasyではVRT実行タイミングをトピックブランチがdevelopブランチへマージされたときとしています。
Visual REGERESSION Testの性質上、SSの比較対象はdevelopブランチの1つ前のコミット時(下記画像の緑丸)のSSになります。 f:id:yoi_ko:20201203194727p:plain
つまりSSの比較には、この期待値(Expected)のコミットハッシュ/実行時(Actual)のコミットハッシュそれぞれを指定してやる必要があります。

    "reg-simple-keygen-plugin": {
      "expectedKey": "${EXPECTED_KEY}", // 環境変数で指定できるようにしておく <- 期待値(緑丸)のコミットハッシュ
      "actualKey": "${ACTUAL_KEY}" // 環境変数で指定できるようにしておく <- 実行(赤丸)のコミットハッシュ
    }

Actualのコミットハッシュのほうは、GitLabCIの場合はvariableから取得できます。
一方、Expected(=developブランチの1つ前のコミット)のコミットハッシュの取得には注意が必要ですので、少し説明します。

まず前提として、GitLab CIのジョブ実行時、デフォルトでは以下のような状況になっています。

  • Runnerにリポジトリがcloneされる
  • cloneの速度を高めるために shallow clone (ドキュメント: Git shallow clone) が行われている
  • checkoutはbranchではなく、ジョブ実行のトリガーとなったコミットそのものに対して行われる( Detached HEAD状態)

Detached HEADとは、コミットがブランチから切り離されて独立してしまっている状態を指します。ブランチに紐ついていないと、コミットログを取得することができません。
developブランチの1つ前のコミットを取得するには、これをattach状態にしてやる必要があります。GitLabCIの場合は以下のコマンドを実行します。
$ git checkout $CI_COMMIT_REF_NAME || git checkout -b $CI_COMMIT_REF_NAME && git pull
これでattach状態となるので、git rev-parse HEAD~1 の実行結果を EXPECTED_KEY に指定すればOKです。

他のCIサービスでの解消方法については、reg-suitの公式ドキュメントに載っているのでこちらを参考にしてみてください。
reg-suit#run-with-ci-service

ちなみに、reg-suitではプルリクエスト時に比較実行することを推奨しているようです。この場合、key-generatorプラグインには reg-keygen-git-hash-plugin を選んだほうが良さそうです。
reg-keygen-git-hash-plugin#README.md
f:id:yoi_ko:20201203201336p:plain

導入してみての所感

ということで、無事CI/CDにVRTを組み込むことができました🎉
動くようになったのが11月頭くらいだったので、現時点で大体1ヶ月くらい運用したことになります。
運用してみてのざっくりした感想です。

  • 意図しないレイアウト崩れが発生しているかも…という心配が無くなった
  • UI Testに比べるとメンテコストが圧倒的に低い(SS撮るだけなので、せいぜいスクロール操作足すくらい)

特に「メンテコストが圧倒的に低い」点は重要かなと思います。
テストは一度作ったら作りっぱなしでいいわけではなく、プロダクトコードの変化と共に常に見直すものです。そのコストが抑えられると、よりプロダクトコードの実装に集中できますし、同時にデグレも防げるので、嬉しいことしかありません。

前編の記事で書いたように、VRTは規模の小さいチームであればあるほど、そのメリットが活きてくるものだと思います。
Androidエンジニアの数自体が少ないという事情も相まって、少人数で開発を行っているケースは案外多いのではないでしょうか。
今回の2つの記事が、そんな小規模開発を行っている方々(もちろんQAチームのあるような大規模チームの方にも!)にとって、有意義なものになれば幸いです😉