概要
Androidアプリの開発において、スクリーンショットテストを行うためのツールとして、Roborazziが有力な候補にあげられます。DroidKaigiや各社の技術ブログでもRoborazziの活用例に関する情報が多く発信されています。
ただ多くの場合は、GitHub Actionsを利用して実現されていることが一般的です。ただ私自身がGitLab CIを利用する状況にあったため、その実現方法に関する情報が不足していました。そこで、本記事ではGitLab CI上でRoborazziを活用したスクリーンショットテストの一例について整理します。
前提
- GitLab CIの実行環境が整備されていること
- Linux, Dockerでの実行を想定
- Roborazziのスクリーンショットテストをプロジェクトに導入済みであること
- ネット上に多くの情報があるため、この記事では割愛します
この記事で実現したいこと
- GitLab CIでRoborazziのスクリーンショットテストを実行する
- UI差分をMRにコメントとして投稿する
スクリーンショットテストの全体像
複雑で大きなジョブになるため、screenshot-test.gitlab-ci.yml
という個別のファイルにジョブを定義します。
メインの.gitlab-ci.yml
からincludeを用いてscreenshot-test.gitlab-ci.yml
のジョブを呼び出します。
include:
- local: './${配置した任意のPath}/screenshot.gitlab-ci.yml'
screenshot-test.gitlab-ci.yml
は以下のような構成になります。
GitLab CIはanchorという機能を使うことで、scriptをひとまとめにすることができます。
今回は処理ごとにanchorへ切り分けて可読性を確保します。
# --------------------------------------------------------------------
# 定数の定義
# --------------------------------------------------------------------
variables:
# GitLab REST APIのエンドポイント
# 参考:https://docs.gitlab.com/ee/api/rest/
GIT_LAB_API_BASE_URL: "https://${GitLabのHost URL}/api/v4/projects/${プロジェクトID}"
# --------------------------------------------------------------------
# anchorの定義
# --------------------------------------------------------------------
...
# --------------------------------------------------------------------
# 実行ジョブの定義
# --------------------------------------------------------------------
screenshot_test:
stage: test
# developブランチに向けたマージリクエストの発行時に実行する
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^develop.*/
script:
# gitのinstall
- apt-get install git --yes >/dev/null
# jsonを扱うjqのinstall
- apt-get install jq --yes >/dev/null
# 比較元のブランチを取得する
- *clone_original_branch
# スクリーンショットテストの実行
- *generate_screenshot_compare_image
# 比較結果のアップロード
- *upload_image
# MRに投稿するメッセージを組み立てる
- *generate_mr_comment
# MRにメッセージを投稿する
- 'curl --request POST --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" --data "body=$(cat comment)" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"'
# 古いメッセージを削除する
- *delete_old_comment
各処理の詳細
アクセストークンの設定
GitLab APIを利用してMRにコメントを投稿するにはアクセストークンが必要になります。
GitLabプロジェクトのSetting > Access Tokens からapi
scopeのアクセストークンを発行します。
発行したアクセストークンはGitLab APIのヘッダーに利用します。yamlに直接トークンを記述するとセキュリティ上よろしくないため、プロジェクトの設定から利用可能な変数として宣言します。
yamlから定数として利用できるように、 Settings > CI/CD > Variables に登録しましょう。
- key:
PROJECT_ACCESS_TOKEN
- Value: 上記で取得したアクセストークンの値
*clone_original_branch
- 目的
- 比較対象のブランチをcloneして、ビルド可能な状態にする
- 処理
- git cloneでtarget branchを取得する
- androidプロジェクトのビルドに必要なsdkのパスを通す
GitLab CIではマージ元のブランチでジョブが実行されるため、比較対象のマージ先のターゲットブランチを取得する必要があります。
アクセストークンを利用することで、SSHでクローンすることが可能です。
参考: https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
また、cloneしたままだとgradleコマンドを実行できないため、local.properties
の作成と、./gradlew
への実行権限の変更を忘れずに行います。
# 比較元のbranchを取得する
.clone_original_branch: &clone_original_branch
- git clone -b $CI_MERGE_REQUEST_TARGET_BRANCH_NAME https://oauth2:${PROJECT_ACCESS_TOKEN}@gitlab.com/${プロジェクト名}.git
# cloneしたブランチに移動する
- cd ${cloneしたプロジェクトパス}
# gradleコマンドを実行できるように設定を行う
- touch local.properties
- echo "sdk.dir=${android-sdkのパス}" >> local.properties
- chmod +x ./gradlew
- export GRADLE_USER_HOME=`pwd`/.gradle
# 初期位置のディレクトリに戻る
- cd ..
*generate_screenshot_compare_image
- 目的
- roborazziを利用してUI差分のスクリーンショットを生成する
-
COMPARE_IMAGE_PATHS
にUI差分スクリーンショットのパスを格納する
- 処理
- 比較元のブランチでスクリーンショットを生成
- MRのブランチで差分スクリーンショットを生成
- 生成された差分スクリーンショットのパスを
COMPARE_IMAGE_PATHS
に格納する
./gradlew compareRoborazziDebug
でUI差分を生成するには、build/outputs/roborazzi
に比較元の画像が必要になります。cloneしたソースで比較元画像を生成して、上記の比較用のパスに移動させます。
UI差分のスクリーンショットが生成されたら後続処理で利用できるようにCOMPARE_IMAGE_PATHS
にパスを格納しておきます。
.generate_screenshot_compare_image: &generate_screenshot_compare_image
# cloneした比較元のソースに移動する
- cd ${cloneしたプロジェクトパス}
# 比較元画像を/build/outputs/roborazziに生成
- ./gradlew :app:recordRoborazziDebug --stacktrace
# 比較元画像を/build/outputs/roborazziに移動させる
- cd ..
- mkdir -p app/build/outputs/roborazzi
- cp -r customer_app_android/app/build/outputs/roborazzi/* app/build/outputs/roborazzi/
# MRのブランチで比較元のスクリーンショットとの差分を取得する
- ./gradlew :app:compareRoborazziDebug --stacktrace
# COMPARE_IMAGE_PATHSの変数にファイルパスを格納する
- COMPARE_IMAGE_PATHS+=$(find app/build/outputs/roborazzi -type f -name "*compare*")
# UI差分が0件なら正常終了
- |
if [ ${#COMPARE_IMAGE_PATHS[*]} -eq 0 ]; then
echo "UIの差分はありませんでした"
exit 0
fi
*upload_image
- 目的
- UI差分の画像をMRで表示できるように、GitLabへアップロードする
- 処理
- GitLab APIを利用して画像をアップロードして、URLを取得する
- アップロードした画像URLを
UPLOAD_IMAGE_URLS
に格納する
generate_screenshot_compare_image
でCOMPARE_IMAGE_PATHS
に画像が格納されています。これらをGitlab APIを利用してアップロードします。このAPIはアップロード後のパスをjsonで返すので、MRのコメント用にUPLOAD_IMAGE_URLS
へ格納しておきます。
# $COMPARE_IMAGE_PATHS をGitLabにアップロードして、$UPLOAD_IMAGE_URLS に変換する
.upload_image: &upload_image
- UPLOAD_IMAGE_URLS=()
- |
for imagePath in $COMPARE_IMAGE_PATHS
do
response=$(curl --request POST --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" --form "file=@${imagePath}" "${GIT_LAB_API_BASE_URL}/uploads")
UPLOAD_IMAGE_URLS+=($(echo $response | jq -r '.markdown'))
done
*generate_mr_comment
- 目的
- MRのコメント文章を作成する
- 処理
-
COMPARE_IMAGE_PATHS
とUPLOAD_IMAGE_URLS
をテーブル形式に成形する
-
この辺りは自由ですが、テーブル形式でファイル名(COMPARE_IMAGE_PATHS
)と対応させてUI差分を表示すると見やすい表現になります。一行ずつcomment
というファイルにコメントを書き込んでいきます。
# ファイル名とアップロードした画像URLをtable形式に整形する
# 整形したメッセージは comment ファイルに格納する
.generate_mr_comment: &generate_mr_comment
- echo "📷<UIの変更を検知しました<br>" > comment
- echo "|File name|Image|" >> comment
- echo "|-|-|" >> comment
- |
for ((i = 0; i < ${#UPLOAD_IMAGE_URLS[*]}; i++))
do
# そのままだと文字列が長すぎるので、sedで30文字ごとに改行を挿入する
path=$(echo ${COMPARE_IMAGE_PATHS[i]}| sed -r 's/.{30}/&<br>/g')
echo "|${path}|${UPLOAD_IMAGE_URLS[i]}|" >> comment
done
MRへのコメント投稿
- 目的
- MRにメッセージを投稿する
- 処理
- notes APIを利用して
generate_mr_comment
で作成したコメントを投稿する
- notes APIを利用して
# MRにメッセージを投稿する
- 'curl --request POST --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" --data "body=$(cat comment)" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"'
*delete_old_comment
- 目的
- MRに投稿された古いコメントを削除する
- 処理
- notes APIでMRのコメントを全件取得する
- UI差分のコメントを抽出して、最新1件以外を削除する
スクリーンショットテストを複数回実行すると、そのたびにコメントが重複して投稿されるのは少し煩雑です。古いコメントは削除するようにジョブに組み込みます。
MRのコメントの取得、削除も同様にnotes APIで可能です。
# 古いスクリーンショットのMRコメントを削除する
.delete_old_comment: &delete_old_comment
# MRのコメントを取得する
- |
curl --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" >> comments.json
# スクリーンショットテストに関するコメントIDを抽出する
- screenshot_comment_ids=($(jq -r '.[] | select(.body | test("UIの変更を検知しました")) | .id' comments.json))
# 全体に1件以下しかスクリーンショットテストに関するコメントが無い場合、正常終了
- |
if [ ${#screenshot_comment_ids[@]} -le 1 ]; then
echo "スクリーンショットのコメントが1件以下なので、コメントの削除処理を終了します"
exit 0
fi
# 最新1件以外のスクリーンショットテストに関するコメントは削除する
- old_comment_ids=("${screenshot_comment_ids[@]:1}")
- echo ${old_comment_ids[@]}
- |
for id in $old_comment_ids
do
echo "${id}のコメントIDを削除しました"
curl --request DELETE --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes/${id}"
done
完成形
ここまでの内容をまとめると以下の内容になります。
# --------------------------------------------------------------------
# 定数の定義
# --------------------------------------------------------------------
variables:
# GitLab REST APIのエンドポイント
# 参考:https://docs.gitlab.com/ee/api/rest/
GIT_LAB_API_BASE_URL: "https://${GitLabのHOst URL}/api/v4/projects/${プロジェクトID}"
PROJECT_NAME: ${プロジェクト名}
ANDROID_SDK_PATH: ${android-sdkのパス}
# CI/CD variablesで定義
# PROJECT_ACCESS_TOKEN: ...
# --------------------------------------------------------------------
# ジョブから呼び出されるscriptの定義
# --------------------------------------------------------------------
.clone_original_branch: &clone_original_branch
- git clone -b $$CI_MERGE_REQUEST_TARGET_BRANCH_NAME https://oauth2:${PROJECT_ACCESS_TOKEN}@gitlab.com/${プロジェクト名}.git
# cloneしたブランチに移動する
- cd $PROJECT_NAME
# gradleコマンドを実行できるように.setupと同等の準備をcloneしてきたプロジェクトに行う
- touch local.properties
- echo "sdk.dir=${ANDROID_SDK_PATH}" >> local.properties
- chmod +x ./gradlew
- export GRADLE_USER_HOME=`pwd`/.gradle
- cd ..
.generate_screenshot_compare_image: &generate_screenshot_compare_image
# cloneした比較元のソースに移動する
- cd $PROJECT_NAME
# 比較元画像を/build/outputs/roborazziに生成
- ./gradlew :app:recordRoborazziDebug --stacktrace
# 比較元画像を/build/outputs/roborazziに移動させる
- cd ..
- mkdir -p app/build/outputs/roborazzi
- cp -r ${PROJECT_NAME}/app/build/outputs/roborazzi/* app/build/outputs/roborazzi/
# MRのブランチで比較元のスクリーンショットとの差分を取得する
- ./gradlew :app:compareRoborazziDebug --stacktrace
# COMPARE_IMAGE_PATHSの変数にファイルパスを格納する
- COMPARE_IMAGE_PATHS+=$(find app/build/outputs/roborazzi -type f -name "*compare*")
# UI差分が0件なら正常終了
- |
if [ ${#COMPARE_IMAGE_PATHS[*]} -eq 0 ]; then
echo "UIの差分はありませんでした"
exit 0
fi
.upload_image: &upload_image
- UPLOAD_IMAGE_URLS=()
- |
for imagePath in $COMPARE_IMAGE_PATHS
do
response=$(curl --request POST --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" --form "file=@${imagePath}" "${GIT_LAB_API_BASE_URL}/uploads")
UPLOAD_IMAGE_URLS+=$(echo $response | jq -r '.markdown')
done
.generate_mr_comment: &generate_mr_comment
- echo "📷<UIの変更を検知しました<br>" > comment
- echo "|File name|Image|" >> comment
- echo "|-|-|" >> comment
- |
for ((i = 0; i < ${#UPLOAD_IMAGE_URLS[*]}; i++))
do
# そのままだと文字列が長すぎるので、sedで30文字ごとに改行を挿入する
path=$(echo ${COMPARE_IMAGE_PATHS[i]}| sed -r 's/.{30}/&<br>/g')
echo "|${path}|${UPLOAD_IMAGE_URLS[i]}|" >> comment
done
- cat comment
.delete_old_comment: &delete_old_comment
# MRのコメントを取得する
- |
curl --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" >> comments.json
# スクリーンショットテストに関するコメントIDを抽出する
- screenshot_comment_ids=($(jq -r '.[] | select(.body | test("UIの変更を検知しました")) | .id' comments.json))
# 全体に1件以下しかスクリーンショットテストに関するコメントが無い場合、正常終了
- |
if [ ${#screenshot_comment_ids[@]} -le 1 ]; then
echo "スクリーンショットのコメントが1件以下なので、コメントの削除処理を終了します"
exit 0
fi
# 最新1件以外のスクリーンショットテストに関するコメントは削除する
- old_comment_ids=("${screenshot_comment_ids[@]:1}")
- echo ${old_comment_ids[@]}
- |
for id in $old_comment_ids
do
echo "${id}のコメントIDを削除しました"
curl --request DELETE --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes/${id}"
done
# --------------------------------------------------------------------
# 実行ジョブの定義
# --------------------------------------------------------------------
screenshot_test:
stage: ✅ test
# developブランチに向けたマージリクエストの発行時
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^develop.*/
script:
# gitのinstall
- apt-get install git --yes >/dev/null
# jsonを扱うjqのinstall
- apt-get install jq --yes >/dev/null
# 比較元のブランチを取得する
- *clone_original_branch
# スクリーンショットテストの実行
- *generate_screenshot_compare_image
# 比較結果のアップロード
- *upload_image
# MRに投稿するメッセージを組み立てる
- *generate_mr_comment
# MRにメッセージを投稿する
- 'curl --request POST --header "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" --data "body=$(cat comment)" "${GIT_LAB_API_BASE_URL}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"'
# 古いメッセージを削除する
- *delete_old_comment
artifacts:
when: always
paths:
- '**/build/outputs/roborazzi'
expire_in: 1 day
まとめ
この記事ではGitLab CIでRoborazziを実現する方法について整理しました。
この例ではapp
配下でのみ実行となっていますが、マルチモジュールの場合はモジュール毎にRoborazziのジョブを実行する必要があります。下記のような処理を追加することでマルチモジュールの場合にも対応できますね。
- git diffで変更のあるmoduleを検出する
- 変更のあるmoduleに対して
./gradlew :${module名}:recordRoborazziDebug
を実行
私自身はAndroidエンジニアを生業にしておりLinuxは不慣れでしたが、GitLab CIはLinuxコマンドをふんだんに使うため学びが多くありました。ChatGPTに聞けばLinux, CIの知識は十分にサポートしてくれるため、今後も恐れずに自動化へ挑戦していきたいですね。