😽

FlutterアプリにVRTを導入する

2023/10/10に公開

はじめに

センキャクの開発では、lintや単体テストのほか、VRTを導入し、画面差分チェックを行って、デザインの崩れがないかテストしています。

FlutterのVRTの実行には、test用に便利なパッケージが提供され、golden_toolkitが有名です。今回は、カタログ作成で利用するwidgetbookを使って、VRT環境を作成します。

目次

  • パッケージの初期設定
  • Widgetにカタログ用の設定を追加する
  • スナップショット用のテストを作成する
  • テストの実行

パッケージの初期設定

widgetbookは、3つのパッケージで構成されています。widgetbook単体でも使えますが、アノテーションによる自動生成を使うと、簡単に導入できます。

パッケージ名 機能
widgetbook widgetbook本体
widgetbook_annotation widgetbookのカタログ生成用のアノテーションを付加
widgetbook_generator アノテーションの設定を元にカタログ用Dartを生成

widgetbookとwidgetbook_annotationをdependencies、widgetbook_generatorをdev_dependenciesに追加します。

flutter pub add widgetbook widgetbook_annotation dev:widgetbook_generator

Widgetにカタログ用の設定を追加する

アノテーションの設定

作成したWidgetに対して、widgetbookのアノテーションを設定して書き出されるようにします。
UseCaseにnameとtypeを指定し、作成したWidgetを返す関数を定義します。最終的に一つの定義でまとまるので、定義するメソッドは他のWidgetでも被らないようにしてください。作成したコンポーネントとは別のファイル(*.story.dart)を用意して、実際に使用する際は読み込まないようにします。

import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

.UseCase(
  name: 'Button',
  type: Button,
)
Widget $ButtonBasic(BuildContext context) => Button();

カタログの設定ファイルの生成

カタログの設定ファイルを生成するために、カタログ表示用のアプリ(widgetbook.generator.dart)を作成します。その際に、@windgetbook.App()を定義して、カタログの設定ファイルが書き出されるようにします。

import 'package:app/widgetbook.generator.directories.g.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

void main() {
  runApp(const ProviderScope(child: WidgetbookApp()));
}

.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});

  
  Widget build(BuildContext context) {
    return Widgetbook.material(
      directories: directories,
      addons: [],
      appBuilder: (context, child) => child,
    );
  }
}

build_runnerを実行すると、ファイル(widgetbook.generator.directories.g.dart)が生成されます。

flutter packages pub run build_runner build

スナップショット用のテストを作成する

testファイルの作成

FlutterのVRTには、Wigdetに対して保存されている画像を比較するmatchesGoldenFile関数が用意されています。

matchesGoldenFileには二つの機能があり、画像のアップデートと比較処理を行います。通常では、画像を比較するのですが、testの実行時のオプション(--update-goldens)をつけると、指定されたパスにWidgetの画像を書き出します。

今回、画像の比較自体は、別の比較ツールを使うので、画像の生成のみを行うようにし、通常時ではパスするようにします。autoUpdateGoldenFilesという変数がtestパッケージにあり、これがtrueの場合は、--update-goldensがついている状態になります。したがって、trueでない場合にテストを実行しないように処理を中断して、スキップさせます。

import 'package:flutter_test/flutter_test.dart';

Future<void> main() async {
  TestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Widgetbook snapshots', (tester) async {
    if (!autoUpdateGoldenFiles) {
      tester.printToConsole('Skip Snapshot');
      return;
    }
  });
}

設定の読み込み

build_runnerで書き出したカタログの設定ファイルを読み込んで、テストを逐次実行できるようにします。書き出された設定はツリー構造になっているので、コンポーネントだけを処理できるようにリストに変換します。

import 'package:app/widgetbook.generator.directories.g.dart';
import 'package:widgetbook/src/navigation/models/models.dart';
import 'package:widgetbook/widgetbook.dart';

final items = _loadItems('', directories);

Map<String, WidgetbookUseCase> _loadItems(
  String path,
  Iterable<NavigationNodeDataInterface> useCases,
) {
  Map<String, WidgetbookUseCase> result = {};
  useCases.forEach(
    (e) {
      if (e is MultiChildNavigationNodeData) {
        final children = _loadItems('$path/${e.name}', e.children);
        result.addAll(children);
      } else if (e is WidgetbookUseCase) {
        result['$path/${e.name}'] = e;
      }
    },
  );

  return result;
}

スナップショットの保存

リストからWidgetを生成して、一つずつテストを実行します。matchesGoldenFileにパスを指定すると、そのパスにファイルを保存します。

for (final item in items.entries) {
  final scenarioWidget = MaterialApp(
    title: item.key,
    debugShowCheckedModeBanner: false,
    home: Material(
      child: Builder(builder: item.value.builder),
    ),
  );

  await tester.pumpWidget(scenarioWidget);
  await tester.pumpAndSettle();

  await expectLater(
    find.byWidget(scenarioWidget),
    matchesGoldenFile(
      'screenshots${item.key}.png',
    ),
  );
}

テストの実行

以上で、Flutter上でスナップショットを書き出すところまで作成できました。テストの実行には、最初にbuild_runnerを実行して、その後testを実行すれば、画像が書き出されます。

flutter packages pub run build_runner build
flutter test --update-goldens

画像比較用のreg-suitを設定し、それに合わせたGithub Actionsを作成すれば、テスト環境の構築は完了します。reg-suitとGithub Actionsの設定については、今回は割愛したいと思います。

おわりに

  • 今回はFlutterアプリにVRTを導入する方法についてまとめてみました。みなさんもぜひ試してみてください。
  • センキャクでは一緒にプロダクト開発をしてくれる仲間を絶賛募集しています。少しでもご興味ある方はこちらから。
センキャク Tech Blog

Discussion