VRTのススメ - Flutter Golden Testによる視覚的回帰テストの実践
概要
Flutter開発において、UIの変更による意図しない視覚的バグを防ぐことは重要な課題です。今回は、Flutter Golden Testを活用したVRT(Visual Regression Testing)の実装について、実際のプロジェクトでの適用例を交えて解説します。
導入背景
今回VRTを導入したプロジェクトは、すでに数年間本番環境で稼働しているFlutterアプリケーションです。コードベースの書き方や構成は少し古めの部分もあり、最新のベストプラクティスとは異なる実装も存在します。
しかし、このアプリはまだまだサービスとして成長フェーズにあり、今後も継続的に機能追加や改修を行っていく予定です。そこで直面した課題が以下の点でした。
直面していた課題
-
レガシーコードへの不安
- 既存機能への影響範囲が把握しづらい
- リファクタリング時の意図しない副作用
-
レビューの限界
- 全画面の目視確認は現実的に困難
- レビュアーによる見落としリスク
-
開発速度とのバランス
- 慎重になりすぎて開発速度が低下
- 積極的な改善への心理的障壁
これらの課題を解決し、既存機能を守りながら積極的な開発を継続するために、VRTの導入を決定しました。
技術スタック
- Flutter 3.19.5+
- golden_toolkit 0.15.0
- flutter_test(標準テストフレームワーク)
VRTとは
Visual Regression Testing(VRT)は、UIの視覚的な変更を自動的に検出するテスト手法です。事前に作成されたスクリーンショット(Golden File)と現在のUIを比較し、差分がある場合にテストを失敗させます。
VRTの主なメリット
- 意図しないUI変更の検出:レイアウトの崩れやデザインの不具合を早期発見
- レビューの効率化:視覚的な変更を一目で確認可能
- リファクタリングの安全性: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,
);
}
学んだこと
意外だった落とし穴
-
ネットワーク画像の取り扱い
- CachedNetworkImageのモック化が困難
-
mockNetworkImagesFor
の使用が必須
-
非同期処理の安定化
-
pumpAndSettle
だけでは不十分な場合がある - 適切な待機時間の設定が重要
-
-
Golden Fileの管理
- 複数デバイス対応により大量のファイルが生成
- 適切な命名規則とディレクトリ構成が重要
今後使えそうな知見
-
テストの階層化
- 画面レベル、コンポーネントレベルでの段階的テスト
- 状態管理を含むインテグレーションテスト
-
CI/CDとの統合
- Golden Fileの自動更新フロー
- プルリクエストでの視覚的差分確認
-
パフォーマンス最適化
- 必要な部分のみのテスト実行
- 並列実行による時間短縮
もっと良い書き方の発見
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の更新が含まれていないか
- デバイス別の表示に問題はないか
運用のベストプラクティス
-
定期的なGolden Fileの整理
- 不要になったテストケースのファイルを削除
- ディレクトリ構造の整理
-
チーム内での認識合わせ
- Golden Test更新のタイミングをチームで共有
- レビュー基準の明文化
-
トラブルシューティング
- 環境差異による失敗時は、macOS環境で再実行
- フォントやアイコンの差異は、テスト用のモックフォントを使用
注意点
1. Golden Fileの管理
- Golden Fileはバージョン管理に含める
- 大量のファイルサイズに注意
- 定期的なファイルの整理が必要
2. テストの安定性
- 非同期処理の完了を確実に待機
- 外部依存の適切なモック化
- 環境依存の要素の排除
3. 運用面での考慮
- 開発者間でのGolden Fileの共有方法
- 意図的な変更時の更新手順
- レビュープロセスの確立
終わりに
VRTは、Flutter開発におけるUI品質の向上に大きく貢献します。特に、数年運用されているアプリケーションや、レガシーコードを含むプロジェクトにおいて、その効果は絶大です。
導入後の変化
-
開発者の心理的安全性の向上
- リファクタリングへの抵抗感が減少
- 「壊してしまうかも」という不安からの解放
-
レビューの質の向上
- 視覚的な差分により、影響範囲が一目瞭然
- レビュアーの負担軽減
-
開発速度の向上
- 安心して積極的な改善が可能に
- 既存機能への影響を気にせず新機能開発に集中
既存のアプリケーションが古い実装を含んでいても、それは導入を諦める理由にはなりません。むしろ、そういったプロジェクトこそVRTの恩恵を最大限に受けることができます。
適切に実装されたVRTは、開発者の安心感を高め、より積極的なコード改善を促進します。今回紹介したテクニックを参考に、プロジェクトに適したVRTの導入を検討してみてください。
Discussion