👀

VRTのススメ - Flutter Golden Testによる視覚的回帰テストの実践

に公開

概要

Flutter開発において、UIの変更による意図しない視覚的バグを防ぐことは重要な課題です。今回は、Flutter Golden Testを活用したVRT(Visual Regression Testing)の実装について、実際のプロジェクトでの適用例を交えて解説します。

導入背景

今回VRTを導入したプロジェクトは、すでに数年間本番環境で稼働しているFlutterアプリケーションです。コードベースの書き方や構成は少し古めの部分もあり、最新のベストプラクティスとは異なる実装も存在します。

しかし、このアプリはまだまだサービスとして成長フェーズにあり、今後も継続的に機能追加や改修を行っていく予定です。そこで直面した課題が以下の点でした。

直面していた課題

  1. レガシーコードへの不安

    • 既存機能への影響範囲が把握しづらい
    • リファクタリング時の意図しない副作用
  2. レビューの限界

    • 全画面の目視確認は現実的に困難
    • レビュアーによる見落としリスク
  3. 開発速度とのバランス

    • 慎重になりすぎて開発速度が低下
    • 積極的な改善への心理的障壁

これらの課題を解決し、既存機能を守りながら積極的な開発を継続するために、VRTの導入を決定しました。

技術スタック

  • Flutter 3.19.5+
  • golden_toolkit 0.15.0
  • flutter_test(標準テストフレームワーク)

VRTとは

Visual Regression Testing(VRT)は、UIの視覚的な変更を自動的に検出するテスト手法です。事前に作成されたスクリーンショット(Golden File)と現在のUIを比較し、差分がある場合にテストを失敗させます。

VRTの主なメリット

  1. 意図しないUI変更の検出:レイアウトの崩れやデザインの不具合を早期発見
  2. レビューの効率化:視覚的な変更を一目で確認可能
  3. リファクタリングの安全性:UI変更を伴わないコード変更の保証

実装内容

1. Golden Testの基本的な構成

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';

void main() {
  group('SamplePage', () {
    testGoldens('should match golden file', (tester) async {
      final widget = MaterialApp(
        home: SamplePage(),
      );

      await mockNetworkImagesFor(() async {
        final builder = DeviceBuilder()
          ..overrideDevicesForAllScenarios(devices: [TestDevice.iPhone15])
          ..addScenario(
            name: 'logged_in',
            widget: widget,
          );

        await tester.pumpDeviceBuilder(builder);

        await screenMatchesGolden(tester, 'SamplePage');
      });
    });
  });
}

2. 複数デバイスでのテスト

final builder = DeviceBuilder()
  ..overrideDevicesForAllScenarios(devices: [
    TestDevice.iPhone15,
    TestDevice.iPhone15ProMax,
    TestDevice.pixel4,
  ])
  ..addScenario(
    name: 'logged_in',
    widget: targetWidget(userState: createTestUserState()),
  )
  ..addScenario(
    name: 'not_logged_in',
    widget: targetWidget(userState: null),
  );

3. 複数の状態でのテスト

testGoldens('should handle different states', (tester) async {
  final scenarios = [
    ('logged_in', createTestUserState()),
    ('not_logged_in', null),
    ('user_not_passed', createTestUserState(passed: false)),
  ];

  for (final (name, userState) in scenarios) {
    await mockNetworkImagesFor(() async {
      final builder = DeviceBuilder()
        ..overrideDevicesForAllScenarios(devices: [TestDevice.iPhone15])
        ..addScenario(
          name: name,
          widget: targetWidget(userState: userState),
        );

      await tester.pumpDeviceBuilder(builder);

      await screenMatchesGolden(tester, 'SamplePage_$name');
    });
  }
});

4. テストヘルパーの活用

// test_helpers.dart
Widget createTestApp({
  required Widget child,
  UserState? userState,
}) {
  return ProviderScope(
    overrides: [
      if (userState != null) userViewModelProvider.overrideWith(
        (ref) => createMockUserViewModel(userState),
      ),
    ],
    child: MaterialApp(
      home: child,
    ),
  );
}

UserState createTestUserState({
  bool passed = true,
}) {
  return UserState(
    id: 1,
    name: 'Test User',
    passed: passed,
  );
}

学んだこと

意外だった落とし穴

  1. ネットワーク画像の取り扱い

    • CachedNetworkImageのモック化が困難
    • mockNetworkImagesForの使用が必須
  2. 非同期処理の安定化

    • pumpAndSettleだけでは不十分な場合がある
    • 適切な待機時間の設定が重要
  3. Golden Fileの管理

    • 複数デバイス対応により大量のファイルが生成
    • 適切な命名規則とディレクトリ構成が重要

