[:content]
はじめに
こんにちは。ないぱかです。
弊 OSS Flutter アプリの One Page に E2E テストフレームワークの Patrol を導入しました。
5〜6年前に E2E テストを導入する際に Flutter の公式 E2E テストツールである integration_test を導入しようとしたことがあるのですが、
ネイティブの権限ダイアログ操作ができなかったり、WebView を操作できないのでログインができなかったりと制約が厳しく、E2E テスト導入に難航していたのを覚えています。。。
その点、Patrol はネイティブの権限ダイアログ操作や WebView 操作もサポートしているため、実用的な E2E テストを実装することができます。
しかも、Dart でテストコードを書くことができるのが嬉しいですね。
この記事では、Patrol を導入するまでの手順をまとめています。
対応 PR は以下です。
github.com
前提
One Pageは以下の構成になっています。
- Flutter 3.44.2
- Dart 3.12.2
- FVM 使用
- Dart Define で環境分け
- マルチパッケージ構成(Pub Workspace と melos を使用)
Patrol 関連のバージョンは以下の通りです。
- patrol_cli: 4.4.0
- patrol: 4.6.1
2026年6月19日に SPM 対応された patrol 4.7.0-dev.1 がリリースされているので、正式リリースされたらアップデートしたいですね。
pub.dev
導入手順
基本的には以下の公式ドキュメント通りに進めれば問題ないですが、いくつかハマった点があったので、そこを中心に記載します。
patrol.leancode.co
1. patrol_cli のインストール
まずは Patrol UI テストを実行するための CLI ツールである patrol_cli をインストールします。
公式ドキュメントでは、flutter pub global activate patrol_cli と記載されていますが、ローカルのグローバルとプロジェクトの Flutter バージョンが異なっていたので、念の為 FVM が指す SDK に対してインストールすることにしました。
> fvm flutter pub global activate patrol_cli
Package patrol_cli is currently active at version 4.4.0.
Downloading packages... .
The package patrol_cli is already activated at newest available version.
To recompile executables, first run `flutter pub global deactivate patrol_cli`.
Installed executable patrol.
Activated patrol_cli 4.4.0.
インストールが完了したら、patrol doctor コマンドを実行して、環境が正しくセットアップされているか確認します。
公式ドキュメントの注意書きにもありますが、patrol 内部では Flutter CLI を呼び出していますが、何も指定しないとグローバルの Flutter SDK を参照してしてしまいます。
そのため、今回は .zshrc に PATROL_FLUTTER_COMMAND を設定して、FVM が指す Flutter SDK を参照するようにしました。
export PATROL_FLUTTER_COMMAND="fvm flutter"
> patrol doctor
Patrol doctor:
Patrol CLI version: 4.4.0
Flutter command: fvm flutter
Flutter 3.44.2 • channel stable
Android:
• Program adb found in /Users/xxx/Library/Android/sdk/platform-tools/adb
• Env var $ANDROID_HOME set to /Users/xxx/Library/Android/sdk
iOS / macOS:
• Program xcodebuild found in /usr/bin/xcodebuild
• Program ideviceinstaller found in /opt/homebrew/bin/ideviceinstaller
Web:
• Program node found in /opt/homebrew/opt/node@20/bin/node
• Program npm found in /opt/homebrew/opt/node@20/bin/npm
2. pubspec.yaml の設定
patrol パッケージをプロジェクトの dev_dependencies に追加します。
dev_dependencies:
patrol: ^4.6.1
続いて、patrol セクションも追加します。
今回は開発版アプリでテストを実行するため、開発版のアプリのパッケージ名とバンドル ID を指定しました。
Flavor 対応している場合は flavor パラメータで指定することもできますが、本アプリでは Dart Define で環境を分けているため、直接開発環境でのパッケージ名とバンドル ID を指定する必要があります。
patrol:
app_name: One Page Dev
android:
package_name: com.naipaka.onepage.dev
ios:
bundle_id: com.naipaka.onepage.dev
3. Android セットアップ
Android のセットアップは公式ドキュメント通りです。
3-1.build.gradle.kts への追加内容
defaultConfig 内に以下を追加します。
testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
android ブロック内に testOptions を追加します。
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
dependencies にも以下を追加します。
androidTestUtil("androidx.test:orchestrator:1.5.1")
3-2. MainActivityTest.java の作成
android/app/src/androidTest/java/com/naipaka/onepage/MainActivityTest.java を新規作成します。
公式 example app のものをベースに、パッケージ名を com.naipaka.onepage に変更しました。
各設定の役割
PatrolJUnitRunner: Patrol が Dart テストを Android の JUnit テストとして実行するためのカスタムランナー
clearPackageData = "true": テスト間でアプリデータを完全クリアし、テストの独立性を確保
ANDROIDX_TEST_ORCHESTRATOR: 各テストを個別プロセスで実行し、テスト間の分離を強化
MainActivityTest.java: Patrol が Dart 側のテストを列挙・実行するためのエントリーポイント
4. iOS セットアップ
iOS のセットアップもほぼ公式ドキュメント通りです。
4-1. Xcode で RunnerUITests ターゲットを作成
File > New > Target... → UI Testing Bundle
- Product Name:
RunnerUITests に変更
- Organization Identifier:
com.naipaka.onepage.dev (開発版のバンドル ID に合わせる)
- Language: Objective-C(公式ドキュメントで指定。
RunnerUITests.m が Objective-C のマクロを使うため)
- Target to be Tested:
Runner
4-2. RunnerUITestsLaunchTests.m の削除
Xcode が自動生成する RunnerUITestsLaunchTests.m を Xcode 上で Move to Trash で削除。
4-3. iOS Deployment Target の確認
RunnerUITests の Build Settings → iOS Deployment Target を Runner と同じに設定。
今回は 15.0 に設定しました。
4-4. RunnerUITests.m の置き換え
Xcode が自動生成した内容を公式ドキュメントの内容に置き換え:
@import XCTest;
@import patrol;
@import ObjectiveC.runtime;
PATROL_INTEGRATION_TEST_IOS_RUNNER(RunnerUITests)
4-5. Podfile に RunnerUITests ターゲットを追加
Runner ターゲット内、RunnerTests の下に追加。
target 'RunnerUITests' do
inherit! :complete
end
SPM 対応されたらこの対応は不要になりそうですね。
4-6. flutter build ios --config-only の実行
mkdir -p patrol_test && touch patrol_test/example_test.dart
fvm flutter build ios --config-only patrol_test/example_test.dart
4-7. pod install
pod install --repo-update
4-8. Configuration Set を Flutter の xcconfig に変更
RunnerUITests の Base Configuration を Runner と同じ Flutter xcconfig に変更します。
| Configuration |
Base Configuration |
| Debug |
Flutter/Debug.xcconfig |
| Release |
Flutter/Release.xcconfig |
| Profile |
Flutter/Release.xcconfig |
pod install 後、RunnerUITests の Base Configuration は Pods-Runner-RunnerUITests.*.xcconfig になっています。
しかしこの xcconfig には FLUTTER_ROOT が含まれず、Build Phases の xcode_backend build / xcode_backend embed_and_thin が Generated.xcconfig を読めなくなってしまうため、Runner と同じ Flutter の xcconfig を指定する必要があるようです。
Flutter xcconfig に変更すると pod install で「CocoaPods が base configuration を設定できない」警告が出ますが、動作に支障はありませんでした。
ここも SPM 対応されたら、CocoaPods の設定が不要になるので、解消されそうですね。
4-9. Build Phases の追加
RunnerUITests ターゲットに2つの Run Script Phase を Xcode で追加します。
- xcode_backend build
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
- xcode_backend embed_and_thin
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed_and_thin
Build Phases の最終的な順序は以下のとおりです。
- Target Dependencies
- Run Build Tool Plug-ins
- [CP] Check Pods Manifest.lock
- xcode_backend build
- Compile Sources
- Link Binary With Libraries
- Copy Bundle Resources
- [CP] Embed Pods Frameworks
- xcode_backend embed_and_thin
4-10. Parallel Execution の無効化
Product → Scheme → Edit Scheme → Test で、RunnerUITests を追加し、Options を開き Parallelization を無効にします。
動画ではチェックボックスでの設定になっていますが、Xcode 26 ではチェックボックスではなくドロップダウン(「Parallelization: Enabled (If Possible)」)に変更されています。

