2
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?

More than 3 years have passed since last update.

【Flutter + Widgetテスト】非同期処理を特定タイミングまで待機させる

Posted at

非同期処理中に表示するUIを確認したい

みなさんはテスト書いてますか?私はあまり書いていませんので、意識的に書くよう心がけています。今回は非同期処理が絡むUIテストのお話。

APIを叩いて結果を取得するなど非同期処理を行なっている間、プログレスバーを表示してユーザ体験を損なわないような工夫は常套手段です。非同期処理の例として、次のようなインターフェイスで定義される処理を考えます。

hoge_api.dart
abstract class HogeAPI {
  Future<Hoge> fetch();
}

UIの変化と非同期処理の流れは、

  1. UIをあるボタンを押下すると
  2. プログレスバーを表示
  3. 非同期処理HogeAPI#fetchを呼び出す
  4. 非同期処理が終わったらプログレスバーを非表示&UIへデータ反映

このプログレスバーの表示を確認するテストを書きます。HogeAPImockitoでモックして、状態管理に使用しているflutter_riverpodのProviderScopeで差し替えています。

hoge_page_test.dart
@GenerateMocks([HogeAPI])
void main() {
  testWidgets("プログレスバーの確認", (tester) async {
    // setup
    final api = MockHogeAPI();
    await tester.pumpWidget(ProviderScope(
      overrides: [
        hogeAPIProvider.overrideWithValue(api),
      ],
      child: const MaterialApp(
        title: title,
        home: HogePage(),
      ),
    ));

    when(api.fetch(any)).thenAnswer((_) async {
      return mockValue;
    });

    // test
    await tester.tap(find.byKey(buttonKey));
    await tester.pump();

    // check
    expect(
      find.byWidgetPredicate((widget) => widget is CircularProgressIndicator),
      findsOneWidget,
    );
  }
}

問題

プログレスバーが表示される前に非同期処理が終わってしまうのでexpectが失敗する。解決策として、非同期処理で適当な時間だけ遅延させる手が思いつきます。

hoge_page_test.dart
    when(api.fetch(any)).thenAnswer((_) async {
+     await Future.delayed(Duration(milliseconds: 500));
      return mockValue;
    });

しかし適当な遅延時間とは?短すぎると遅い実行環境ではまた失敗するし、長すぎるとテストの実行に要する全体時間が長くなってしまいます。

非同期処理を任意のタイミングまで待機させる

要はプログレスバーの表示を確認するexpectが終了するまで非同期処理HogeAPI#fetchが終わらないよう保証する必要があります。そこでFuture.delayedに代わり、任意のタイミングで終了できるようなFutureを返すクラスを自作します。

latch.dart
class Latch {
  /// period: 終了条件を監視する時間幅
  Latch({Duration? period})
      : _period = period ?? const Duration(milliseconds: 100);

  final Duration _period;
  var _waiting = true;

  /// このlatchが`#complete`で終了されるまで待機するFuture
  ///
  /// すでに`#complete`で終了済みの場合は即座に完了するFutureを返す
  Future<void> get wait => _waiting
      ? Future.sync(() async {
          while (_waiting) {
            await Future.delayed(_period);
          }
        })
      : Future.value();

  /// `#wait`で返したFutureを終了する
  ///
  /// **注意** Futureは即座に終了しない
  /// 指定した時間間隔でこの終了呼び出しを監視して`#complete`が呼ばれた以降のタイミングでFutureを終了させる
  void complete() {
    _waiting = false;
  }
}

Latch#waitが返すFutureLatch#completeの呼び出しで終了できます。テストで使うには、expectで確認してからLatch#completeを呼び出し非同期処理HogeAPI#fetchを終了させます。

hoge_page_test.dart
+   final latch = Latch();
    when(api.fetch(any)).thenAnswer((_) async {
+     await latch.wait;
      return mockValue;
    });
...
    expect(
      find.byWidgetPredicate((widget) => widget is CircularProgressIndicator),
      findsOneWidget,
    );
+   latch.complete();
2
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
2
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?