1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Patrolでテストを書く(テスト編)

Last updated at Posted at 2024-05-19

はじめに

【Flutter】Patrolでテストを書く(導入まで) の続きです。
実際にpatrolで統合テストを書いてネイティブビューを操作してみたいと思います。

FinderWidgetTesterの基本的な使い方については触れません。

環境

・flutter: 3.19.6
・dart: 3.3.4
・patrol: 3.6.1
・patrol_cli: 2.7.0

基本のテスト

まずはFlutterプロジェクト作成時にデフォルトで用意されているDemoアプリに対してテストを書いてみます。
↓ Demoアプリというのはコレのことです。

patrolを使わない場合

patrolでもFinderやWidgetTesterは使えます。
なので、既にintegration_testでテストを書いている場合でも比較的低コストでpatrolへ切り替えることができそうです。
まずはpatrolを使用せずテストを書いてみます。
導入編で作成したintegration_testディレクトリにdemo_test.dartを作成してテストを追加します。

demo_test.dart
void main() {
  patrolTest('demo', (PatrolIntegrationTester $) async {
    await $.pumpWidgetAndSettle(const MyApp());
    
    expect(
      find.text('Flutter Demo Home Page'),
      findsOneWidget,
    );

    expect(
      find.text('0'),
      findsOneWidget,
    );

    final button = find.byType(FloatingActionButton);

    expect(
      button,
      findsOneWidget,
    );

    await $.tester.tap(button);

    await $.tester.pumpAndSettle();

    expect(
      find.text('1'),
      findsOneWidget,
    );

    await $.tester.tap(button);

    await $.tester.pumpAndSettle();

    expect(
      find.text('2'),
      findsOneWidget,
    );
  });
}

$.testerWidgetTesterを使用できます。
テストを書いたら以下のコマンドで統合テストを実行します。
注意点として、名前はdemo_test.dartである必要はありませんが、対象はintegration_test内にある_test.dartで終わるファイル名でないといけません。

・Android

patrol test --target integration_test/demo_test.dart -d emulator-5554

・iOS

patrol test --target integration_test/demo_test.dart -d 'iPhone 15'

patrolを使う場合

次にpatrolを使用してテストを書き直してみます。

demo_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:patrol_example/main.dart';

void main() {
  patrolTest('demo', (PatrolIntegrationTester $) async {
    await $.pumpWidgetAndSettle(const MyApp());

    expect(
      $('Flutter Demo Home Page'),
      findsOneWidget,
    );

    expect(
      $('0'),
      findsOneWidget,
    );

    final button = $(FloatingActionButton);

    expect(
      button,
      findsOneWidget,
    );

    await button.tap();

    // 「1」というテキストを含むWidgetが表示されるまで待機
    await $('1').waitUntilVisible();

    expect(
      $('1'),
      findsOneWidget,
    );

    await button.tap();

    await $('2').waitUntilVisible();

    expect(
      $('2'),
      findsOneWidget,
    );
  });
}

正直簡単過ぎるテストなのであまり変わったように見えませんが、少し簡潔になった気がします。
他にも例えば、単純なDemoアプリではメリットを感じませんが、実際のアプリではHTTP通信等の非同期処理があり、tester.pumpAndSettle()だけでは描画待ちできないケースもあると思います。
こういったケースでもpatrolが使えると便利です。

試しに、Demoアプリのボタンタップ時、インクリメントする処理にdelayを追加してください。

lib/main.dart
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // これを追加
          await Future.delayed(const Duration(seconds: 3));
          _incrementCounter();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );

その後、patrolを使わない場合で書いたテストを実行するとテストは失敗します。
ボタンタップ時、カウントが「0」 -> 「1」に描画されるのをtester.pumpAndSettle()で待機できないからです。
次にpatrolを使う場合で書いたテストを実行してみるとテストが成功すると思います。
$('1').waitUntilVisible()が「1」というテキストが描画されるまで待機してくれるからです。

