[Flutter] 個人開発のアプリでGolden Testsを使ってみた

ある勉強会向けにFlutterでタイマーアプリを作成しました :timer: 

- [lttimer](https://github.com/wakamenod/lttimer)　　　

<img width="300" alt="unnamed.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/517b5b24-c7c6-065b-d47b-addb30fb52ce.png">


このアプリのウィジェットテストに、前々から試してみたかったGolden Testsを導入したので、その共有になります。


## Golden Testsとは
Golden Testsは、一般的にVRT(Visual Regression Test)と呼ばれるものです。ウィジェットテスト中にアプリのスクショを取って、変更前のスクショと比較することで、意図しない変更がないかを検知できます。

Golden Tests 自体はFlutterのフレームワークの一部として提供されている[機能](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter) ですが、より簡単かつ効率的にテストを実行する為の[golden_toolkit](https://pub.dev/packages/golden_toolkit)というパッケージがあり、こちらが使われることが多いと思います[^1]。自分のアプリでもgolden_toolkitを使ってGolden Testsを導入しました。

ちなみに`Golden`とは、とったスクショの画像ファイルを指すようです（なぜGolden...? :medal:  )


## 実際のアプリでの使用例

アプリ内の実際のGoldenテストの[ソースコード](https://github.com/wakamenod/lttimer/blob/master/test/src/screens/timer_screen_test.dart#L88)がこちらです:

``` dart
  testGoldens('Lightning talkセッションの30秒後', (WidgetTester tester) async {
    await loadAppFonts();
    const size6 = Size(375, 667);

    final settings = Settings(
        sessionMode: TimerType.lightning,
        sessionTime: const Duration(minutes: 2),
        longSessionTime: const Duration(minutes: 40),
        isContinuous: true,
        intervalTime: const Duration(minutes: 1),
        bell1: const Duration(seconds: 10),
        bell2: const Duration(seconds: 30),
        bell3: const Duration(seconds: 150), // セッション時間を超えているので表示されない
        longModeBell1: Duration.zero,
        longModeBell2: Duration.zero,
        longModeBell3: Duration.zero,
        isDarkMode: true,
        showCongratsAnimation: true,
        congratsDanmakuComments: '');

    final controller = TimerController();
    final container = await createProviderContainer(controller, settings);
    await tester.pumpWidgetBuilder(
        TestWidget(![golden_lightning_in_30sec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/63aada56-4794-3db5-f46e-dde14baa7a82.png)

          container: container,
        ),
        surfaceSize: size6);
    controller.elapse(const Duration(seconds: 30));
    await screenMatchesGolden(tester, 'golden_lightning_in_30sec');
  });
```

このテストでは、主に以下の三点を確認しています：

1. 30秒経過後のタイマーの秒数の表示 
1. 30秒経過後のタイマーの円の描画
2. タイマーに設定したベルの位置

このテストを用意して[Golden生成コマンド](https://github.com/wakamenod/lttimer/blob/master/Makefile.toml#L99)を実行すると、以下のようなスクショが撮られます。

![golden_lightning_in_30sec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/f91e0877-20d7-7f3b-6fa6-5d5afa613fe2.png)

画像を目視で確認し、問題なければこのファイルをGoldenとして採用します。

仮にこの後の実装で、タイマーの秒数表示にバグが混入したとします。すると、ウィジェットテスト実行時にテストが失敗し、以下の4種類の画像ファイルが生成されます。

![Screenshot from 2023-09-24 09-18-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/8c237896-eb23-fa3e-f48e-a1326fe1b3bf.png)

このように、UIのどの部分でバグが発生したのか分かるようになっています。

## ハマった点
Github Actionsを使ってテストを回しているのですが、ローカルでは通ってるGolden Testsが、CIだと落ちてしまう現象に遭遇しました。調べたところ、Goldenを生成したローカル環境のOSと、CIのOSを合わせておかないと、テキスト描画などで差分が出ててしまうようです... 

- [Golden files are not consistent between Flutter versions and different OS versions #36667](https://github.com/flutter/flutter/issues/36667)

こちらは、CIとローカルのOSを合わせることで解決しています。


## 感想
今回、初めてFlutterのGoldenテストを触ってみましたが、想像していたより使い所は多いと感じました。

Flutterでは色々な種類のテストが揃っているので、使い所を覚えていきたい！


[^1]: Very Good Venturesが[alchemist](https://pub.dev/packages/alchemist) というパッケージを作ってますが、あまり使用例は見かけません。