はじめに
この記事はGoogleのCodelabにて公開されている以下のチュートリアルの個人的なまとめです。
Flutterアプリをテストする方法
テスト対象のアプリ
一覧ページとお気に入りページがあり、アイコンタップでお気に入り登録と解除ができるよくあるパターンのアプリです。
お気に入りの管理にProviderを使用しています。
アプリのディレクトリ構成
testing_app/
┣ lib/
┃ ┣ models/
┃ ┃ ┗ favorites.dart
┃ ┗ screens/
┃ ┗ favorites.dart
┃ ┗ home.dart
┗ test/
中身のコードは以下を参照してください
codelabのコード
テストファイルのディレクトリ構成
プロジェクト作成時に自動生成されているtestフォルダに追加していきます。
テストファイルを追加する際は、アプリ側の構成と合わせて配置するとわかりやすいです。
今回の場合は以下の通りです。
testing_app/
┣ ...
┗ test/
┗ models/
┃ ┗ favorites_test.dar
┣ favorites_test.dart
┗ home_test.dart
テストファイルにはlib側のファイル名 + _testで命名します。
※テストランナーがテストファイルを検索する際に使用するようなので必ずつけましょう
ユニットテスト
単純な関数やメソッドに対するテストを行います。
計算処理や追加処理などが該当します。
今回はmodels/favorites.dart
がテスト対象
準備
pubspec.ymlに以下を記載
dev_dependencies:
test: ^1.14.4
flutter_test: ^1.0.1
実装
import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';
void main() {
group('App Provider Tests', () {
var favorites = Favorites();
test('A new item should be added', () {
var number = 35;
favorites.add(number);
expect(favorites.items.contains(number), true);
});
test('An item should be removed', () {
var number = 45;
favorites.add(number);
expect(favorites.items.contains(number), true);
favorites.remove(number);
expect(favorites.items.contains(number), false);
});
});
}
テスト内容説明
・お気に入り追加テスト
・お気に入り削除テスト
使用したメソッドなどの説明
・group(description, body)
複数の関連するテストをまとめることができます。
・test(description, body)
テスト本体です。
・expect(actual, matcher)
結果を比較します。
(CLI)テストを実行する
$ flutter test test/models/favorites_test.dart
すべてOKだった場合の結果表示はこうなります。
00:06 +2: All tests passed!
(GUI)テストを実行する
アイコンを押下するだけです。
すべてOKだった場合の結果はこうなります。
ウィジェットテスト
ボタンタップでsnackbarが表示されるかなどのウィジェットの挙動をテストします。
テスト対象
・screens/favorites.dart
・screens/home.dart
実装
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';
Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
create: (context) => Favorites(),
child: MaterialApp(
home: HomePage(),
),
);
void main() {
group('Home Page Widget Tests', () {
testWidgets('Testing if ListView shows up', (tester) async {
await tester.pumpWidget(createHomeScreen());
expect(find.byType(ListView), findsOneWidget);
});
testWidgets('Testing Scrolling', (tester) async {
await tester.pumpWidget(createHomeScreen());
expect(find.text('Item 0'), findsOneWidget);
await tester.fling(find.byType(ListView), Offset(0, -200), 3000);
await tester.pumpAndSettle();
expect(find.text('Item 0'), findsNothing);
});
testWidgets('Testing IconButtons', (tester) async {
await tester.pumpWidget(createHomeScreen());
expect(find.byIcon(Icons.favorite), findsNothing);
await tester.tap(find.byIcon(Icons.favorite_border).first);
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.text('Added to favorites.'), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsWidgets);
await tester.tap(find.byIcon(Icons.favorite).first);
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.text('Removed from favorites.'), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsNothing);
});
});
}
テスト内容説明
上から順に
・お気に入り一覧(ListView)が表示されているか確認
・スクロールで'Item 0'が画面外に出たか確認
・favorite_borderアイコンをタップでお気に入り追加され、snackBarに追加メッセージが表示されることと、favoriteアイコンをタップでお気に入りから削除され、snackBarに削除メッセージが表示されることを確認
使用したメソッドなどの説明
・createHomeScreen()
Providerによる状態管理を再現するために使用します。
・testWidget(description, callback)
ウィジェットテストの場合はこちらを使用します。
・tester
アプリを操作するための変数です(WidgetTest型
)。
ボタンをタップ、スクロールなどはすべてtesterで行います。
・pumpWidget(widget)
テスト対象のウィジェットを準備する。
・fling(finder, offset, speed)
finderで指定したウィジェットをスクロールする。
・tap(finder)
finderで指定したウィジェットをタップする
・pumpAndSettle(duration)
ウィジェットの描画完了待機
・find
flutter_testパッケージが提供するクラス(CommonFinders
)
ウィジェットを探すのに使用します。
こちらも色々なメソッドが用意されています。
・byType(type)
引数で渡された型に一致するウィジェットを探索し、Finder
型で返す
・byIcon(icon)
アイコンで探す。
・byText(text)
引数のテキストと一致するテキストを保持しているウィジェットを探す。
・expect()の第二引数について
第一引数のfind結果と比較する定数です
findsNotiong
ウィジェットがfindツリー上に存在しない
findsOneWidget
ウィジェットがfindツリー上に一つだけ存する
テストを実行する
※実行方法はユニットテストと同様なので割愛
インテグレーションテスト
準備
pubspec.ymlに以下を追加します
dev_dependencies:
integration_test: ^1.0.1
flutter_driver:
sdk: flutter
インテグレーションテストではtesting_app/test
ディレクトリは使用しません。
ディレクトリとテストファイルを以下のように追加します。
testing_app/
┣ ...
┗ integration_test/ ←new
┣ driver.dart ←new
┗ app_test.dart ←new
実装
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
・integrationDriver()
flutter_driveでテストを実行するためのアダプターで、計測したパフォーマンスデータの出力なども行ってくれます。
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';
void main() {
group('Testing App Performance Tests', () {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
testWidgets('Scrolling test', (tester) async {
await tester.pumpWidget(TestingApp());
final lastFinder = find.byType(ListView);
await binding.watchPerformance(() async {
await tester.fling(lastFinder, Offset(0, -500), 10000);
await tester.pumpAndSettle();
await tester.fling(lastFinder, Offset(0, 500), 10000);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_summary');
});
testWidgets('Favorites operaitons test', (tester) async {
await tester.pumpWidget(TestingApp());
final iconKeys = [
'icon_0',
'icon_1',
'icon_2',
];
for (var icon in iconKeys) {
await tester.tap(find.byKey(ValueKey(icon)));
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.text('Added to favorites.'), findsOneWidget);
}
await tester.tap(find.text('Favorites'));
await tester.pumpAndSettle();
final removeIconKey = [
'remove_icon_0',
'remove_icon_1',
'remove_icon_2',
];
for (final iconKey in removeIconKey) {
await tester.tap(find.byKey(ValueKey(iconKey)));
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.text('Removed from favorites.'), findsOneWidget);
}
});
});
}
使用したメソッドなどの説明
・ensureInitialized()
テストドライバーの初期化確認と最初期化などを行ってくれる
・binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
フレームのポンピング設定(fullyLiveの場合は意図的にポンピングしなくとも自動的にポンピングするようです)
・watchPerformance()
パフォーマンスを計測して指定した名称でjson出力してくれます。
出力先:test_app/build/integration_response_data.json
テストを実行する
$ flutter drive --driver integration_test/driver.dart --target integration_test/app_test.dart --profile
実行結果
すべてOKだとこんな感じで出力されます。
Running "flutter pub get" in testing_app... 4.9s
Signing iOS app for device deployment using developer identity: "Apple Development: arlezplue.7@gmail.com (5KFW69RP5Y)"
Running pod install... 1,341ms
Running Xcode build...
└─Compiling, linking and signing... 5.9s
Xcode build done. 11.9s
Installing and launching... 14.8s
VMServiceFlutterDriver: Connecting to Flutter application at http://127.0.0.1:54091/jJdzr8Y-sYM=/
VMServiceFlutterDriver: Isolate found with number: 4018486944099739
VMServiceFlutterDriver: Isolate is paused at start.
VMServiceFlutterDriver: Attempting to resume isolate
flutter: 00:00 +0: Testing App Performance Tests Scrolling test
VMServiceFlutterDriver: Connected to Flutter application.
flutter: 00:11 +1: Testing App Performance Tests Favorites operaitons test
flutter: 00:20 +2: Testing App Performance Tests (tearDownAll)
flutter: Warning: integration_test test plugin was not detected.
flutter: 00:20 +3: All tests passed!
All tests passed.