😊

Flutterアプリに画像回帰テストを導入する方法(Alchemist + GitHub Actions)

に公開

こんにちは、takuroです。
今回は、Flutterアプリに画像回帰テスト(Visual Regression Test)を導入方法を紹介します。

背景

開発中のUIコンポーネントに対し、以下のような課題を感じていました

  • デザインの崩れやリグレッションがレビューでしか検出されない
  • PRごとにUIの変化を確認するのが手間
  • コンポーネントが増えるほど、人的チェックのコストが増大

これらの課題を解決するために、「UIの見た目をテストする仕組み」として画像回帰テストの自動化を導入しました。

Alchemistとは

Alchemistは、FlutterアプリケーションのUIコンポーネントを対象に、スクリーンショットを自動生成してGoldenテストを行うためのライブラリです。
Goldenテストとは、UIの見た目を画像として保存し、変更が加えられた際に差分を検出するテスト手法です。これにより、デザインの崩れやリグレッションを効率的に検出できます。

主な特徴:

  • 複数のUIバリエーションを一括でテスト可能
  • テーマやデバイスごとの描画をサポート

Alchemistを活用することで、UI品質を保ちながら開発効率を向上させることができます。

実装

実装環境

flutter: 3.29.3
alchemist: 0.10.0

1. alchemistの設定

pubspec.yamlに依存関係を追加

dev_dependencies:
alchemist: ^0.10.0

2. Goldenテストの実装

まず、Alchemistを使ってGoldenテストを実装します。以下のコードは、ボタンコンポーネントのさまざまなバリエーションをテストする例です。
Alchemistを使うことで、複数のバリエーションのUIを一括でGoldenテストできます。

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

void main() {
  goldenTest(
    'Button variants render correctly',
    fileName: 'button_variants',
    builder: () => GoldenTestGroup(
      children: [
        GoldenTestScenario(
          name: 'Default (enabled)',
          child: ElevatedButton(
            onPressed: () {},
            child: const Text('Default'),
          ),
        ),
        GoldenTestScenario(
          name: 'Disabled',
          child: const ElevatedButton(
            onPressed: null,
            child: Text('Disabled'),
          ),
        ),
        GoldenTestScenario(
          name: 'Primary color',
          child: ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.blue,
            ),
            onPressed: () {},
            child: const Text('Primary'),
          ),
        ),
        GoldenTestScenario(
          name: 'Secondary color',
          child: ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.green,
            ),
            onPressed: () {},
            child: const Text('Secondary'),
          ),
        ),
        GoldenTestScenario(
          name: 'Small size',
          child: SizedBox(
            height: 30,
            child: ElevatedButton(
              onPressed: () {},
              child: const Text('Small'),
            ),
          ),
        ),
        GoldenTestScenario(
          name: 'Large size',
          child: SizedBox(
            height: 60,
            child: ElevatedButton(
              onPressed: () {},
              child: const Text('Large'),
            ),
          ),
        ),
      ],
    ),
  );
}

4. ゴールデン画像の生成

ゴールデン画像を生成するために、以下のコマンドを実行します。

flutter test --update-goldens

これにより、goldensディレクトリに画像が保存されます。

5. CI/CDの設定

ワークフローでは、baseブランチでスナップショットを取得し、targetブランチでスナップショットを撮影して比較します。
これにより、画像の取り直しの手間を削減し、効率的なテストが可能になります。

6. ベースライン管理用のWorkflow追加

developやfeature/**ブランチにPushされた際に、ベースライン画像を生成・アップロード。

このワークフローは:

  1. 依存関係をインストール
  2. ゴールデンテストを実行し、ゴールデン画像を生成
  3. ゴールデン画像をアーティファクトとしてアップロード
name: golden-baseline

permissions:
  contents: read

on:
  push:
    branches:
      - develop
      - feature/**

jobs:
  build-goldens:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Run golden tests
        run: flutter test --update-goldens

      - name: Upload golden images
        uses: actions/upload-artifact@v4
        with:
          name: golden-baseline
          path: test/**/goldens/*.png

これにより、比較対象のゴールデン画像を常に最新状態に保てます。

7. PR時の差分チェック用Workflow追加

PRが作成された際に、ゴールデン画像と現在のUIを比較し、差分がある場合はPRを失敗させるWorkflowを追加します。

このワークフローは:

  1. PR の base ブランチから golden baseline PNG をダウンロード

  2. 現在のブランチで golden を生成

  3. 差分画像を生成(flutter test のビルトイン diff 機能を利用)

  4. 差分があれば PR を失敗させ、差分画像をアップロード

name: golden-diff-check

permissions:
  contents: read

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  golden-diff:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout PR branch
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: stable

      - name: Install dependencies
        run: flutter pub get

      # ダウンロード:baseブランチの golden PNG(事前に生成されている前提)
      - name: Download golden baseline PNG
        uses: dawidd6/action-download-artifact@v3.1.4
        with:
          name: golden-baseline
          workflow: golden-baseline.yml
          branch: ${{ github.base_ref }}
          path: test/goldens/base

      # カレントブランチで golden test 実行(差分があれば *_diff.png が出る)
      - name: Run golden tests
        run: flutter test

      # 差分画像をアップロード(失敗時のみ)
      - name: Upload golden diffs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: golden-diff
          path: "**/*_diff.png"

まとめ

画像回帰テストは、UI品質を保ちつつレビューコストを下げる有力な武器です。
今回のPRでは、その基盤構築とCI連携を行いました。

「UIの変化に強い開発体制」を目指して、今後もカバレッジ拡大や仕組みの強化を進めていきます!

Discussion