非同期処理中に表示するUIを確認したい
みなさんはテスト書いてますか?私はあまり書いていませんので、意識的に書くよう心がけています。今回は非同期処理が絡むUIテストのお話。
APIを叩いて結果を取得するなど非同期処理を行なっている間、プログレスバーを表示してユーザ体験を損なわないような工夫は常套手段です。非同期処理の例として、次のようなインターフェイスで定義される処理を考えます。
abstract class HogeAPI {
Future<Hoge> fetch();
}
UIの変化と非同期処理の流れは、
- UIをあるボタンを押下すると
- プログレスバーを表示
- 非同期処理
HogeAPI#fetch
を呼び出す - 非同期処理が終わったらプログレスバーを非表示&UIへデータ反映
このプログレスバーの表示を確認するテストを書きます。HogeAPI
はmockito
でモックして、状態管理に使用しているflutter_riverpod
のProviderScopeで差し替えています。
@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
が失敗する。解決策として、非同期処理で適当な時間だけ遅延させる手が思いつきます。
when(api.fetch(any)).thenAnswer((_) async {
+ await Future.delayed(Duration(milliseconds: 500));
return mockValue;
});
しかし適当な遅延時間とは?短すぎると遅い実行環境ではまた失敗するし、長すぎるとテストの実行に要する全体時間が長くなってしまいます。
非同期処理を任意のタイミングまで待機させる
要はプログレスバーの表示を確認するexpect
が終了するまで非同期処理HogeAPI#fetch
が終わらないよう保証する必要があります。そこでFuture.delayed
に代わり、任意のタイミングで終了できるようなFuture
を返すクラスを自作します。
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
が返すFuture
はLatch#complete
の呼び出しで終了できます。テストで使うには、expect
で確認してからLatch#complete
を呼び出し非同期処理HogeAPI#fetch
を終了させます。
+ final latch = Latch();
when(api.fetch(any)).thenAnswer((_) async {
+ await latch.wait;
return mockValue;
});
...
expect(
find.byWidgetPredicate((widget) => widget is CircularProgressIndicator),
findsOneWidget,
);
+ latch.complete();