FlutterのWidgetTestで待ち(疑似スリープ)を入れる

# 環境
訳あって最新版を使っていません(macをCatalinaにアップデートできない==Xcodeの最新版を入れられないので)。
従って、バージョン違いによる不動作などあるかも知れません。お気づきの点があったら是非コメント下さい。

※本稿は特に、Flutterテストパッケージの今後のアップデートにおいて、動作・仕様が変わる可能性があります。

```console
$ flutter --version
Flutter 1.12.13+hotfix.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 27321ebbad (4 months ago) • 2019-12-10 18:15:01 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0
```

# 1.指定時間待つ

いわゆるsleepですね。

```Dart
await tester.pump(new Duration(seconds: 3));
```

３秒待ってやる。

# 2.描画が終わるのを待つ

```Dart
await tester.pumpAndSettle();
```

描画フレームがなくなるまで`pump`してくれるというものです。
Androidでいうところの`waitForIdleSync`でしょうか。

こちらは、起動時に非同期でデータを取ってたりして、`ChangeNotifier#notifyListeners`したあと、その変更を受け取ったウィジェットの描画更新が終わるのを待ちたいときなんかにも使えます。ただ処理待ち中描画がずっと更新されていないと待ちを抜けてしまうので、ローディングウィジェットを出すなどが必要になってきます。(一瞬で終わるような処理なら問題ないですが、I/Oが絡んだりする場合は、確実に対応が必要です)

ずっとアニメーションするようなものがあると、多分タイムアウトまで待つことになります。(※未確認)

# 3.Futureタスクを待つ

非同期関数の終わりを待つ場合です。

```Dart
await tester.runAsync(() async {
      await someFutureTask();
      // test code
    });
```

# 4.Widgetの構築にタイマーが仕込んである場合

## (1)一般的な話
どうしても何か少し反応を遅らせたいとか、ローディングを最低でも2秒表示したいとか、まあどうかなと思うけど要件としてあると思います。
そんなとき、

```Dart
Future<void> someFunction() async {
  await Future.delayed(Duration(seconds: 3));
  // 処理
}
```

こんな風に書くと思います。

が、これがウィジェットのビルドに関連しているところに入っていると、`WidgetTest`するときに、エラーが出てしまいます。

```console
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A Timer is still pending even after the widget tree was disposed.
```

「タイマーがWidgetのdipose後にも残っている」というのが問題らしいのですが・・・

この辺りの議論を追っていくと、「Timerのリークチェックは必要」派の意見を読むことが出来ます。
https://github.com/flutter/flutter/issues/24166

もちろん反対する人もいて、議論が続いているようですが、今すぐにテストを書かなければならない人には結論を待っている時間はありません。

では、どうしましょ？

実は、`tester.runAsync()`が使えます。

```Dart
  await tester.runAsync(() async {
    await tester.pumpWidget(MyHomePage());
    expect(find.text("sample")), findsOneWidget);
  });
```

`MyHomePage`の中のウィジェットが、タイマー(Future.delayed)を使って更新されている場合で、その非同期処理が終わる前の状態を確認したい場合は、このようにします。

タイマーが終わった後の状態で確認を行いたい場合は、`pumpAndSettle`で終わるのを待てば良いです。(ただし、タイマー中ずっと画面更新がかかっていなければなりません)

## (2)ローディングを出している時のテスト

もうちょっと具体的な話として、ローディングを出して処理の終わりを待って表示を変えるウィジェットのテストの例を出します。

まあつまり、私が引っかかったのはこれでした。
ローディング表示を目視で確認できるように、ViewModelクラスのデータロード部分で、暫定的にこのようにしていました。

```Dart
 /// 仮データ作成
 Future<List<int>> _getPoints() async {
    await Future.delayed(Duration(seconds: 3));

    final List<int> list = List.of([1,2,3,4,5,40]);
 }
```

本当はもっと長いリストです(汗)

で、この処理の前後で、
`showLoadingFlag = false;`
`showLoadingFlag = true;`

として、`showLoadingFlag`を参照してローディング(`CircularProgressIndicator`)を出し分けていました。
(※`ChangeNotifierProvider`を使っています)

テストとしては、

- (a) データ取得が終わる前に、ローディングが表示されているか？
- (b) データ取得が終わったら、必要なウィジェットが表示されているか？
- (c) データ取得が終わったら、ローディングは消えているか？

というのを確認したくなりますよね。
(b),(c)は`pumpAndSettle`で待てば問題ありません。
問題は(a)です。

単にこうしただけでは、「Timerが残ってるよ！」エラーが出てしまいます。

```Dart
  await tester.pumpWidget(testMainPage());
  expect(
      find.byWidgetPredicate((Widget widget) => widget is CircularProgressIndicator),
      findsOneWidget);
```

`pumpAndSettle`を使えばローディングが消えてしまうので、テスト自体が失敗します。

解決策は、`runAsync`を使って、こうします。

```Dart
  await tester.runAsync(() async {
    await tester.pumpWidget(testMainPage());
    expect(
        find.byWidgetPredicate(
            (Widget widget) => widget is CircularProgressIndicator),
        findsOneWidget);
  });
```

`pumpWidget`を`runAsync`の(コールバックの)中で呼びます。
これで、ローディング前のテストを行うことが出来ます。

将来的には、私のは`Futuer.delayed`を消すでしょうから、多分問題なくなるでしょうが、どうしても残したい場合があるだろうなと思い、調べました。

## (3)なんでこうなるの？

WidgetTestの中では、タイマーは基本的にFakeで、進んでいないそうです。
それで、Widgetツリーのタイマーは発火されずに残ってしまう、ということのようです。
`runAsync`は説明を見れば書いてありますが、実際にタイマーを動かしてくれるようです。なのでWidgetツリーのタイマーが発火され、タイマーが消費されたことになり、エラーとならなくなるわけです。(※発火さえしていれば良いようで、delayedの時間をうんと長くしても、上記のテスト自体は直ぐに終わり、エラーも出なくなります)

確かに、テスト用に敢えてFakeなタイマーにしているくせに、テスト終了時にタイマー発火してないよ！って怒られるのは、なんだか納得いかない気もします(笑)

[前述のスレッド](https://github.com/flutter/flutter/issues/24166)の議論の行く末が気になるところですね。

ちなみに、このエラーが入るようになったのは、Flutter 1.5.4あたりからのようです。
いきなりテストエラー出始めた人はパニックだったろうなあ(^^;

# 参考

How to wait for a future to complete in Flutter widget test?
https://stackoverflow.com/questions/52176777/how-to-wait-for-a-future-to-complete-in-flutter-widget-test

How to unit test whether the ChangeNotifier's notifyListeners was called in Flutter/Dart?
https://stackoverflow.com/questions/59932068/how-to-unit-test-whether-the-changenotifiers-notifylisteners-was-called-in-flut

タイマー使用時のWidgetTestエラー関係
https://stackoverflow.com/questions/54021267/test-breaks-when-using-future-delayed/54021863#54021863
https://github.com/flutter/flutter/issues/11181