FlutterのWidgetTest攻略メモ(随時更新)
WidgetTestを書いていて、Riverpodでビューの操作を行うクラスを全部DIして、ProviderのOverrideでMockに差し替えることをやっている。Mockにはmocktailを使っていて、このテスト戦略はシンプルでよい。サクサクテストが書ける。
どうせ忘れるので、WidgetTestを書いてきて遭遇したエラー内容をメモる。RiverPodのバージョンはもちろん1.0だ!
FutureメソッドのMock
Flutter書いているとtype 'Null' is not a subtype of type 'Future<T>
のエラーにであう。Futureを使っているロジックは、明示的にちゃんとMockで値を返すように設定しないとアカン。
今回はboolだったのでこうなった。any()
はどんな引数でもええでというやつ。returnしないとnullが返るから気をつけよう。
どうせ違うのは戻りがあるかないかぐらいで、ProviderでDIするロジックってイベントハンドラなので基本voidだから、もうちょい簡単になるかも。
// 戻り値あり、引数あり when(() => fakeService.doSomething(any())).thenAnswer((_) async { return Future<bool>.value(true);}); // 戻り値なし、引数あり when(() => fakeService.doSomething(any())).thenAnswer((_) => Future.value()); // 引数も戻り値もなし when(fakeService.doSomething).thenAnswer((_) => Future.value();});
ViewModelとして機能するStateNotifierにHTTPでAPI取ってくる処理までも内包していいのか、ちょっと悩んでる。今は外してる。つまり、Repositoryの役目を果たすクラスはRiiverPodのProvider、画面の入出力に必要な値をひとつのPageStateにまとめて、StateNotifierで管理してる。ま、これで動くからそれでいいかなぁ。
FutureProviderの上書き
FutureProviderで保有している型(ここではHogePageState
)に対し、てきとーな値を入れておく。ローカルストレージ系のものは、ローカルストレージからXXを取得してオブジェクトに入れた前提で動かせば良いと思われる。
SharedPreferencesをProviderでキャッシュする作戦をとっている場合、SharedPreferencesをMockしたProviderを作るのがちょっと面倒かもしれん。get
の引数でthenAnswer
連打の印象。書いてないから違ったらごめんね。
hogeFutureProvder.overrideWithValue(const AsyncValue.data(HogePageState( name: "hoge", type: "foo")))
No Media Query
MaterialApp
で囲ってない。
Could not find a generator for route RouteSettings
Mockで適当に差し込んだMaterialAppにRoutesの定義がないので、画面遷移に失敗する。
Widgetの型を指定し、そこに含まれるテキストで検索する
widgetWithText
が俺たちにはあった。
await tester.tap(find.text("Press")); await tester.pumpAndSettle(); expect(find.widgetWithText(AlertDialog, "Dialog Pressed"), findsOneWidget);
findのAPI、全部見たほうがええな。
StateNotifierProviderのMockができない
こういうコードを書くとProviderException
が発生し、StateNotifierProvider#addListener
でtype 'Null' is not a subtype of type '() => void'
というエラーが出る。voidの関数が入るべき所にNull入れるなよボケってところ。
class FakeFooStateNotifier extends Mock implements FooStateNotifier {} testWidgets('ItemPage Search Test', (WidgetTester tester) async { final fakeViewModel = new FakeFooStateNotifier(); await tester.pumpWidget(ProviderScope( overrides: [ fooState.overrideWithValue(fakeViewModel), ], child: MaterialApp( home: FooPage(), ))); });
addListener
がnullだったら適当にvoid Function作ったろかでノリでwhen(() => fakeViewModel.addListener(any())).thenReturn(() => {});
とか書くと、state
が与えられていないせいかBad state: Tried to read the state of an uninitialized provider
という別の例外がでた。
1.0以前はfakeViewModel.state.overrideWithValue(mock)
みたいな書き方ができたけど、1.0の場合はstate
がprotectedなので、外からアクセスできない。
StateNotifier自体がMockできないので、stateを操作する以外のロジック(例えばローカルストレージ読み込み)を書いてはいけないと思われる。Providerのref
を使ってStateNotifierProviderを読み込ませ、そこで状態を変更しよう。StateNotifierとStateNotifierを操作するサービスクラスの2つに分けたほうがいいな。Mockで差し替わらないというのが最大の理由だけど。
同じ問題に直面した人がIssueを立てているけど、多分スルーされる気がする。
うっかりSharedPreferenceを読み込んだコードをMockしないで死亡
この場合、エラーも何も出ないのよね。Widgetテストの宿命かもしれないけど、テスト対象のウィジェットで例外吐いている内容を捕まえられない。デバッグモードなら行けるのかなぁ...
SharedPreferenceを参照してるコードをMockで差し替えるのを漏れていて、画面遷移に失敗しており、画面遷移するはずのテストにコケていた。慣れの問題だろうけど...
Widgetテスト書いてると、きちんと外部ソースとビューのレンダリングの責務が分けられるのは実感する。その意味でクリーンというか疎結合になる。
あと、Golden Test(事前のスクショとテスト操作後のスクショ比べるやつ)は、あんまり有効性を感じない。ビューのリグレッションを検知することより、インテグレーションテストで、外部ソースと適切に連携できているかテストするほうが、品質は高まる気がしているため。