麻雀集計スマホアプリのE2Eテスト用にMaestroを導入したので、設定内容を忘れないようにまとめておく。
対象はReact Native / Expoで作成しているAndroidアプリ。
JestではロジックやHookなどをテストしているが、実際の画面操作やDeep Link、VIEW / EDIT / OWNERによる表示差などはMaestroで確認することにした。
通信異常系についてはmitmproxyと組み合わせているが、それは別記事にする。
導入と基本構成
開発環境はUbuntu。
Maestro CLIをインストールする。
curl -fsSL "https://get.maestro.mobile.dev" | bash
PATHが通っていなければ追加する。
export PATH="$PATH:$HOME/.maestro/bin"
.bashrcに書いた場合は反映。
source ~/.bashrc
確認。
maestro --version
Javaも必要なので確認しておく。
java -version
現在はJava 17以上が必要。
Ubuntuなら例えば、
sudo apt install openjdk-17-jdk
で入れられる。
Android実機の確認
このプロジェクトではAndroid実機で確認している。
USBデバッグを有効にして、
adb devices
で確認する。
List of devices attached
XXXXXXXXXXXX device
となっていればよい。
Expo Development Buildについても、通常の開発時と同じように実機で起動できる状態にしておく。
.maestroフォルダーを作成
プロジェクトルートに.maestroを作成した。
現在は次の構成。
.maestro/
├── config.yaml
├── README.md
├── flows/
├── p0/
├── p1/
├── p2/
└── scripts/
最初に作るなら、
mkdir -p .maestro/flows
mkdir -p .maestro/p0
mkdir -p .maestro/p1
mkdir -p .maestro/p2
mkdir -p .maestro/scripts
としておけばよい。
テストは重要度で分けている。
p0
リリース前に必ず確認したいもの
p1
主要機能
p2
設定や細かな画面遷移など
最初は全部flowsでもよかったが、テストが増えてきたので分けた。
最小のFlow
MaestroのテストはYAMLで書く。
例えば、
.maestro/p0/startup.yaml
を作成。
appId: com.anzaihome.mahjongapp.dev
name: P0 - アプリ起動確認
tags:
- p0
---
- launchApp
- assertVisible: '麻雀大会 集計'
---より上がFlowの設定。
appId:
name:
tags:
などを書く。
---より下が実際の操作。
よく使うコマンド
アプリ起動
- launchApp
アプリの状態を消して起動したい場合は、
- launchApp:
clearState: true
画面に文字があることを確認
- assertVisible: '麻雀大会 集計'
表示されていないことを確認
- assertNotVisible: '参加者を追加'
VIEW権限の確認などでよく使う。
ボタンを押す
- tapOn: '保存'
文字入力
- tapOn: '大会名'
- inputText: 'Maestroテスト大会'
Deep Linkを開く
このアプリではかなり使う。
- openLink:
link: mahjongapp-dev://mahjong/tournament/xxxxxxxx
共有リンクから直接大会や卓を開くテストができる。
環境変数とテスト実行
Deep LinkやアプリIDを各YAMLに直接書くと管理が面倒になる。
そのためFlowでは、
appId: ${APP_ID}
や、
- openLink:
link: ${TOURNAMENT_VIEW_LINK}
のようにしている。
Maestro CLIでは、
maestro test \
-e APP_ID=com.anzaihome.mahjongapp.dev \
-e TOURNAMENT_VIEW_LINK='mahjongapp-dev://mahjong/tournament/xxxxxxxx' \
.maestro/p0/access-control.yaml
のように-eで環境変数を渡せる。
ただ、実際には変数がかなり多い。
例えば、
APP_ID
GROUP_NAME
GROUP_OWNER_LINK
GROUP_EDIT_LINK
GROUP_VIEW_LINK
TOURNAMENT_NAME
TOURNAMENT_EDIT_LINK
TOURNAMENT_VIEW_LINK
TABLE_NAME
TABLE_EDIT_LINK
TABLE_VIEW_LINK
PLAYER_1
PLAYER_2
PLAYER_3
PLAYER_4
など。
毎回コマンドラインに書くのは無理なので、.maestro/config.yamlにまとめることにした。
config.yaml
現在はこんな感じ。
env:
APP_ID: com.anzaihome.mahjongapp.dev
GROUP_NAME: Maestroテストグループ
GROUP_OWNER_LINK: mahjongapp-dev://mahjong/group/OWNER_KEY
GROUP_EDIT_LINK: mahjongapp-dev://mahjong/group/EDIT_KEY
GROUP_VIEW_LINK: mahjongapp-dev://mahjong/group/VIEW_KEY
TOURNAMENT_NAME: Maestroテスト大会
TOURNAMENT_EDIT_LINK: mahjongapp-dev://mahjong/tournament/EDIT_KEY
TOURNAMENT_VIEW_LINK: mahjongapp-dev://mahjong/tournament/VIEW_KEY
TABLE_NAME: 卓1
TABLE_EDIT_LINK: mahjongapp-dev://mahjong/table/EDIT_KEY
TABLE_VIEW_LINK: mahjongapp-dev://mahjong/table/VIEW_KEY
テストデータを作り直してリンクが変わった場合も、基本的にはここを修正する。
config.yamlのenvだけでは動かなかった
ここは少しハマった。
最初は、
maestro test \
--config .maestro/config.yaml \
.maestro/p0/access-control.yaml
とすれば、
env:
APP_ID: com.anzaihome.mahjongapp.dev
が、
appId: ${APP_ID}
にそのまま入ると思っていた。
しかし実際には、
Launch app "undefined"
になった。
つまり${APP_ID}が解決されていなかった。
現在のMaestroのドキュメントを見るとconfig.yamlのenv:自体は使えることになっているようだが、少なくとも自分の環境・実行方法では期待した動きにならなかった。
毎回、
-e APP_ID=...
-e GROUP_VIEW_LINK=...
-e TOURNAMENT_VIEW_LINK=...
を書くのも避けたい。
そのため、config.yamlを自分で読み込んで-e KEY=VALUEに変換するシェルスクリプトを作った。
maestro-test.sh
作成場所。
.maestro/scripts/maestro-test.sh
やっていることは単純で、
.maestro/config.yaml
↓
env:を読み込む
↓
-e KEY=VALUE
に変換
↓
maestro testを実行
というもの。
現在使っているものは以下。
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE=".maestro/config.yaml"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: $CONFIG_FILE was not found. Run this script from the project root." >&2
exit 1
fi
if ! command -v maestro >/dev/null 2>&1; then
echo "Error: maestro command was not found in PATH." >&2
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <flow-or-directory> [maestro test options...]" >&2
exit 1
fi
ENV_ARGS=()
while IFS=$'\t' read -r key value; do
[[ -z "$key" ]] && continue
value="${value%$'\r'}"
if [[ ${#value} -ge 2 ]]; then
if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
value="${value:1:${#value}-2}"
elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
value="${value:1:${#value}-2}"
fi
fi
ENV_ARGS+=("-e" "${key}=${value}")
done < <(
awk '
/^env:[[:space:]]*$/ {
in_env = 1
next
}
in_env && /^[^[:space:]#]/ {
exit
}
in_env && /^[[:space:]]+[A-Za-z_][A-Za-z0-9_]*:[[:space:]]*/ {
line = $0
sub(/^[[:space:]]+/, "", line)
key = line
sub(/:.*/, "", key)
value = line
sub(/^[^:]+:[[:space:]]*/, "", value)
print key "\t" value
}
' "$CONFIG_FILE"
)
if [[ ${#ENV_ARGS[@]} -eq 0 ]]; then
echo "Error: no environment variables were found under env: in $CONFIG_FILE." >&2
exit 1
fi
ENV_COUNT=$((${#ENV_ARGS[@]} / 2))
echo "Loaded ${ENV_COUNT} Maestro environment variables from ${CONFIG_FILE}."
exec maestro test \
--config "$CONFIG_FILE" \
"${ENV_ARGS[@]}" \
"$@"
set -euo pipefail
スクリプト先頭の、
set -euo pipefail
は今後も忘れそうなのでメモ。
-e
途中のコマンドが失敗したら終了
-u
未定義変数を使ったらエラー
pipefail
パイプ途中のコマンドが失敗してもエラー扱い
テスト用のスクリプトなので、途中で何か失敗した状態でそのまま続けるより、この設定の方がよい。
テスト実行
実行権限を付ける。
chmod +x .maestro/scripts/maestro-test.sh
以降はプロジェクトルートから、
.maestro/scripts/maestro-test.sh \
.maestro/p0/access-control.yaml
で実行する。
これでconfig.yamlの環境変数が自動的に、
-e APP_ID=...
-e GROUP_NAME=...
-e TOURNAMENT_VIEW_LINK=...
などに変換される。
毎回環境変数を書く必要がなくなった。
今は基本的に直接maestro testを実行するより、このスクリプト経由で実行している。
テストFlowの組み立て方
このアプリではVIEW / EDIT / OWNERの権限制御がある。
例えばVIEWリンクでは、
- 大会名は見える
- 編集ボタンは出ない
- 参加者追加は出ない
EDITリンクでは、
- 編集ボタンが出る
- 参加者追加ができる
という違いがある。
簡略化すると、
appId: ${APP_ID}
name: P0 - VIEW・EDIT・OWNER の権限制御
tags:
- p0
- permissions
---
- launchApp
- assertVisible: '麻雀大会 集計'
- openLink:
link: ${TOURNAMENT_VIEW_LINK}
- extendedWaitUntil:
visible: ${TOURNAMENT_NAME}
timeout: 30000
- assertNotVisible: '${TOURNAMENT_NAME}を編集'
- assertNotVisible: '参加者を追加'
- openLink:
link: ${TOURNAMENT_EDIT_LINK}
- extendedWaitUntil:
visible: ${TOURNAMENT_NAME}
timeout: 30000
- assertVisible: '${TOURNAMENT_NAME}を編集'
- assertVisible: '参加者を追加'
という感じ。
Jestでは確認しにくい、実際にDeep Linkを開いた結果、画面上に編集UIが出るかを確認できる。
共通処理はrunFlow
同じ操作を何回も書くものは共通Flowにしている。
例えばDeep Linkで未保存のページを開くと、
このページを保存しますか?
が表示される場合がある。
これを各テストへ書くのではなく、
.maestro/flows/dismiss-save-page-prompt.yaml
に分離。
呼び出し側は、
- runFlow: ../flows/dismiss-save-page-prompt.yaml
とする。
テストファイル自体もかなり読みやすくなる。
固定の待ち時間はできるだけ使わない
最初は、APIが返るまで3秒待つ、みたいなテストを書きたくなるが、端末や通信状態によって変わるので固定sleepはなるべく入れていない。
通常は、
- assertVisible: '大会名'
で待たせる。
APIレスポンスなど少し時間がかかるものだけ、
- extendedWaitUntil:
visible: ${TOURNAMENT_NAME}
timeout: 30000
としている。
Maestro Studio
CLIとは別にMaestro StudioというGUI版もある。
Linux版はAppImage。
ダウンロード後は、
mkdir -p ~/.local/opt/maestro-studio
として、
mv ~/Downloads/MaestroStudio.AppImage \
~/.local/opt/maestro-studio/
へ移動した。
実行権限。
chmod +x ~/.local/opt/maestro-studio/MaestroStudio.AppImage
起動。
~/.local/opt/maestro-studio/MaestroStudio.AppImage --no-sandbox
Ubuntu 24.04ではAppImage実行用に、
sudo apt install libfuse2t64
も必要だった。
Maestro Studioは現在ほぼ使っていない
GUI上で端末の画面を見ながらFlowを作れたり、UI要素を確認できるので、最初は便利そうだと思った。
ただし、このプロジェクトでは環境変数をかなり多く使っている。
CLI側では、
.maestro/config.yaml
にまとめてあり、
.maestro/scripts/maestro-test.sh
から一括で読み込んでいる。
一方、Maestro Studioではこのconfig.yamlの環境変数をそのまま使えず、Studio側のEnvironment Managerに登録する必要がある。
しかも、
APP_ID
GROUP_NAME
GROUP_OWNER_LINK
GROUP_EDIT_LINK
GROUP_VIEW_LINK
TOURNAMENT_NAME
TOURNAMENT_EDIT_LINK
TOURNAMENT_VIEW_LINK
TABLE_EDIT_LINK
...
を1個ずつ登録する必要がある。
このプロジェクトでは環境変数の数が多いため、これはかなり使いづらい。
テストデータを作り直してDeep Linkが変更された場合、config.yamlを修正したうえで、Studio側のEnvironment Managerも修正することになり、二重管理になる。
そのため現在はMaestro Studioを通常のテスト実行には使っていない。
基本は、
.maestro/scripts/maestro-test.sh \
.maestro/p0/access-control.yaml
というCLI実行。
Studioを使う可能性があるのは、このUIがMaestroからどう認識されているか確認したい、といった場合くらい。
環境変数をファイルから一括で読み込めるようになれば、Studioももう少し使いやすくなると思う。
testIDとmaestro hierarchy
Maestroでは、まず画面上の文字を使って操作対象を指定できる。
- tapOn: '保存'
同じtextやaccessibilityTextを持つ要素が複数あり、想定したコンポーネントを判別できない場合は、React Native側のtestIDを使う。
testIDは全部のコンポーネントに付けない。表示文字だけでは対象を一意に決められない場合や、E2Eで重要な操作が不安定になる場合に限定する。
<Pressable
testID="round-selector"
accessibilityLabel={roundName}
onPress={handlePress}
>
React Nativeで指定したtestIDは、maestro hierarchyの出力ではresource-idとして表示される。
"accessibilityText" : "第1局",
"resource-id" : "round-selector",
"clickable" : "true"
MaestroのテストFlowでは、resource-idではなくidセレクターとして指定する。
- tapOn:
id: round-selector
テストで意図しない要素が押されたり、要素が見つからないエラーになった場合は、Maestroが実際に認識しているUI階層をファイルへ出力して確認する。
maestro hierarchy > hierarchy.txt
階層全体が出力されるので、そのまま上から読むのではなく、対象の表示文字やtestIDで検索する。
rg -n 'round-selector|第1局' hierarchy.txt
rgが入っていなければgrep -nでも確認できる。
grep -n -E 'round-selector|第1局' hierarchy.txt
検索結果の周辺でtext、accessibilityText、resource-id、clickable、boundsを確認し、Flowで使うセレクターを決める。
JestとMaestroの使い分け
今のところ、
Jest
ロジック
Hook
データ変換
API周辺の単体テスト
Maestro
実際の画面操作
Deep Link
権限制御
入力
保存
画面遷移
という使い分けになっている。
MaestroはYAMLだけで実機操作をかなり簡単に書けるので、React NativeのE2Eテストとしては扱いやすい。
一方で、テストが増えてくると、環境変数、テスト用データ、Deep Link、通信異常系、テスト後の状態あたりの管理が重要になる。
特にこのプロジェクトでは環境変数が多いため、
config.yaml
↓
maestro-test.sh
↓
-e KEY=VALUE
↓
maestro test
という実行方法にしておくのが一番扱いやすかった。
現在はこれを基本の実行方法としている。
通信のofflineやHTTP 500などをMaestroから切り替える方法については、mitmproxy側の仕組みも含めて別にまとめる。