今後使えそうな知見

  1. テストの階層化

    • 画面レベル、コンポーネントレベルでの段階的テスト
    • 状態管理を含むインテグレーションテスト
  2. CI/CDとの統合

    • Golden Fileの自動更新フロー
    • プルリクエストでの視覚的差分確認
  3. パフォーマンス最適化

    • 必要な部分のみのテスト実行
    • 並列実行による時間短縮

もっと良い書き方の発見

1 . テストユーティリティの活用

extension GoldenTestExtension on WidgetTester {
  Future<void> pumpAndSettleWithNetworkImages(
    Widget widget, {
    Duration timeout = const Duration(seconds: 10),
  }) async {
    await mockNetworkImagesFor(() async {
      await pumpWidget(widget);
      await pumpAndSettle(timeout);
    });
  }
}

運用方法

Golden Testの更新手順

Golden Testは、UIの意図的な変更を反映させる必要があります。以下の手順で運用します:

1. ローカルでの更新コマンド

flutter test --update-goldens

2. 更新が必要なタイミング

  • UI変更後: デザイン変更、レイアウト調整、新しいUI要素の追加時
  • テーマ変更後: カラーテーマ、フォント、スタイル変更時
  • 新規画面追加時: 新しい画面のGolden Testを初回実装時
  • 意図的なビジュアル変更: 仕様変更によるUI更新時

3. 更新時の注意点

  • Golden Testの更新前に、変更内容が意図したものであることを必ず確認
  • 予期しないビジュアル変更の場合は、まず原因を調査してから更新
  • macOS環境での実行を推奨(環境による描画差異を避けるため)
  • 改修完了後、意図した範囲でのUIの変更の場合は、差分の出た画像ファイルも一緒にcommit

CI/CDでの自動テスト

GitHub Actionsを使用して、プルリクエスト時に自動的にVRTを実行します:

1. テストの分離実行

name: Flutter CI

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test-type:
          - name: 'Unit Tests'
            command: 'flutter test --exclude-tags golden --coverage'
          - name: 'Static Analysis'
            command: 'flutter analyze --no-fatal-infos'
  
  golden-tests:
    runs-on: macos-latest  # macOSで実行して環境差異を最小化
    name: Golden Tests
    steps:
      - name: Run Golden Tests
        run: flutter test --tags golden

2. CI運用のポイント

  • 環境の統一: macOS環境でGolden Testを実行し、開発環境との差異を最小化
  • テストの分離: 通常のテストとGolden Testを分けて並列実行
  • タグによる管理: --tags goldenでGolden Testのみを選択的に実行

レビュープロセス

1. プルリクエストでの確認事項

  • Golden Fileの差分が意図した変更と一致しているか
  • 影響範囲が想定内か
  • 他の画面に予期しない変更がないか

2. 差分の見方

# GitHubのFiles changedタブで画像差分を確認
# 変更前後の画像を並べて比較可能

3. レビューのチェックリスト

  • UIの変更は仕様通りか
  • 変更の影響範囲は適切か
  • 不要なGolden Fileの更新が含まれていないか
  • デバイス別の表示に問題はないか

運用のベストプラクティス

  1. 定期的なGolden Fileの整理

    • 不要になったテストケースのファイルを削除
    • ディレクトリ構造の整理
  2. チーム内での認識合わせ

    • Golden Test更新のタイミングをチームで共有
    • レビュー基準の明文化
  3. トラブルシューティング

    • 環境差異による失敗時は、macOS環境で再実行
    • フォントやアイコンの差異は、テスト用のモックフォントを使用

注意点

1. Golden Fileの管理

  • Golden Fileはバージョン管理に含める
  • 大量のファイルサイズに注意
  • 定期的なファイルの整理が必要

2. テストの安定性

  • 非同期処理の完了を確実に待機
  • 外部依存の適切なモック化
  • 環境依存の要素の排除

3. 運用面での考慮

  • 開発者間でのGolden Fileの共有方法
  • 意図的な変更時の更新手順
  • レビュープロセスの確立

終わりに

VRTは、Flutter開発におけるUI品質の向上に大きく貢献します。特に、数年運用されているアプリケーションや、レガシーコードを含むプロジェクトにおいて、その効果は絶大です。

導入後の変化

  1. 開発者の心理的安全性の向上

    • リファクタリングへの抵抗感が減少
    • 「壊してしまうかも」という不安からの解放
  2. レビューの質の向上

    • 視覚的な差分により、影響範囲が一目瞭然
    • レビュアーの負担軽減
  3. 開発速度の向上

    • 安心して積極的な改善が可能に
    • 既存機能への影響を気にせず新機能開発に集中

既存のアプリケーションが古い実装を含んでいても、それは導入を諦める理由にはなりません。むしろ、そういったプロジェクトこそVRTの恩恵を最大限に受けることができます。

適切に実装されたVRTは、開発者の安心感を高め、より積極的なコード改善を促進します。今回紹介したテクニックを参考に、プロジェクトに適したVRTの導入を検討してみてください。

参考リンク

91works Tech Blog

Discussion