他のPatrol finderやアクションの使用方法・メリットついては以下が参考になると思います。
https://patrol.leancode.co/finders/usage

ネイティブビューのテスト

次にOSの権限ダイアログに対してテストを書きます。
雑にpermission_handler を使用してボタン押下時に位置情報の権限許可ダイアログを表示するようにしました。
ダイアログから権限を設定するとTextに権限状態を表示するようにしてます。

.dart

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  PermissionStatus? _status;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  Future<void> _updateLocationStatus() async {
    PermissionStatus status = await _getLocationStatus();
    setState(() {
      _status = status;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // ----- 追加 ----- //
            TextButton(
              onPressed: () =>
                  _requestLocation().then((_) => _updateLocationStatus()),
              child: const Text('ボタン'),
            ),
            if (_status != null)
              Text(
                '$_status',
              ),
            // --------------- //
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

  Future<void> _requestLocation() async => Permission.location.request();

  Future<PermissionStatus> _getLocationStatus() async =>
      await Permission.location.status;
}

テストがこちらです。
ネイティブビューの操作に対しては$.nativeを使用します。
注意点として、iOSで権限ダイアログを操作する場合、言語設定はEnglish(US)にする必要があります。それ以外の言語では権限をうまく選択することができません。

一応、English(US)以外の言語でテストする方法もあるみたいです

.dart
void main() {
  patrolTest('demo', (PatrolIntegrationTester $) async {
    await $.pumpWidgetAndSettle(const MyApp());

    // ----- 省略 ----- //

    await $('ボタン').tap();

    // 権限ダイアログが表示されるまで待機
    await $.native.isPermissionDialogVisible();
    // 「使用中のみ許可」を選択
    await $.native.grantPermissionWhenInUse();
    // 権限状態が表示されているWidgetを探す
    final status = $('${await Permission.location.status}');

    await status.waitUntilVisible();

    expect(
      status,
      findsOneWidget,
    );
  });
}

テスト実行時にエラーになった場合

私が遭遇したエラーと解決方法についてメモしておきます。
公式のFAQを見たり、エラーメッセージでissuesを検索すると似たようケースが結構ありました。

Unsupported class file major version 65

  • エラーメッセージ
FAILURE: Build failed with an exception.
        
* What went wrong:
Execution failed for task ':gradle:compileGroovy'.
> BUG! exception in phase 'semantic analysis' in source unit '/Users/username/fvm/versions/3.19.6/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy' Unsupported class file major version 65

これについてはFAQに記載がありました。

It's most likely caused by using incompatible JDK version. Run javac -version to check your JDK version. Patrol officially works on JDK 17, so unexpected errors may occur on other versions. If you have AndroidStudio or Intellij IDEA installed, you can find the path to JDK by opening your project's android directory in AS/Intellij and going to Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Gradle JDK. Learn more

要するにPatrolはJDK 17で動くことを想定してるので他のバージョンだとエラーが発生するかもとのこと。

  • 自分の環境を確認したところ、やはり17ではありませんでした。
javac -version
> javac 21.0.1
  • 17をインストール
sdk list java
sdk install java 17.x.x-open
  • 17に切り替え
sdk use java 17.x.x-open
  • Android StudioのGradle JVM設定確認
  1. android/build.gradleからAndroid Studioを開く
  2. Settings > Build, Execution, Deployment > Build Tools > Gradle に移動
  3. Gradle JDK に17.x.x-open バージョンが設定されていることを確認

おわりに

基本的な使い方は以上です。ネイティブビューを操作できること以外にも色々できるので時間があれば試してみようと思います。
難点としては、テスト時のスクリーンショットを撮ることはできない(以前のバージョンにはあったみたいです)ので、スクショが目的ならpatrolは選択肢から外れてしまいそうです。

参考

Patrol Finders Usage
Patrol CLI commands test

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?