こちらの記事は 「MEDLEY Summer Tech Blog Relay」 の 15日目 の記事です ![]()
人材プラットフォーム QAグループの菅原です。現在はジョブメドレーのiOSアプリを開発するクロスファンクショナルなチームにQAエンジニアとして参加しています。このアプリはFlutter製で、これまでWebアプリやiOS/AndroidネイティブなアプリのE2Eテスト自動化には関わってきたもののFlutterアプリについては今回が初めてでした。E2Eテスティングフレームワークの選定から導入、チームへの展開までを、入社してからの4ヶ月で少しずつ行ってきたので、今回はその過程で得た知見や気づきを共有できればと思います。
なぜE2Eテスト自動化が必要だったか
求職者アプリでは「変更によって重要な機能が壊れていないか早く・繰り返し確認できるようにしたい」という理由や、一般的に、問題の発覚が遅れるほどその修正コストが跳ね上がるというデータもあるため、品質担保の手段の1つとして導入を検討していました。
候補の絞り込み
これまでiOS / Webアプリのテスト自動化をゼロから導入した経験もありますが、当時はE2Eテスティングフレームワークの種類も今ほど豊富ではなく、プラットフォーム毎に「これよね」と言えるような時代でした。しかし現在は様々な選択肢があるため、以下のような項目で比較検討を進めました。(抜粋)
| 比較項目 | 内容 |
|---|---|
| テストの記述言語 | ノーコード / YAML / Dartなど、何で書くか・作るか |
| テストの実行スピード | 1テストあたりの実行速度だけでなく、テスト前後の準備・後処理まで含めた実際の所要時間 |
| UI要素へのアクセス | Widget、ネイティブUI、WebViewにロードされた要素へのアクセスが可能か |
| テストコードの管理場所 | アプリと同じリポジトリで管理できるか、ツール側のクラウド環境のみか |
| テストの作成コスト・学習コスト | 記述言語やデザインパターンなど、学習コストをどれほど割けばある程度理解できるようになるか(AIに書かせるとはいえ) |
| 利用費用 | 無料か否か。有料の場合、それを越える恩恵があるか |
| サポート・運用体制 | 日本語サポートの有無、コミュニティの活発さなど |
最終的に残ったのは次の3つです。
- Patrol
- Maestro
- ノーコード・ローコードツール
Patrol
Flutter公式のintegration_testをベースにしているため、テストコードがアプリ本体と同じDartプロセス・同じWidget treeの中で動作します。原則としてSemanticsを経由せずWidget treeを直接検索できるため、UI要素の取得のための通信やポーリングは発生しません。システムダイアログの操作などiOSネイティブなUI要素の処理のみXCUITestに委譲します。
┌───────────────────────────────────────────────┐
│ 同一 Dart プロセス │
│ │
│ ┌────────────────┐ 直接UIを検索┌──────────────┐│
│ │ テストコード │ ─────────▶│ Widget tree ││
│ └───────┬────────┘ └──────────────┘│
│ │ 通信は発生しない │
└──────────┼────────────────────────────────────┘
│ システムダイアログなど、Flutterの外側の要素は
│ HTTP経由でXCUITestランナーに処理を委譲
▼
┌────────────────────────────────────────────┐
│ XCUITestランナー │
└──────────┬─────────────────────────────────┘
│ アクセシビリティAPIでUI要素を取得・操作
▼
┌────────────────────────────────────────────┐
│ システムダイアログ・通知など │
└────────────────────────────────────────────┘
実際のテストコードはこんな雰囲気です(簡略化&Page Object Modelの形式)
// Page Object
class LoginPage {
LoginPage(this.$);
final PatrolIntegrationTester $;
Finder get emailForm => find.bySemanticsLabel('メールアドレス');
Finder get passwordForm => find.bySemanticsLabel('パスワード');
Finder get loginButton => find.text('ログインする');
Future<void> doLogin({required String email, required String password}) async {
await $.enterText(emailForm, email);
await $.enterText(passwordForm, password);
await $.tap(loginButton);
await $.pumpAndSettle();
}
}
// Tests
patrolTest('ログイン後、マイボタンが表示されていること', ($) async {
await app.main();
await $.pumpAndSettle(timeout: const Duration(seconds: N));
await pages.top.goToLoginPage();
await pages.login.doLogin(email: foo, password: bar);
expect(
pages.home.myButton,
findsOne,
reason: 'マイボタンが表示されていること',
);
});
Maestro
Maestroは、maestro cliがHTTP経由でiOSドライバーに指示を送り、Semanticsが公開したアクセシビリティツリーを介してUIを操作する、いわばブラックボックス型のツールです。テストはYAMLで宣言的に書き、アプリの実装言語やアプリの内部構造を意識する必要はありません。
┌─────────────────┐ HTTPで問い合わせ ┌──────────────────────────┐
│ Maestro CLI │ ───────────────────▶│ Maestro iOS ドライバー │
│ │ ◀───────────────────│ (XCUITest ランナー) │
└─────────────────┘ まだ見つからなければ └─────────────┬────────────┘
見つかるまで問い合わせ │ アクセシビリティAPIでUI要素を取得・操作
┌──────────▼───────────────┐
│ Flutter App │
└──────────────────────────┘
サンプルコード(抜粋)
- tapOn:
text: "ログインページへ行く"
- tapOn:
text: "メールアドレス"
- inputText: ${email}
- tapOn:
text: "パスワード"
- inputText: ${password}
- tapOn:
text: "ログインする"
- assertVisible:
text: "マイボタン"
ノーコード・ローコードツール
GUI操作でテストを組み立てられるツールで、社内でも既に運用実績があり、ナレッジが溜まっている心強い選択肢でした。今回は、テストコードをアプリと同じリポジトリでバージョン管理し、開発と同じ流れでレビュー・運用したいという狙いがあったため、その観点からMaestro・Patrolを中心に検討を進めました。
Maestro vs Patrol
前述の通り、最後まで比較したのはMaestroとPatrolです。実際に、サンプルのテストを5回実行した際の、テストステップの実行所要時間を比較すると以下の結果となりました。
- Patrol: 102秒
- Maestro: 206秒
テスト単体だけ見れば、Patrolの方が約2倍速いという結果でした。ところが、テストを実行開始する前段の下準備や実行後の後処理といったテスト全体の合計時間で比較すると、逆転した結果となりました。
| フェーズ | Patrol | Maestro |
|---|---|---|
| Flutterビルド | 〜80秒 | 83秒 |
| XCUITestランナーのコンパイル | 〜27秒 | なし |
| テスト単体の合計 | 102秒 | 206秒 |
| Maestro CLI初回起動コスト | なし | 8秒 |
| アプリのアンインストール作業の合計 | 46秒 | 計測せず。後述 |
| アプリの再インストール+XCUITestランナー再起動の合計 | 89秒 | なし |
| 合計 | 364秒 | 298秒 |
*Flutterビルド、XCUITestランナーのコンパイルの秒数は推定値、それ以外は実測値です
合計では、Maestroの方が約1.22倍速いという結果になりました。逆転の理由は上述したように、Patrol側にだけ発生する周辺コストです。
- Patrolを使うには、アプリ専用のXCUITestランナーをその都度コンパイルする必要があり、Maestroは汎用的にビルド済みの実行ドライバを使い回すため、この待ち時間は発生しません
- 各テスト終了後、Maestroは
xcrun simctl uninstallコマンドでアプリを削除するため体感値としてはほぼゼロですが、Patrolはユーザーと同じ手順でアプリを削除する必要があり、時間がかかっていました(Patrolでも同コマンドで削除できないか試みましたが、原理的に難しそうでした) - 削除後の再インストール時、Patrolはアプリ専用のXCUITestランナーを再起動する必要があり、その分時間がかかります。Maestroの汎用ドライバではこの手順自体が発生しません
決め手
最終的にMaestroを選択し、主な決め手は次の3点でした。
- テスト全体の実行スピード
- 運用フェーズでの認知負荷の低さ
- 前提知識が相対的に少なくすみ、メンテナーを増やしやすいこと
今回のE2E自動テストは複雑な業務ロジックの正しさまで担保するテストレイヤーではなく、重要なユーザーシナリオが壊れていないかを確認することに主眼をおいていました。それゆえ、Patrolのアプリ内部にまで踏み込んだ柔軟なテストは魅力的ではありつつも、今回は求めないことにしました。
また、Patrolの場合、Dartの文法理解に加えて、実際のテストではPage Object Modelのようなデザインパターンを使うことが予測され、テストのステップを読むだけでなく、呼び出し先のページオブジェクトのクラスなどまで行き来して初めて「何を確認しているテストなのか」を理解できます。一方のMaestroは、YAMLに画面遷移や入力操作などのテストステップが基本的にそのまま順番に並んでいるため、1つのファイルを上から読むだけでテストの意図が伝わります。このテストの核心にたどり着くまでの作業、読む文字数の少なさこそがMaestroの認知負荷の低さの要因の1つだと考えており、この理解のしやすさは、横展開のしやすさやメンテナーの増やしやすさにもつながると考え、決め手の1つとなりました。AIにテストを書かせる前提ではいるものの、人が微修正を加えることが現時点では稀にあるため、そうした場面でもすぐに対応できる分かりやすさは重要でした。
AIに書かせる
記述がシンプルなMaestroですが、現在はテストシナリオを人からAIに渡して以下の作業を行うskillで自律的にE2Eテストを作成し、QAエンジニアがレビューするという役割分担で進めています。今後はこのシナリオの洗い出し自体も任せていき、QAエンジニアはレビューに専念する形を目指しテストの拡充スピードをさらに上げていきたいと思っています。
- シミュレーターのセットアップ・動作確認
- テストの設計方針のチェック
- 既存のテスト構成の把握
- シナリオの解析・要件判定
- コードの調査と画面要素の確認
- テストの生成
- 実機でのテスト実行、失敗時の調査・修正・再実行(With Maestro MCP)
組織への広がり
現在、私が参加する求職者アプリ以外に、ジョブメドレーの採用管理アプリを担当するQAエンジニアもMaestroを使い始めました。繰り返しになりますが、Maestroは相対的に学習コストが低いということもあり、記述言語の文法やデザインパターンなどの勉強会を行わずに、初期のセットアップやテストのディレクトリ構成などについて共有するのみで採用管理アプリ側でもすぐ使用できるようになりました。
こうしたツールを選んだことで、E2Eテストにかかるコストそのものを下げられたと感じています。特定のスキルを持つ人にしか読めない・書けない・直せないテストは、AIがいるとはいえ書いた本人がいなくなった途端にメンテされにくくなり、いずれ形骸化する恐れが高いです。今回、QAエンジニアもAIと一緒にすぐ書き始められ、ソフトウェアエンジニアも無理なくレビューに参加できる体制を作れたことで、誰か一人に依存しない継続的な運用の土台ができました。この「継続しやすさ」があるからこそ、これからAIを使ってテストの数を増やしていっても、組織としてちゃんと育て続けられると考えています。
まとめ
まだテストを書き始めたばかりのフェーズなので、今回採用したMaestroを通じて「こんな価値提供ができるようになった」といったことを次回共有できたらいいなと思います。
MEDLEY Summer Tech Blog Relay 16日目の記事は奥澤さんです! 明日もお楽しみに!✨