この記事は MIXI DEVELOPERS Advent Calendar 2022 21日目の記事です。
この記事ではFlutterのテストについて、テスト手法の1つであるGoldenTestをどのように実装するかご紹介したいと思います。
GoldenTestとは
そもそもGoldenTestとは何なのか簡単に説明します。
GoldenTestとは「過去に実行したテストの結果を保存し、再度テストを実行する際にそのファイルと同じ結果になるかどうかをチェックするテスト」です。
RegressionTestの手法の1つで、Flutterとりわけクライアントサイドの開発においては、UIが予期せず変更されていないかを確かめるテストとなります。
※RegressionTestは回帰テストとも言い、プログラムの一部を変更したことで他の箇所に不具合が出ていないかを確認するためのテストです。
今回はflutter create
で作成できるアプリを例にしてどのようにテストするかご紹介したいと思います。
下準備
テスト対象にするアプリを作成
今回使用するFlutterのバージョンは以下にします。
$ flutter --version
Flutter 3.3.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 6928314d50 (5 weeks ago) • 2022-10-25 16:34:41 -0400
Engine • revision 3ad69d7be3
Tools • Dart 2.18.2 • DevTools 2.15.0
最初に雛形となるアプリを作成します。
$ flutter create flutter_golden_test
flutter run
を実行するとこのようなアプリが起動できます。
GoldenTestを組み込む
GoldenTestのパッケージ「golden-toolkit」を追加します。
※余談ですがgolden_toolkitはebayで開発しているOSSのようです。
$ flutter pub add golden_toolkit
pubspec.yml
に以下のように依存関係が追加されます。
dependencies:
flutter:
sdk: flutter
〜中略〜
golden_toolkit: ^0.13.0
次にプロジェクト直下にdart_test.yaml
を追加して以下のように記述してGoldenTestの設定を追加します。
tags:
golden:
テストを書いてみる
先ほど作成したアプリのテストを実際に書いていきます。
testディレクトリにmy_app_golden_test.dart
を追加して以下のように記述します。※ファイル名は何でも良いです
import 'package:flutter/material.dart';
import 'package:flutter_golden_test/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
// テスト対象のウィジェット
const targetWidget = MyApp();
// テスト対象の画面サイズ
const targetSize = Size(392, 813);
testGoldens(
'my_app',
(WidgetTester tester) async {
// 指定したウィジェットを指定したサイズでビルド
await tester.pumpWidgetBuilder(targetWidget, surfaceSize: targetSize);
// 保存しているスクリーンショットとUIが一致するか検証
await screenMatchesGolden(tester, 'my_app');
},
);
}
以下のコマンドを実行します。
$ flutter test --update-goldens
するとtest
ディレクトリ以下に./goldens/my_app.png
が生成されます。
こちらが冒頭に記載した「過去に実行したテストの結果」になります。
このままだとフォントやアイコンが読み込めていないので、読み込み処理を追加します。
test
ディレクトリ直下にflutter_test_config.dart
を追加し以下のように記述します。
import 'dart:async';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadAppFonts();
return testMain();
}
再びflutter test --update-goldens
を実行すると画像ファイルが更新されます。
先ほどマスクされていた文字やアイコンなどが表示されるようになります。
また、このままだとサイズを指定してテストを行う形なので、ページ全体のテストとしては不適感があります。
そこでテストを以下のように変更します。
import 'package:flutter_golden_test/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
const targetWidget = MyApp();
testGoldens(
'my_app',
(WidgetTester tester) async {
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(
devices: [
Device.phone,
Device.iphone11,
],
)
..addScenario(
widget: targetWidget,
name: 'my_app',
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'my_app');
},
);
}
golden_toolkitのDevice
というクラスを指定してテストを行う形に変更しました。
今回はDevice
クラスに定義されているphone
とiphone11
を使用していますが、システムの要件に合わせてよしなに変更すると良いと思います。
flutter test --update-goldens
を実行すると以下のように./test/goldens/my_app.png
が更新されます。
UIを変更してみる
テスト結果がわかりやすいようにデバイスではなくサイズを指定したテストで見ていきます。
その1(プライマリーカラーを変更)
この状態で先程の「過去に実行したテストの結果」を変えずにflutter test
を実行してみると以下のようにテストが失敗するようになります。
❯ flutter test
00:01 +0: loading /Users/.../flutter_golden_test/test/my_app_golden_test.dart
00:02 +1: /Users/.../flutter_golden_test/test/my_app_golden_test.dart: my_app
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
Golden "goldens/my_app.png": Pixel test failed, 5.53% diff detected.
Failure feedback can be found at
/Users/.../flutter_golden_test/test/failures
When the exception was thrown, this was the stack:
#0 LocalFileComparator.compare (package:flutter_test/src/_goldens_io.dart:101:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
════════════════════════════════════════════════════════════════════════════════════════════════════
00:02 +1 -1: /Users/.../flutter_golden_test/test/my_app_golden_test.dart: my_app [E]
Test failed. See exception logs above.
The test description was: my_app
To run this test again: /Users/.../flutter/bin/cache/dart-sdk/bin/dart test /Users/.../flutter_golden_test/test/my_app_golden_test.dart -p vm --plain-name 'my_app'
00:02 +1 -1: Some tests failed.
また、test
ディレクトリ以下にfailures
ディレクトリが生成され、配下に失敗したテストの結果が保存されます。
こちらを参照すると画像ファイルベースでテストの実行結果が確認できます。
my_app_isolatedDiff.png | my_app_maskedDiff.png | my_app_masterImage.png | my_app_testImage.png |
---|---|---|---|
flutter test --update-goldens
を実行して「過去に実行したテストの結果」を更新した後に、flutter test
を実行すると再度テストが通るようになります。
その2(テキストを変更)
数字の上のYou have pushed the button this many times:
という文言をchanged
という文言に変えてみます。
先程と同様に「過去に実行したテストの結果」を変えずにflutter test
を実行すると失敗します。テストを実行して失敗した結果がfailures
ディレクトリ以下に保存されます。
my_app_isolatedDiff.png | my_app_maskedDiff.png | my_app_masterImage.png | my_app_testImage.png |
---|---|---|---|
その3(アイコンを変更)
例によって「過去に実行したテストの結果」を変えないとflutter test
が失敗するようになり、テストの実行結果ファイルがfailuresディレクトリ以下に保存されます。
my_app_isolatedDiff.png | my_app_maskedDiff.png | my_app_masterImage.png | my_app_testImage.png |
---|---|---|---|
まとめ
色や文字、アイコンを変更してテストが通らなくなることを確認しました。
今回は意図的にUIを変更して確認してみましたが、本来は内部のロジックを変更したりリファクタリングを行った際などに効力を発揮するテスト手法になります。意図しない不具合が起きていないかチェックすることが出来るようになり、品質をより強固に保つことができるようになります。
品質担保するのにGoldenTestのみでは不十分だとは思いますが、CIに組み込んで上手く仕組み化出来れば、人では気づけないような微妙な違いなどを自動で検出できるとても有用なツールになるのではないかと思います。
Flutterはテストも手軽に実装することができますが、今回ご紹介したGoldenTestも今回ご紹介したように割と手軽に導入できるのではないかと思います。ぜひ取り入れてみてはいかがでしょうか。
今回使用したコードはgithub上にも公開しておりますので見てみてください。
https://github.com/yukihiro-numata/flutter_golden_test