5. テスト実行
テストコードは patrol_test ディレクトリに配置します。
まずは、公式ドキュメントのスモークテストを patrol_test/example_test.dart に配置しました。
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
void main() {
patrolTest(
'counter state is the same after going to home and switching apps',
($) async {
await $.pumpWidgetAndSettle(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('app')),
backgroundColor: Colors.blue,
),
),
);
expect($('app'), findsOneWidget);
if (!Platform.isMacOS) {
await $.platform.mobile.pressHome();
}
},
);
}
このプロジェクトでは Dart-Define を使用しているため、--dart-define-from-file で dev 環境の値を渡す必要があります。
cd packages/app
patrol test \
-t patrol_test/example_test.dart \
--dart-define-from-file=dart_defines/dev.env
Selected device: iPhone 17 Pro (E0A9C9EF-8E0B-4FD2-B81E-6980E178854A)
• Building app with entrypoint test_bundle.dart for iOS simulator (debug)...
Building com.naipaka.onepage.dev for simulator (ios)...
✓ Completed building app with entrypoint test_bundle.dart for iOS simulator (78.7s)
• Running app with entrypoint test_bundle.dart for iOS simulator on simulator iPhone 17 Pro...
✅ counter state is the same after going to home and switching apps (/example_test.dart) (0s)
Test summary:
📝 Total: 1
✅ Successful: 1
❌ Failed: 0
⏩ Skipped: 0
⏱️ Duration: 28s
✓ Completed executing app with entrypoint test_bundle.dart for iOS simulator on simulator iPhone 17 Pro (28.6s)
6. 初期化設定
公式ドキュメント「Initializing app inside a test」セクションに、テスト内でアプリを起動する際の制約が記載されています。
WidgetsFlutterBinding.ensureInitialized() を呼んではいけない
runApp() を使ってはいけない — 代わりに $.pumpWidget() を使う
FlutterError.onError を変更してはいけない — Crashlytics 等の監視ツールがエラーを横取りすると、テストエンジンがエラーを検知できず、テストが失敗しても終了しなくなる
公式 example の patrol_test/common.dart では createApp(PatrolIntegrationTester $) 関数を定義し、テストから呼ぶパターンを採用していました。
上記の制約に従い、main.dart から createApp() を呼び出すように変更しました。
lib/app_initializer.dart(新規)
class AppInitializer {
const AppInitializer._();
static Future<Widget> createApp({Tracker? tracker}) async {
final flavor = Flavor.values.byName(const String.fromEnvironment('flavor'));
await Firebase.initializeApp();
final effectiveTracker = tracker ?? Tracker();
final (_, prefsClient) = await (
LocaleSettings.useDeviceLocale(),
PrefsClient.initialize(),
).wait;
return ProviderScope(
overrides: [
flavorProvider.overrideWithValue(flavor),
trackerProvider.overrideWithValue(effectiveTracker),
prefsClientProvider.overrideWithValue(prefsClient),
],
observers: [providerLogger],
child: TranslationProvider(child: const App()),
);
}
}
lib/main.dart(変更)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final tracker = Tracker();
FlutterError.onError = tracker.onFlutterError;
PlatformDispatcher.instance.onError = tracker.onPlatformError;
Isolate.current.addErrorListener(tracker.isolateErrorListener());
final app = await AppInitializer.createApp(tracker: tracker);
runApp(app);
}
テストコード
初期化処理を変更したので、実際にアプリを起動してテストするテストコードを作成しました。
データ読み込みが 10 秒(デフォルト)を超えることがあったため、timeout を 30 秒に設定しています。
patrol_test/common.dart
Future<void> createApp(PatrolIntegrationTester $) async {
final app = await AppInitializer.createApp();
await $.pumpWidget(app);
await $.waitUntilVisible($(K.homePage), timeout: const Duration(seconds: 30));
await $.waitUntilVisible(
$(K.diaryCalendar),
timeout: const Duration(seconds: 30),
);
}
@isTest
void patrol(
String description,
Future<void> Function(PatrolIntegrationTester) callback, {
bool? skip,
List<String> tags = const [],
}) {
patrolTest(description, skip: skip, callback, tags: tags);
}
patrol_test/example_test.dart
import 'common.dart';
void main() {
patrol('shows the diary calendar after launch', ($) async {
await createApp($);
expect($(K.diaryCalendar), findsOneWidget);
});
}
テスト実行ログは以下のようになりました。
✓ Completed building app with entrypoint test_bundle.dart for iOS simulator (77.9s)
• Running app with entrypoint test_bundle.dart for iOS simulator on simulator iPhone 17 Pro...
✅ shows the diary calendar after launch (/example_test.dart) (27s)
Test summary:
📝 Total: 1
✅ Successful: 1
❌ Failed: 0
⏩ Skipped: 0
📊 Report: /Users/ryota/work/personal/apps/onepage/packages/app/build/ios_results_1781933158597.xcresult
⏱️ Duration: 45s
✓ Completed executing app with entrypoint test_bundle.dart for iOS simulator on simulator iPhone 17 Pro (45.7s)
まとめ
今回は、Flutter アプリの E2E テストフレームワークである Patrol を導入しました。
まだテストコードは書いてないものの、かなり快適に E2E テスト環境が構築できそうです。
近いうちに、実際にテストコードを追加するとともに、CI でのテスト実行も設定していきます。
OSS で公開しているリポジトリなので、無料で利用できる GitHub Actions で完結させたいですね〜。