Flutter-GetX下でのView(Widget)のテスト

FlutterのView(Widget)のテストで、状態管理やルート管理にGetXを使用していた場合のテスト側の実装方法について。

  • 2022/11/29修正
    正しく動いた例 に関して考察と対処が間違っていたので修正。
    tearDown(Get.reset);
    を入れるべきなのが正しい実装だった。
    GetXパッケージのページhttps://pub.dev/packages/getにも書かれていたのに、恥ずかしい。

経緯

ViewとViewModelを使った画面で、国際化対応の表示テスト(Flutter-Widget Test時の日本語表示参照)をしていた時に正しい表示にならなかった。
問題が出た例と、正しく動いた例で解説したい。

悪い例

Flutter-Widget Test時の日本語表示のテスト用コードに、ViewModel側情報をバインドした実装が悪い例になる。

void main() {
  for (final lang in ["ja", "en"]) {
    ThemeData? theme;
    Locale? locale;
    switch (lang) {
      case "en":
        locale = const Locale('en', 'US');
        theme = null;
        break;
      case "ja":
        locale = const Locale('ja', 'JP');
        theme = ThemeData(fontFamily: "IPAGothic");
        break;
    }
    testGoldens('View1_$lang', (WidgetTester tester) async {
      final testWidget = GetMaterialApp(
          locale: locale,
          theme: theme,
          home: テスト用ウィジェット()
          initialBinding: BindingsBuilder(() => Get.put<ViewModel>(_Test())),);

      await tester.pumpWidgetBuilder(testWidget);
      await screenMatchesGolden(tester, 'test${lang}_post');
    });
  }
}

想定していた動作は、ViewModelというクラスがテスト用ウィジェット作成前に生成され、それが適用される。日本語環境、英語環境のループ処理内で、各々別々のViewModelが生成される。

と思っていたのだけど、実際にはViewModelは1度しか生成されず、日本語環境のテストの後の英語環境のテストで、そのオブジェクトが継続されて使用されてしまった。

そのため、想定していた画面表示と異なる表示となってしまった。

その時のGetXのデバッグメッセージが以下になる。

LoginViewModelというのがViewModelになのだけど、日本語環境でのテスト完了時に削除されず、(たぶん)そのまま英語環境で使用されているのだろうと思われるログだった。

正しい実装

void main() {
  tearDown(Get.reset); // これが必要だった
  for (final lang in ["ja", "en"]) {
    ThemeData? theme;
    Locale? locale;
    switch (lang) {
      case "en":
        locale = const Locale('en', 'US');
        theme = null;
        break;
      case "ja":
        locale = const Locale('ja', 'JP');
        theme = ThemeData(fontFamily: "IPAGothic");
        break;
    }
    testGoldens('View1_$lang', (WidgetTester tester) async {
      final testWidget = GetMaterialApp(
          locale: locale,
          theme: theme,
          home: テスト用ウィジェット()
          initialBinding: BindingsBuilder(() => Get.put<ViewModel>(_Test())),);

      await tester.pumpWidgetBuilder(testWidget);
      await screenMatchesGolden(tester, 'test${lang}_post');
    });
  }
}

tearDown(Get.reset); を入れて、GetXの設定状態をリセットすることで対応できた。

正しく動いた例

他のテストで、Get.toでページをpushするようなケースでは正しく動いていたようだったので、ページを表示するような実装にして見た。
テスト用のウィジェット作成部分だけだけど。

final testWidget = GetMaterialApp(
  locale: locale,
  theme: theme,
  initialRoute: "/test",
  getPages: [
    GetPage(
        name: "/test",
        page: () => const テスト用のビュー(),
        binding: BindingsBuilder(
          () {
            Get.put<ViewModel>(_Test());
          },
        )),
  ],
);

以上の実装で、ViewModel側が正しく作成されるようになった。

Get.toを使うかgetPagesで埋め込むか

補足だけど、Viewのテストをする際にGet.toを使うか、GetMaterialAppにgetPagesで埋め込むかどちらが良いかについて。

これはアプリケーション内でGet.to(もしくはpush)で表示させるものは、テスト環境でもGet.toを使ったほうが良いと思う。
これは、戻るボタンを表示させるという意味と、前の画面に戻すという操作のテストを行うため。

それ以外、ルートになるようなページの場合はGetMaterialAppにgetPagesで埋め込んだ方がいい。
こちらは戻るボタンが表示されないから。

Viewのテスト方法

さらに補足。

GetXでViewとViewModelで分離させた場合にView側のテストでは、ViewModel側クラスをextendsで派生させ、Viewから呼び出すものをほぼすべてオーバーライドしてテストするようにしている。

ただテストと言っても、View側から指定されたメソッドが呼び出されているかという簡単なテストのみ。
テスト用メソッドでは呼び出した関数名を保存しておいて、その関数名との一致テストをするような感じ。

そのためView側のテストをより簡単にするため、View側では複雑な実行文を記述せず、すべてViewModel側に委譲するようにしてみた。
画面更新のタイミングをViewModel側で指示できるGetXならではかもしれない。
(StatefulWidgetを使うのではなくGetBuilder<T>を使うようにしたり)

2つほど例を挙げると、以下の様な感じかな。

  • bool値のトグル操作
    value = !value
    と書くところを関数化してViewModel側に委譲する。
  • IconButtonのonPressedのメソッド登録
    IconButtonの選択可否はonPressedにnullがあるかFunctionが登録されているかで判断されていることもあり、value ? funcion : nullという書き方が多いかもしれない。
    この部分もViewModel側に移してしまう。
    void Function()?を返す関数をViewModel側に実装するような感じ。

 

コメント

タイトルとURLをコピーしました