⚠️

[Flutter] Patrol 逆引き大全 (E2Eテスト)

2024/11/01に公開

FlutterのE2Eテストにはさまざまな議論がありますが、私のプロジェクトではPatrolを採用しています。本記事では、公式のサンプルアプリを参考にしながら、Patrolを使ったテストの具体例をメソッド別に、逆引きで検索できる形式でまとめました。

目次から操作したい機能を選び、各機能ごとにテストコードの例を確認できるようになっているため、実際の開発プロジェクトでのテスト設計に役立てていただければ幸いです

※公式のサンプルアプリで使用されているような、使用頻度が高いものをまとめています。

こんな人におすすめ

・E2EテストでNative操作をしたい
・Patrolを効率よく使いたい
・Patrolでどんなことができるのか知りたい

単体テストの考え方について知りたい方は、こちらもぜひ!
https://zenn.dev/renren0112/articles/88111b123029d7

Patrol について

Patrolは、Flutterのintegration_testに対する強力な代替として、より実際の環境に近い自動テストを提供するフレームワークです。

Patrolでは、OSの操作やシステム設定の変更が可能なほか、Nativeの画面をSelectorを通して識別でき、通知やWebViewのテストも実施できます。また、コードがintegration_testと類似しているため、直感的でわかりやすいです。

https://patrol.leancode.co/

セットアップ

https://patrol.leancode.co/getting-started

環境

  • M3 Macbook Pro
  • Flutter 3.24.4 • channel stable
  • patrol_cli: 3.2.1
  • patrol: 3.11.2
  • patrol_finders: 2.1.3
  • java version: 17.0.13
  • Android 14 (API 34) (emulator)

公式のサンプルを参照しよう!

公式のサンプルアプリをcloneして、一度ローカルで動かしてみることをお勧めします!

https://github.com/leancodepl/patrol/tree/master/dev/e2e_app

ビルドコマンド

$ patrol test -t  integration_test/example_test.dart

基本

keyを識別

ex)Textに、keyを指定

Text(
  key: const Key('key'),...
),

Textの文字を取得

$(#key).text

指定された要素が画面に表示されるまで待機

await $.waitUntilVisible($(#keyname)); 

描画が終わるのを待つ

await $.pumpAndSettle();

指定時間待つ

await $.pump(Duration(seconds: 5));

TextFieldに文字を入力

await $(#keyname).enterText('Hello, Flutter!');

タップ

await $.tap($(#keyname));

n番目をタップ

.at(n).tap();

特定のwidgetが表示されるまでスクロール

await $.scrollUntilVisible(
finder: $(#keyname), scrollDirection: AxisDirection.down);

特定の文字が表示されるまで自動スクロール

ex)listでindex番号を文字として表示するときに、100番目までスクロール

await $('index: 100').scrollTo();

特定の文字を含むwidgetが存在するか確認する

expect($('Hello, Flutter!'), findsOneWidget);

widgetが存在しないことを確認する

expect($("Can't touch this"), findsNothing);

特定の数のwidetが存在することを確認する

expect(find.byType(Card), findsNWidgets(3));

Nativeの機能を操作しよう!

ホームに戻る

await $.native.pressHome();

アプリを開く

await $.native.openApp();

Androidの場合は通知シェード、iOSの場合は通知センターを開く

await $.native.openNotifications();

通知をタップする

await $.native.tapOnNotificationBySelector(
  Selector(textContains: 'Someone liked'),
);

別のアプリを開く

ex) chromeのアプリを開くケース

browserId = 'com.android.chrome';
await $.native.openApp(appId: browserId);

ディープリンク

await $.native.openUrl('patrol://check/somepath?query=10');

Androidではクイック設定シェード、iOSではコントロールセンターを開く

await $.native.openQuickSettings();

bluetoothのオンオフ

await $.native.disableBluetooth();
await $.native.enableBluetooth();

機内モードのオンオフ

await $.native.disableAirplaneMode();
await $.native.enableAirplaneMode();

セルラー接続(モバイルデータ)のオンオフ

await $.native.disableCellular();
await $.native.enableCellular();

wifiのオンオフ

await $.native.disableWifi();
await $.native.enableWifi();

システムのダークモードのオンオフ

await $.native.disableDarkMode();
await $.native.enableDarkMode();

位置情報のオンオフ

await $.native.disableLocation();
await $.native.enableLocation();

音量のアップダウン

await $.native.pressVolumeUp();
await $.native.pressVolumeDown();

Nativeの画面をスワイプする

ex) 設定のページ

appId = 'com.android.settings';
// 設定アプリを開く
await $.native.openApp(appId: appId);
// スワイプ
await $.native.swipe(
  from: Offset(0.5, 0.8),
  to: Offset(0.5, 0.2),
  appId: appId,
);

Nativeの画面をタップする

appId = 'com.android.settings';
// 設定アプリを開く
await $.native.openApp(appId: appId);
// 画面をタップする
await $.native.tapAt(
  Offset(0.5, 0.8),
  appId: appId,
);

「おおよその位置情報」を許可をセレクト

※位置情報許可ダイアログ出現時

await $.native.selectCoarseLocation();

「正確な位置情報」を許可をセレクト

※位置情報許可ダイアログ出現時

await $.native.selectFineLocation();

パーミッションを操作しよう!

const _timeout = Duration(seconds: 5); 

アクセス許可を否定する

※ _timeoutの設定も可能

if (await $.native.isPermissionDialogVisible(timeout: _timeout)) {
  await $.native.denyPermission();
}

アプリの使用中は許可する

if (await $.native.isPermissionDialogVisible()) {
  await $.native.grantPermissionWhenInUse();
}

一度だけ許可

if (await $.native.isPermissionDialogVisible()) {
  await $.native.grantPermissionOnlyThisTime();
}

Webviewを操作しよう!

ボタンをタップする

ex) webサイトが表示された時に出る
Accept all cookies のボタンをタップする。

await $.native.tap(Selector(text: 'Accept all cookies'));

Android の EditText,iOS の TextField または SecureTextFieldに文字を入れ込む

 await $.native.enterTextByIndex(
  'test@gmail.com',
  // n番目のテキストフィールド
  index: 0,
  keyboardBehavior: KeyboardBehavior.alternative,
);

native2について

native2を使うことで、AndroidとiOSのネイティブ機能に対して統一された操作が可能です。

await $.native2.tap(
NativeSelector(
  android: AndroidSelector(text: 'Log in'),
  ios: IOSSelector(label: 'Log in'),
 ),
);

nativeAutomatorとnativeAutomator2

$.nativeは、nativeAutomator の省略形であり
https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator-class.html

$.native2は、nativeAutomator2 の省略形です。
https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator2-class.html

native2は、アプリが実行されている OS とのやり取りを可能にする新しいセレクター API を備えたネイティブオートメーションです。

このように、AndroidとiOSで異なるプロパティ(textやlabel)を指定することで、OSごとの画面構造の違いを意識せずに、同じ操作を行えるようになります。native2を使えば、AndroidとiOSに対して統一されたテストが可能です。

まとめ

native機能を活用したテストは、ユーザーのエクスペリエンスをリアルに再現できるので、アプリの信頼性が向上します。E2Eテストには労力がかかりますが、品質向上に不可欠です。ぜひ、Patrolの機能を活用して、効率よくテストを実施してみてください⭐️

アップデートされた時は、随時更新していければと思います。

Discussion