Life is Really Short, Have Your Life!!

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

FlutterのWidgetテストをちゃんとできるような設計にするお勉強

FlutterのテストにはUnit / Widget / Integrationの3つの段階があるが、外部データソースのアクセスを利用しないでDartのオブジェクト上だけでテストを行うのが、UnitとWidgetのテスト。

とはいっても、昨今で外部リソースを一切使わないケースはない。

  • HTTPSAPI通信
  • FirebaseのRemoteConfig
  • SharedPrefrenceに代表されるOSが持つローカルストレージ
  • SQLiteのようなローカルDB

ただHTTP通信やローカルストレージをMockにした所で、ほとんど使えない。それらを内部で呼び出している関数(ロジック)をそっくりMockに差し替えることができる必要がある。もちろん、プロダクトで動くソースコードに手を加えることなく。ビューに外部データソースに関するロジックを埋め込むとウィジェットテストを行えないのだ。

このようなケースに対してどう対応するかだが、まぁ簡単に言えば、ビューから呼び出されているビジネスロジックをMockで上書きするしかないのでははかろうか。そのための仕組みとして、またまたRiverpodの出番となる。RiverpodにはProviderをオーバーライドできる機能があるのだ。

このソースを見た時に、ほおおおおおそういうことねぇえええと思った。

github.com

class MockNewsViewModel extends Mock implements NewsViewModel {}

void main() {
  final mockNewsViewModel = MockNewsViewModel();
  when(mockNewsViewModel.fetchNews).thenAnswer((_) => Future.value());
  when(() => mockNewsViewModel.news)
      .thenReturn(Result.success(data: dummyNews));

  testWidgets('App widget test', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          newsViewModelProvider.overrideWithValue(mockNewsViewModel),
        ],
        child: const App(),
      ),
    );
  });

ViewModelに該当するクラスをそっくりそのままMockに差し替える。これはmocktailパッケージで提供される。なんでこんなことができるのかさっぱりわからないのが辛いので、いつかコードを読まないといけない。リフレクションに近いなにかだと思う。

差し替えたViewModelクラスに対して、適当な値を突っ込む。thenReturnとかthenAnswerがそれ。Mockだから決め打ちで良い。非同期で呼びされるメソッドの場合は、thenAnswerを使うようだ。staticなメソッドのMockはできないらしいが、インスタンスメソッド内でstaticメソッドが呼びされている場合は、どうなんだろう。多分大丈夫だろきっと。

ListViewのテストでは、hogeが2行目に表示されていることを捕まえる必要があり、下記にあるように、何行目のアイテムを捕まえるコードを書かないといけない。

stackoverflow.com

1つ困ったのが、SQLiteのMockテスト。今回、読み取り専用のマスタデータをsqlite形式で持ってる。検索パラメーターは2つある。この場合、決め打ちでthenAnswerしちゃったら、ビューの表示確認にはなるけど、SQLの組み立てのテストにはならない。SQLを文字列で組み立てるようなことをすると、テスタビリティが損なわれるんだな。他のやり方を考えよう。最悪はユニットテストレベルで担保すればいいや。