フューチャー技術ブログ

Flutter ウィジェットテスト入門

はじめに

こんにちは、TIGの村田です。
Dart/Flutter連載の最終日ということで、今回はFlutterでのウィジェットテストについてご紹介します。

事前準備

環境構築はこちらに沿って実施します。私はmacOSを利用していますが、利用しているOSごとに丁寧に手順が準備されているので、それに従って環境構築を進めればOKです。

本記事の前提となるFlutterのバージョンは 2.0.6 です。

$ flutter --version
Flutter 2.0.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 1d9032c7e1 (3 weeks ago) • 2021-04-29 17:37:58 -0700
Engine • revision 05e680e202
Tools • Dart 2.12.3

ちなみに今回 channel の設定が stable となっているんですが、ちょうど1年くらい前(2020.06)に書いたこの記事では、Flutter Web を使うためには channel は beta である必要がありました。進歩ですね。喜ばしい限りです。

さて、Flutterはコマンドでデフォルトのアプリを作成できてしまうのでさくっと作ります。

$ flutter create app

作成された tester ディレクトリの中に入り、早速アプリを立ち上げてみます。起動デバイスはどれでも良いですが、本記事では Flutter web を利用することとします。

$ cd app
$ flutter run -d chrome

するとおなじみのデフォルトアプリが立ち上がります。

デフォルトのカウンタアプリ

画面右下の FloatingActionButton を押下すると画面中央のカウンタがインクリメントされていく、というシンプルな作りになっています。

ここまで出来たら事前準備は完了です。

ウィジェットテストを実施する

ここからが本題です。皆さんは Flutter の create コマンドを利用してアプリを作成するとテストのコードも一緒に生成されることをご存知でしょうか?

app
L android
L ios
L lib
L test
L widget_test.dart

詳細なディレクトリ構成は割愛しますが、上記のような形で生成されたものの中に test というディレクトリがあり、その配下に widget_test.dart というファイルが存在します。

widget_test.dart
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

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

import 'package:tester/main.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());

// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

Flutter のテストは以下のコマンドで実行でき、成功すると All tests passed! と表示されます。

$ flutter test test/widget_test.dart
00:02 +1: All tests passed!

これだけだと何が起きているのか分からないので、テストファイルの中身を順繰りに見ていくことにします。

pubspec.yaml の定義

まず、それとなく実行したテストですが、そもそもテスト実行のためには flutter_test パッケージが必要となります。

dev_dependencies:
flutter_test:
sdk: flutter

上記は pubspec.yaml の記載ですが、Flutter の create コマンドを使ってアプリを作成すると自動的にこの依存関係が記載されます。もし自分で pubspec.yaml を書き上げる場合には flutter_test パッケージへの依存を追記してあげる必要があります。

testWidgets

次にテストファイルの中身を見ていきます。先頭で使用されているのが testWidgets() という関数です。

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// ...
}
// ...
}

これは flutter_test パッケージで定義されている関数で、ウィジェットテストを実施したい時に使うものです。 WidgetTester というヘルパークラスが用意されており、実際のテストコードはこの WidgetTester を活用しつつ記述します。

pumpWidget

次は pumpWidget() についてです。 testWidgets() の先頭に登場する関数です。

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());

pumpWidget() は対象の Widget のインスタンスを生成し、その生成処理が問題なく完了することをチェックします。今回は MyApp が指定されているので、 main.dart にて StatelessWidget として定義されている MyApp がチェック対象です。

pump

pump() は Widget の再生成を促すメソッドです。通常UI操作により描画対象に変更が加わった際には自動的に Widget が再生成されます。

例えば以下の画面。右下のボタンを2回押下したのですが、画面中央の数字がボタン押下に合わせて増えています。ユーザの操作に合わせて Widget の再生成が行われています。

pump()の動作確認のためのアプリ画像

この自動的な Widget の再生成がテスト実行環境では行われません。そのため、テストコードの中で明示的に Widget 再生成の指示を出す必要があり、それがこの pump() というわけです。

find

さて、UIコンポーネントを含むテストで一番大変なのは、テスト対象のオブジェクトをテストプログラム上で特定することではないでしょうか?これが簡単にできるように整備されていればいるほどテスタビリティが高いので、個人的にはとてもうれしいポイントです。

find()flutter_test パッケージが提供するトップレベルの関数で、文字やアイコンなどを元に該当する Widget を特定する Finder として機能します。

今回参照しているテストの中でも数箇所で登場します。

// // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// // Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));

find.text('0') というのが Finder で、0 の文字列を含む Widget を Widget ツリーの中から探索します。今回は画面中央に表示されるカウンターがヒットします。

また、 find.byIcon(Icons.add) ではアイコンを起点に Widget を探索します。今回は画面右下に表示されるプラスマークの書かれた青いボタンがヒットします。

expect

expects() は Matcher と一緒に用いることで、Widget が期待通りに生成されているか否かを検証します。

今回だとまずは以下の部分。初期描画時は画面中央のカウンターは 0 と表記されているはずです。

// // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

次に以下の部分。ボタンを1回押下するので、カウンターの数値は 0 から 1 に変わっているはずです。ちなみに、先程記載したようにボタン押下後には Widget 再生成が行われないので pump() をコールしています。

// // Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);

これでテストファイルの中身はすべて触れることができました。

さいごに

Flutter で実施するウィジェットテストについてご紹介しました。 expect() を使って期待値との比較を行うのはどのテストフレームワークでも似たようなものと思いますので、 flutter_test パッケージや WidgetTester クラスを活用していつものテストを Flutter でもサクッとこなせるようになりたいなと思いました。

Dart/Flutter連載は完走です!皆様お付き合い頂きありがとうございました。