FlutterでRiverpodを用いた画面のWidget Testやってみた

FlutterでRiverpodを用いた画面のWidget Testやってみた

Clock Icon2024.05.07

こんにちは。
モダンアプリコンサル部の坂本です。

前回前々回とFlutterでのUnitTestについて投稿してきました。
今回は前回までに製造したログイン機能に画面を追加し、Widgetのテストを実施してみたいと思います。

画面の製造

UIはそこまで拘らず、しかしある程度実際のプロジェクトで使いそうな部分を踏襲することを目標に製造してみます。

見た目はこのようになりました。

シンプルな見た目ですが、必要最低限のパーツは揃っているのではないかと思います。

続いてソースコードです。

少々長いですが、パスワードのマスクの切り替え、入力フィールドの検証、ログイン処理時のインジケータの表示など、ログイン画面を製造する際に使いそうな機能を盛り込んでみました。

長くなるので詳細は割愛しますが、LoginControllerでログインに関する処理を提供し、LoginUserControllerでログインユーザーの状態(未ログイン、ログイン済)を管理、LoginPageLoginControllerからログイン処理を呼び出し、LoginUserControllerにユーザーのインスタンスが流れてくると画面遷移、という流れとしています。  

テストする

製造した画面のテストコードを書いていきます。

まずは初期状態のテストから。

上記では、画面内の各Widgetを検索し、描画されているかや状態が適切かを評価しています。
テストコードについていくつかポイントがあるので説明していきます。

CommonFinders

まず最初にfindを用いた見慣れない構文が目に入ります。

final titleText = find.widgetWithText(AppBar, "ログイン");

これはFinderと呼ばれるWidgetの要素ツリーから特定の要素を検索してくれるクラスをインスタンス化している部分で、CommonFindersと呼ばれる条件に応じたFinderクラスを提供してくれるクラスを用いています。

CommonFindersはpackage:flutter_test/flutter_test.dartをimportすることで使用できるようになります。

条件の指定方法は複数ありますので、特定したいWidgetの特性に合わせてこちらの記事などを参考にしてみてください。

Finderクラスはインスタンスの生成時点で検索を実施するわけではないので、よく使うWidgetのFinderは予め定義しておくことで使い回すことができます。

testWidgets

これまでのUnitTestとの違いとして、テストコードはtestではなくtestWidgetsに記述しています。

testWidgetsはWidgetTesterを引き渡してくれる点がtestメソッドと異なっており、WidgetテストではこのWidgetTesterを用いてWidgetの描画や操作を実施していくことになります。

上記テストではWidgetTester.pumpWidgetを用いてLoginPageを描画するところからスタートしています。
今回はRiverpodを使用しているため、UncontrolledProviderScopeを用いてWidgetツリーにProviderContainerを公開するよう構成しています。

await tester.pumpWidget(
  UncontrolledProviderScope(
    container: container,
    child: const MaterialApp(
      home: LoginPage(),
    ),
  ),
);

またWidgetTesterはFinderで検索したWidgetを取得することもできるので、Widgetの状態を直接確認するためにも使用できます。

expect(tester.widget<TextField>(passwordTextField).obscureText, isTrue);

イベントの発火

画面はユーザーの操作によって状態が遷移する場合があります。
これらのユーザー操作によるイベントの試験もWidgetTesterを用いることで実施することができます。

上記ではパスワード入力フィールドのマスクのON/OFFの切り替えのテストと、loginId入力フィールドのみに値を設定しないことで入力検証のエラーが表示されているかを評価しています。
WidgetTester.enterTextでテキストフィールドへの文字列入力を、WidgetTester.tapでボタンのタップを実施しています。

await tester.enterText(passwordFormField, "8peJuzJ*naBd");
await tester.tap(loginButton);

評価前にイベント発火後の状態に描画更新する必要があり、WidgetTester.pumpで行うことができます。

await tester.pump();

これにより実際のアプリを操作した場合と同じUIを評価することができるようになります。

まとめ

いかがでしたでしょうか。
WidgetテストではWidgetTesterを用いることでユーザーの操作を模倣し、Finderを用いてWidgetの検索を実施するということがわかりました。
実際のプロジェクトではコストやスケジュールからWidgetのテストを記述する機会は多くないかもしれません。
しかし状態遷移が複雑なWidgetではWidgetテストを実施することで人力での試験より詳細なテストが実施できるケースもあるのではないかと思います。
今後は小さいところからWidgetテストも記述していきたいと思いました。

おまけ

今回のブログへ向けての実装時に、RiverpodのNotifierクラスのモックをMockitoで自動生成したところ、テスト実施時に以下のようなエラーが発生しました。

Class 'MockLoginController' has no instance method '_setElement'.
Receiver: Instance of 'MockLoginController'
Tried calling: _setElement(Instance of 'AutoDisposeAsyncNotifierProviderElement<LoginController,
void>')

エラーの内容としては_setElementメソッドがインスタンスに存在しないというエラーのようで、色々調べてみたところMockitoで自動生成したMockのクラスは元のクラスをimplementsするようになっていますが、publicなメソッドのみしか実装されないことが原因のようでした。

今回はモック対象のクラスが継承しているNotifierクラスを直接継承し、MockクラスをMixinする形でモックを独自実装することで回避しています。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.