【Flutter】screenutilに対応したGolden Testの実装

# はじめに
現在進めているプロジェクトでは画面サイズに合わせてWidgetやフォントサイズを調整できるよう
[flutter_screenutil](https://pub.dev/packages/flutter_screenutil)を導入しています。

Widget Test実装時はGolden Testで実装しているのですがその際に詰まったことを備忘録がてら書きます。

# 前提
[flutter_screenutil](https://pub.dev/packages/flutter_screenutil)とは？
Widgetやフォントサイズを動的に変更できる便利なパッケージです。
詳細な説明は省きますが公式を見て初期設定さえすれば簡単に実装することができます。

Golden Testについて
Golden Testの詳細は省きますが今回の対応では[goolden_toolkit](https://pub.dev/packages/golden_toolkit)を利用しています。
Flutterではパッケージを導入せずともGolden Testの実装は可能ですが、よりテストを実装しやすくするために上記のパッケージを導入しています。

今回のソースコードはGithubにおいてあるので詳細気になる方は見てください。

https://github.com/taiseiiiii/screenutil_golden_demo


# 目次
1. [簡単なサンプルの確認](#簡単なサンプルの確認)
2. [実際に発生したエラー](#実際に発生したエラー)
3. [解消法](#解消法)
4. [最後に](#最後に)
5. [参考文献](#参考文献) 

# 簡単なサンプルの確認
Flutterのチュートリアルでも利用されているカウンターアプリにGolden Testを実装してみます。
flutter_test_configを以下のように実装します。flutter_test_configの詳細については以下を読めばわかりやすいと思います。
[flutter_test library](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html)

テストコード実装の際にWidget Testの対象となるWidgetを指定すると思うのですがその時にpageWrapperでラップするとプロジェクト独自のtheme等の設定を適用できるので以下の様な構成にしています。

TestDeviceではgolden_tookitで異なるデバイスのUIを確認する際のデバイス一覧を定義しています。
```Device.iphone11```の様な書き方をすると簡単にDeviceを定義することもできます。


```:flutter_test_config.dart
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  return GoldenToolkit.runWithConfiguration(
    () async {
      await loadAppFonts();
      await testMain();
    },
    config: GoldenToolkitConfiguration(
      // Currently, goldens are not generated/validated in CI for this repo. We have settled on the goldens for this package
      // being captured/validated by developers running on MacOSX. We may revisit this in the future if there is a reason to invest
      // in more sophistication
      skipGoldenAssertion: () => !Platform.isMacOS,
      enableRealShadows: true,
    ),
  );
}

Widget pageWrapper(Widget widget) =>
  MaterialApp(
    // ThemeやfontoFamiliyの指定はココで
    home: widget,
    debugShowCheckedModeBanner: false,
  );

class TestDevice {
  static const all = [
    Device(
      size: Size(375, 667), 
      name: 'iPhoneSE',
      devicePixelRatio: 3,
    ),
    Device(
      size: Size(375, 812), 
      name: 'iPhone13Mini',
      devicePixelRatio: 3,
    ),
    Device(
      size: Size(428, 926), 
      name: 'iPhone13ProMax',
      devicePixelRatio: 3,
    ),
  ];
}
```

肝心のテストコードは以下です。
```:screenutil_golden_test.dart
void main() {
  testGoldens('MyHomePage', (tester) async {
    final builder = DeviceBuilder()
      ..overrideDevicesForAllScenarios(devices: TestDevice.all)
      ..addScenario(
        widget: materialWrapper(const MyHomePage(title:'screenutil_golden_demo')),
        name: 'default',
      );

    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'MyHomePage');
  });
}

```
# 実際に発生したエラー
```flutter test --update-goldens```すると各デバイスサイズのスクリーンショットが生成されるイメージだったのですが以下のエラーが発生しました。

```
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following LateError was thrown building MyHomePage(dirty, state: _MyHomePageState#9eb02):
LateInitializationError: Field '_minTextAdapt@249084504' has not been initialized.

The relevant error-causing widget was:
  MyHomePage

When the exception was thrown, this was the stack:
#0      ScreenUtil._minTextAdapt (package:flutter_screenutil/src/screen_util.dart)
#1      ScreenUtil.scaleText (package:flutter_screenutil/src/screen_util.dart:177:7)
#2      ScreenUtil.setSp (package:flutter_screenutil/src/screen_util.dart:204:44)
#3      SizeExtension.sp (package:flutter_screenutil/src/size_extension.dart:18:33)
#4      _MyHomePageState.build (package:screenutil_golden_demo/main.dart:53:65)
#5      StatefulElement.build (package:flutter/src/widgets/framework.dart:5080:27)
#6      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4968:15)
#7      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5133:11)
#8      Element.rebuild (package:flutter/src/widgets/framework.dart:4690:5)
#9      ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4950:5)
#10     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5124:11)
#11     ComponentElement.mount (package:flutter/src/widgets/framework.dart:4944:5)
...     Normal element mounting (214 frames)
```

スタックトレースを確認するとscreenutilに起因するエラーっぽい。。
screenutilパッケージを利用する際の初期設定がテストコードでは反映されていないことが原因っぽいことはわかるような。。

以下のようにpageWrapperを書き換えて見ましたが今度はスクリーンショットの解像度？がおかしくなる。。
```:flutter_test_config.dart
Widget pageWrapper(Widget widget) =>
  ScreenUtilInit(
    designSize: const Size(375,812),
    builder: (context, child) =>
      MaterialApp(
        home: widget,
        debugShowCheckedModeBanner: false,
      )
  );
```

実際に生成されたスクリーンショットは以下
![スクリーンショット 2023-06-11 22.56.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3126590/e8a6125c-6e1b-f09b-6f77-0822990796fa.png)

# 解消方法

https://github.com/OpenFlutter/flutter_screenutil/issues/115

screenutilのリポジトリでそれっぽいissueがたてられていました。
MediaQueryでラップする必要があるっぽい。。？
とりあえず上記で指定されたやり方でpageWrapperを実装してみます。
```flutter_test_config.dart
class _Wrapper extends StatelessWidget {
  const _Wrapper(this.child);
  final Widget child;
  @override
  Widget build(BuildContext context) {
    ScreenUtil.init(context, designSize: const Size(1400, 926),);
    return child;
  }
}

Widget pageWrapper(Widget widget) => 
  MediaQuery(
    data: const MediaQueryData(), 
    child: MaterialApp(
      home: Scaffold(body: _Wrapper(widget)),
      debugShowCheckedModeBanner: false,
    ),
  );
```
テスト実行してみるとうまくいっているようです。
![スクリーンショット 2023-06-11 23.04.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3126590/5beb4711-c176-4a66-a446-982d2394b2ef.png)

# 最後に
なぜテストコード側だけMediaQueryでラップする必要があるかは分からないですが公式のexampleを見た感じだと同様にテストコードを実装していたのでこのやり方で問題はなさそうです。
パッケージの実装を確認しましたが、レスポンシブ対応するためにMediaQueryを利用していました。
なので、テストコード側もMediaQueryでラップして画面サイズ等の情報を定義する必要があるといった理屈なんですかね。
もっとクリティカルな根拠を見つけたら追記します。

# 参考文献
- [flutter_screenutil](https://pub.dev/packages/flutter_screenutil)
- [goolden_toolkit](https://pub.dev/packages/golden_toolkit)
- [flutter_test library](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html)
- [用了 screenutil, 想问一下要怎么用 widget test 确保 widgets 的高度和宽度在任何的屏幕 size 都不会造成 overflow？](https://github.com/OpenFlutter/flutter_screenutil/issues/115)
- [公式のテストコード](https://github.com/OpenFlutter/flutter_screenutil/blob/master/example/test/widget_test.dart)