UPSIDER Tech Blog

Flutter Golden Test の実践的な導入と運用

はじめに

こんにちは。

今回はUPSIDERでモバイルアプリの開発を担当しているApp teamから、Golden Testの実践的な導入と運用について紹介します。

App teamでは開発効率と品質向上の両立を目指しており、全ての画面でGolden Testを活用し、80を超える画面でUI回帰テストを完全自動化しています。

今回は、これらの取り組みについて、詳しく解説していきます。

  • CI/CD自動化
  • カスタムComparatorの実装
  • フォント表示対応
  • テーマ別 × 状態別の網羅的テスト

CI/CD自動化

アプリの機能や画面数が増加するにつれて、UI修正や機能追加時の既存画面への影響を把握することが困難になり、意図しないUI変更がリリースされるリスクが高まりました。 この課題を解決するため、Golden Testを導入し、PR作成時・更新時のGolden Testの差分チェックを自動化しました。

PRチェック用ワークフロー

on:
  pull_request:
    types:
      - opened
      - synchronize
jobs:
  test_goldens:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout
      - uses: ./.github/actions/setup-flutter
        with:
          working-directory: ${{ env.WORKING_DIRECTORY }}
      - run: flutter test -r github > flutter_test_report.txt
      - uses: actions/upload-artifact@v4
        with:
          name: test-goldens
          path: |
            ${{ env.WORKING_DIRECTORY }}/flutter_test_report.txt

しかし、実際に運用してみると、開発者のローカル環境の違いにより、同じコードでも異なる画像が生成される問題が発生し、Golden Testで失敗することがありました。

そこで、GitHub ActionsでGolden Testの画像を生成することで、環境の違いによる画像の差異を解消し、安定したテスト実行を実現しました。また、Docker を用いてローカル環境でも生成できる手段を提供しています。

Golden Test更新用ワークフロー

- name: Generate golden master images
  run: flutter test -t golden --update-goldens
- name: Archive golden images
  uses: actions/upload-artifact
  with:
    name: goldens
    path: test/widget/**/goldens/*.png
    retention-days: 1
- run: |
  git add test/widget/**/goldens/*.png && \
    git -c user.name='${{ env.AUTHOR_NAME }}' -c user.email='${{ env.AUTHOR_EMAIL }}' commit -m '${{ env.MESSAGE }}' && \
    git push origin HEAD || true

カスタムComparatorの実装

GitHub Actionsでの自動生成により環境の違いは解消されましたが、 アンチエイリアスやサブピクセル描画のわずかな違いでGolden Testが失敗する問題が残っていました。そのため、軽微な差分を許容するComparatorを実装することで Golden Testの安定性を向上させました。

class _GoldenTestComparator extends LocalFileComparator {
  static const _threshold = 0.0001;

  @override Future<bool> compare(Uint8List imageBytes, Uri golden) async {
    final ComparisonResult result = await GoldenFileComparator.compareLists(
      imageBytes,
      await getGoldenBytes(golden),
    );

    if (!result.passed && result.diffPercent >= _threshold) {
      throw FlutterError(error);
    }
    return true;
  }
}

フォント表示対応

これまでの取り組みで、Golden Testが安定的に運用できるようになりました。 しかし、フォントの読み込みや絵文字の表示で問題が発生しました。

  • CI環境・ローカル環境・OS間で異なるフォントが適用され、テキスト折り返しや行間が一致しない
  • システムフォントでは日本語や絵文字が正しく表示されず、豆腐文字や欠落が発生する

これらの問題を解決するため、以下の方法で対応しています。

  1. FontManifest.jsonを読み込んで、アプリで使用可能なフォント一覧を取得
  2. 日本語フォント(NotoSansJP)と絵文字フォント(NotoColorEmoji)を追加
  3. font_loader*を使って、追加したフォントを実際に読み込み

*eBayが提供していたgolden_toolkitというpackageを使用しており、この中で実装されているfont_loaderの仕組みをカスタマイズして使用しています。

参考実装: https://github.com/eBay/flutter_glove_box/blob/master/packages/golden_toolkit/lib/src/font_loader.dart

static Future<void> _loadAppFonts() async {
  final fontManifest = await rootBundle.loadStructuredData<List<dynamic>>( 
    'FontManifest.json',
    (string) async => json.decode(string),
  ); 
  final fonts = {
    _defaultFontFamily: './test/assets/fonts/NotoSansJP-Regular.ttf',
    _fallbackEmojiFontFamily: './test/assets/fonts/NotoColorEmoji-Regular.ttf',
  };
  fontManifest.addAll(
    fonts
      .map(
        (key, value) => MapEntry(key, {
          'family': key,
          'fonts': [
            {'asset': File(value).absolute.path},
          ],
        }),
      )
      .values,
   ); 
  for (final font in fontManifest) {
    final fontLoader = FontLoader(font['family']);
    for (final fontType in font['fonts']) {
      fontLoader.addFont(
        rootBundle.load(Uri.decodeComponent(fontType['asset'])), 
      ); 
    }
    await fontLoader.load(); 
  } 
}

テーマ別 × 状態別の網羅的テスト

安定したGolden Testの運用基盤が整ったことで、最後のステップとして網羅的なテストカバレッジの実現に取り組みました。 ライト / ダークテーマごとに生成し、各画面に対するテストでは、Content / Loading / Error を基本として各状態を網羅したテストを実施しています。 {theme}.{page}.{state}.png の形式で統一し、誰でも差分を把握しやすくしています。

// ダークテーマ
dark_theme.home_page.content.png // 通常状態
dark_theme.home_page.loading.png // ローディング状態
dark_theme.home_page.error.png // エラー状態

// ライトテーマ
light_theme.home_page.content.png // 通常状態
light_theme.home_page.loading.png // ローディング状態
light_theme.home_page.error.png // エラー状態

まとめ

Golden Testを導入・運用していく中で、開発効率と品質向上の両立を実現しています。

開発効率の向上

ライブラリの更新や共通コンポーネントの修正時に、意図しないUIの変更を自動で検出できるようになりました。 これにより、UIを壊すことなく安全にリファクタリングができ、新機能追加時の既存画面への影響も事前に把握できます。 また、共通コンポーネントの変更が全体にどう影響するかが一目で分かるため、デザインシステムの一貫性を保つことができます。

チーム連携の改善

チーム外との連携が格段に効率的になりました。 また、UI変更箇所を画像で確認できるため、コードレビューの精度と速度が向上しています。

UPSIDERのモバイルアプリ開発では、こうした細かなUI・UX改善にも日々取り組んでいます。同様の課題に取り組んでいる方の参考になれば幸いです。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。
ぜひお気軽にご応募ください。 herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com