Flutterのゴールデンテストパッケージ、alchemist の解説
はじめに
こんにちは!
株式会社アンドエーアイの荻野と申します!
今回は「Flutter で alchemist を使ってゴールデンテストを簡単に実装する」と題して記事を書いていこうと思います!
モチベーション
以前、パッケージを使わない標準の flutter_test だけを使った、ゴールデンテストの実装を記事にしました。
しかしながら、Flutter標準だけだと、複数のサイズや複数の状態を検証しようとしたとき、どうしても実装が複雑になったりしてしまう問題があります。
今回はそれらをより便利な機能をより簡単に実現することができる alchemist というパッケージを使い、どのようなことができるのかを検証していきたいと思います。
alchemistとは
golden_toolkitにインスパイアされた、Flutterのゴールデンテストをより簡単に実装するためのパッケージです。
人気パッケージだったgolden_toolkitがdiscontinued
となったことで、その代替として最近注目されているパッケージとなります。
実装
実装環境
flutter: 3.24.4
alchemist: 0.10.0
プロジェクト作成 & インストール
いつも通りプロジェクトを作成し、alchemist
をインストールします。
# 検証用アプリの作成
flutter create alchemist_app
# alchemistを追加
flutter pub add alchemist
テストを実装する
テストファイル作成
プロジェクトの/test
内にゴールデンテスト用のコードを作成します。
このときファイル名はご多分に漏れず、文末が_test.dart
となるように注意します。
ユニットテストなどと分けるために、test/golden_test/my_widget_golden_test.dart
などとしておくと良いでしょう。
今回はFlutterのカウンターアプリのホーム画面を対象にするので、test/golden_test/my_home_page_golden_test.dart
としておきます。
テストコード実装
テストファイルを追加したところで、まずは以下のようにコードを実装してみます。
void main() {
group('MyHomePage Golden Tests', () {
goldenTest(
'renders correctly',
fileName: 'my_home_page',
builder: () => GoldenTestScenario(
name: 'HomePage',
constraints: BoxConstraints.tight(Size(320, 568)),
child: MyHomePage(title: 'My Home Page'),
),
);
});
}
テストを実行する
ゴールデンイメージの生成
# ゴールデンイメージの生成
flutter test --update-goldens
実行するとテストファイルと同じディレクトリにgoldens/
フォルダが生成され、
以下のように画像が格納されました。
test
├── golden_test
│ ├── goldens
│ │ ├── ci
│ │ │ └── my_home_page.png
│ │ └── macos
│ │ └── my_home_page.png
│ └── my_home_page_golden_test.dart
標準のgolden_testと異なるのは、goldens/
の中に、ci/
とmacos/
というフォルダが生成され、それぞれに画像が格納されている点です。
生成されたゴールデンイメージ
ci | macos |
---|---|
![]() |
![]() |
ci
の方は以前の記事で確認した、標準のgolden_testで生成したイメージと同様の特徴が見て取れます。
一方でmacos
の方はいつも私たちが見ている見た目がそのまま再現されています。
ゴールデンイメージと比較
--update-goldens
を外して、テストを実行します。
# テストを全て実行
flutter test
# ゴールデンテストのみを実行
flutter test --tags golden
# ゴールデンテスト以外を実行
flutter test --exclude-tags golden
成功時
成功時ログ
flutter test
00:01 +0: loading /Users/user/Desktop/alchemist_app/test/golden_test/my_home_page_golden_test.dart
Warning: A tag was used that wasn't specified in dart_test.yaml.
golden was used in:
the test "MyHomePage Golden Tests renders correctly (variant: macOS)"
the test "MyHomePage Golden Tests renders correctly (variant: CI)"
00:02 +2: All tests passed!
失敗時
失敗時はfailures
フォルダがテストファイルと同じディレクトリに生成されます。
また、失敗時画像はフォルダが分かれておらず、プラットフォームテストの結果が格納されるようです。
失敗時ログ
flutter test
00:01 +0: loading /Users/user/Desktop/alchemist_app/test/golden_test/my_home_page_golden_test.dart
Warning: A tag was used that wasn't specified in dart_test.yaml.
golden was used in:
the test "MyHomePage Golden Tests renders correctly (variant: macOS)"
the test "MyHomePage Golden Tests renders correctly (variant: CI)"
00:01 +0: MyHomePage Golden Tests renders correctly (variant: macOS)
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
Golden "goldens/macos/my_home_page.png": Pixel test failed, 0.35%, 668px diff detected.
Failure feedback can be found at /Users/user/Desktop/alchemist_app/test/golden_test/failures
When the exception was thrown, this was the stack:
#0 LocalFileComparator.compare (package:flutter_test/src/_goldens_io.dart:108:5)
<asynchronous suspension>
#1 MatchesGoldenFile.matchAsync.<anonymous closure> (package:flutter_test/src/_matchers_io.dart:121:32)
<asynchronous suspension>
<asynchronous suspension>
#3 _expect.<anonymous closure> (package:matcher/src/expect/expect.dart:123:26)
<asynchronous suspension>
<asynchronous suspension>
#5 expectLater.<anonymous closure> (package:flutter_test/src/widget_tester.dart:518:24)
<asynchronous suspension>
#6 FlutterGoldenTestAdapter.withForceUpdateGoldenFiles (package:alchemist/src/golden_test_adapter.dart:201:14)
<asynchronous suspension>
#7 FlutterGoldenTestRunner.run (package:alchemist/src/golden_test_runner.dart:110:9)
<asynchronous suspension>
#8 goldenTest.<anonymous closure> (package:alchemist/src/golden_test.dart:170:7)
<asynchronous suspension>
#9 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#10 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1027:5)
<asynchronous suspension>
<asynchronous suspension>
(elided 3 frames from dart:async and package:stack_trace)
The exception was caught asynchronously.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:01 +0 -1: MyHomePage Golden Tests renders correctly (variant: macOS) [E]
Test failed. See exception logs above.
The test description was: renders correctly (variant: macOS)
To run this test again: /Users/user/fvm/versions/3.27.1/bin/cache/dart-sdk/bin/dart test /Users/user/Desktop/alchemist_app/test/golden_test/my_home_page_golden_test.dart -p vm --plain-name 'MyHomePage Golden Tests renders correctly (variant: macOS)'
00:01 +1 -1: Some tests failed.
test
└── golden_test
├── failures
│ ├── my_home_page_isolatedDiff.png
│ ├── my_home_page_maskedDiff.png
│ ├── my_home_page_masterImage.png
│ └── my_home_page_testImage.png
├── goldens
│ ├── ci
│ │ └── my_home_page.png
│ └── macos
│ └── my_home_page.png
└── my_home_page_golden_test.dart
このように、標準のみを利用して実装したテストコードと比較して、少ないコード量で、より高機能なゴールデンテストを実装できました。
その他機能
複数のシナリオをテストする
先ほどの実装コードだと、1画像につき1ウィジェットしかテストできないため、以下のようなケースにおいて一覧性がよくありません。
- 複数の画面サイズでの見た目で確認したい。
- ウィジェットのステート毎のパターンを確認したい。
このようなケースにおいて、GoldenTestGroup
が便利です。
これは内部的にはTable
を使用して実装されており、複数のGoldenTestScenario
をグリッド状に並べることが可能となります。
goldenTest(
'renders correctly',
fileName: 'my_home_page',
builder: () => GoldenTestGroup(
children: [
// 画面サイズ別タイトルあり状態
GoldenTestScenario(
name: 'with title, 820 x 1180 (tablet)',
constraints: BoxConstraints.tight(Size(820, 1180)),
child: MyHomePage(title: 'My Home Page'),
),
...
// 画面サイズ別タイトルなし状態
GoldenTestScenario(
name: 'no title, 820 x 1180 (tablet)',
constraints: BoxConstraints.tight(Size(820, 1180)),
child: MyHomePage(),
),
...
],
),
);
テスト内容をカスタマイズする
alchemist にはテスト内容をカスタマイズするAlchemistConfig
というクラスがあります。
使い方
test/flutter_test_config.dart
を作成し、以下のように実装することで、テスト全てに設定を適用することができます。
(testExecutable
を実装することで、テストの前に初期化処理を行うことが可能です。)
import 'dart:async';
import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
return AlchemistConfig.runWithConfig(
config: AlchemistConfig(
forceUpdateGoldenFiles: false,
theme: null,
goldenTestTheme: GoldenTestTheme(
backgroundColor: const Color(0xFF2b54a1),
borderColor: const Color(0xFF3d394a),
nameTextStyle: const TextStyle(fontSize: 18),
padding: EdgeInsets.all(8),
),
platformGoldensConfig: PlatformGoldensConfig(
platforms: HostPlatform.values,
obscureText: false,
renderShadows: true,
filePathResolver: (fileName, environmentName) {
return 'goldens/$environmentName/$fileName.png';
},
theme: ThemeData.dark(),
),
ciGoldensConfig: CiGoldensConfig(
enabled: true,
obscureText: true,
renderShadows: false,
filePathResolver: (fileName, environmentName) {
return 'goldens/$environmentName/$fileName.png';
},
theme: ThemeData.dark(),
),
),
run: testMain,
);
}
また、一部のテストに設定を適用したい場合はAlchemistConfig.runWithConfig()
を使いましょう。
current()
,copyWith()
,merge()
などのメソッドを組み合わせ、現在の設定を部分的にオーバーライドすることも可能です。
AlchemistConfig.runWithConfig(
config: AlchemistConfig.current().merge(myAlchemistConfig),
run: () {
group('MyHomePage Golden Tests', () {
goldenTest(
...
)
});
},
);
AlchemistConfig クラス
プロパティ | デフォルト値 | 説明 |
---|---|---|
bool forceUpdateGoldenFiles | false |
True のとき、--update-goldens タグがない時も常にゴールデンイメージを生成します。 |
ThemeData? theme | null | テストに使用するテーマを指定できます。null のときはデフォルトのThemeData.light() を使用します。 |
GoldenTestTheme? theme | null | 生成されるゴールデンイメージのスクリーンショット以外の部分のテーマを指定できます。画像内で表示される「シナリオ名のスタイル」、シナリオ間の「余白」と「ボーダー」、「背景色」を変更できます。 |
PlatformGoldensConfig platformGoldensConfig | const PlatformGoldensConfig() | CI 以外のプラットフォームでテストを行うときの設定を行います。 |
CiGoldensConfig ciGoldensConfig | const CiGoldensConfig() | CI でテストを行うときの設定を行います。 |
PlatformGoldensConfig & CiGoldensConfig クラス
プロパティ | デフォルト値 | 説明 |
---|---|---|
bool enabled | true | CI テスト、もしくはプラットフォームテストを実行するかどうかを設定できます。 |
bool obscureText | CI: true Platform: false |
true のとき、テキストを矩形でレンダリングします。生成されたゴールデンイメージの項目で CI テストのフォントが矩形で表示されていたのはこの設定によるものです。 |
bool renderShadows | CI: true Platform: false |
true のとき、シャドウを単純な塗りつぶしでレンダリングします。生成されたゴールデンイメージの項目で CI テストのシャドウが黒一色で表示されていたのはこの設定によるものです。 |
FilePathResolver filePathResolver | <_defaultFilePathResolver> | 生成したイメージを保存する相対パスを設定できます。デフォルトはゴールデンイメージの生成の項目の通りです。 |
ThemeData? theme | null | テストタイプ毎のテーマを設定できます。null のときは AlchemistConfig のデフォルトもしくは設定テーマが適用されます。 |
PlatformGoldensConfig クラスのみ
プロパティ | デフォルト値 | 説明 |
---|---|---|
Set<HostPlatform> platforms | すべて | 実行するプラットフォームを限定できます。 |
ジェスチャの実行
goldenTest()
の引数whilePerforming
を使用することで、
ボタンなど、ジェスチャ時のイメージを出力したい場合も対応可能です。
press
の他、longPress
、scroll
のジェスチャにも対応しています。
goldenTest(
whilePerforming: press(find.byType(FloatingActionButton)),
...
)
ゴールデンイメージのリサイズ
goldenTest()
の引数constraints
を使用することで、そのサイズ制約よって画像がリサイズされます。制約を本来のウィジェットサイズよりも小さくした場合、はみ出す部分はクロップされます。
400x400 | 4000x4000 |
---|---|
![]() |
![]() |
ポンピングメソッドの拡張
pumpBeforeTest
goldenTest()
は、デフォルトでtester.pumpAndSettle()
を使用してウィジェットツリーを描画します。
これは設定した時間の間(デフォルト:100ms)だけウィジェットをpump()
し続けるメソッドですが、これでは例えば非同期処理中の表示を確認したい場合や、100ms以上表示を遅延させる場合に対応できません。
このような場合、goldenTest()
のpumpBeforeTest
を設定することで、ゴールデンテスト実行前に、ウィジェットツリーを描画する動作をオーバーライドできます。
goldenTest(
pumpBeforeTest: (tester) async {
await tester.pumpAndSettle(Duration(milliseconds: 2000));
},
builder: () => FutureBuilder(
future: Future.delayed(Duration(milliseconds: 1000))
.then((_) => 'completed'),
initialData: 'waiting...',
builder: (context, snapshot) {
return Text(snapshot.data ?? '');
},
),
)
また、ゴールデンイメージ内で画像データを読み込みたい場合、本パッケージが提供するprecacheImages()
をpumpBeforeTest
に渡すことで、ツリー内のすべての画像をプリロードし、生成されるゴールデンファイルに表示されるようにすることができます。
pumpWidget
goldenTest()
のpumpWidget
引数を指定することでウィジェットを描画する際の動作をオーバーライドし、テスト対象のウィジェットを任意の数のウィジェットでラップすることができます。
例えば下記のように、描画したいウィジェットが特定の親要素(InheritedWidget等)に依存する場合に便利です。
goldenTest(
pumpWidget: (tester, widget) async => {
await tester.pumpWidget(
MyInheritedWidget(
child: widget,
),
),
},
...
)
ScaleFactor の設定
goldenTest()
の引数scaleFactor
を設定することで、テストの scale factor を設定できます。
また、GoldenTestScenario
のwithTextScaleFactor
コンストラクタを使用することで、そのシナリオ毎に scale factor を指定できます。
goldenTest(
'renders correctly',
fileName: 'my_home_page',
// テストのデフォルト scale factor を指定する。
textScaleFactor: 1.0,
builder: () => GoldenTestGroup(
children: [
// withTextScaleFactor コンストラクタを使用する。
GoldenTestScenario.withTextScaleFactor(
name: 'with title, 820 x 1180 (tablet), x2.0',
textScaler: TextScaler.linear(2.0),
constraints: BoxConstraints.tight(Size(820, 1180)),
child: MyHomePage(title: 'My Home Page'),
),
...
],
)
)
最後に
Alchemist を使用すると、標準のみを利用して実装したテストコードと比較して、少ないコード量で、より高機能なゴールデンテストを実装できることがわかりました。
また、メンテナンスやアップデートも問題なく行われていて、Flutterにゴールデンテストを実装する予定の方にとっては強い味方になると感じました。
もし、プロジェクトに VRT の導入を検討されている場合は是非ともご検討ください。
ここまで当記事を読んでいただきありがとうございました!
PR
アンドエーアイでは事業拡大のため、即戦力エンジニアを募集中!Flutterだけでなく、インフラ、Web、ネイティブ開発などの知識を持つ方も歓迎します。最新技術を追い、チームに積極的に貢献できる方をお待ちしています!
採用ページ
エンジニア採用ページ
参考資料
Discussion