Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

FlutterのWidgetTest攻略メモ(随時更新)

WidgetTestを書いていて、Riverpodでビューの操作を行うクラスを全部DIして、ProviderのOverrideでMockに差し替えることをやっている。Mockにはmocktailを使っていて、このテスト戦略はシンプルでよい。サクサクテストが書ける。

riverpod.dev

どうせ忘れるので、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#addListenertype '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を立てているけど、多分スルーされる気がする。

github.com

うっかりSharedPreferenceを読み込んだコードをMockしないで死亡

この場合、エラーも何も出ないのよね。Widgetテストの宿命かもしれないけど、テスト対象のウィジェットで例外吐いている内容を捕まえられない。デバッグモードなら行けるのかなぁ...

SharedPreferenceを参照してるコードをMockで差し替えるのを漏れていて、画面遷移に失敗しており、画面遷移するはずのテストにコケていた。慣れの問題だろうけど...

Widgetテスト書いてると、きちんと外部ソースとビューのレンダリングの責務が分けられるのは実感する。その意味でクリーンというか疎結合になる。

あと、Golden Test(事前のスクショとテスト操作後のスクショ比べるやつ)は、あんまり有効性を感じない。ビューのリグレッションを検知することより、インテグレーションテストで、外部ソースと適切に連携できているかテストするほうが、品質は高まる気がしているため。