麻雀集計スマホアプリへのMaestro導入メモ

Programming

麻雀集計スマホアプリの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.yamlenv:自体は使えることになっているようだが、少なくとも自分の環境・実行方法では期待した動きにならなかった。

毎回、

-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: '保存'

同じtextaccessibilityTextを持つ要素が複数あり、想定したコンポーネントを判別できない場合は、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

検索結果の周辺でtextaccessibilityTextresource-idclickableboundsを確認し、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側の仕組みも含めて別にまとめる。

タイトルとURLをコピーしました