環境
訳あって最新版を使っていません(macをCatalinaにアップデートできない==Xcodeの最新版を入れられないので)。
従って、バージョン違いによる不動作などあるかも知れません。お気づきの点があったら是非コメント下さい。
$ flutter --version
Flutter 1.12.13+hotfix.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 27321ebbad (4 months ago) • 2019-12-10 18:15:01 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0
WidgetTest
Flutterでは、WidgetTestというもので、UI要素をUnitTestできます。
画面が実際にエミュレーターなどで起動するわけではないので、実行速度はとても早いです。
Androidでいう、RobolectricでEspressoのUIテストをするみたいな感じですね。あれよりはずっと安定性が高いようです。(まだちょっと触っただけですけど)
目的
チュートリアルにある書き方だと、あまり実用的じゃ無い(そのままコピペしてもエラーになる)ので、直ぐに使えるコード(説明)を目指します。
基本的な書き方(ウィジェットを直接起動する)
Appレベルではなく、下位の子ウィジェットを単独でテストできますが、実は、チュートリアルのまんまでは、基本的に以下のようなエラーが出て、テストできません。
No Directionality widget found.
順番に見ていきます。
MyWidget
というウィジェットクラスを作ってあるとします。
※StatelssWidget
で動作確認しているので、StatefulWidget
だと若干挙動が異なる可能性があります。
MyWidget
には、以下の子ウィジェットがあります。
- Text:整数
20
を表示する - Text:小数
11.5
を表示する - Icon:
Icons.thumb_up
を表示する
上記の表示をチェックするテストは、チュートリアル通りに書くと、こうなります。
void main() {
testWidgets('Widgetテスト', (WidgetTester tester) async {
// ウィジェットを起動させる
await tester.pumpWidget(MyWidget());
// 子ウィジェットのチェック
expect(find.text('20'), findsOneWidget);
expect(find.text('11.5'), findsOneWidget);
expect(find.byIcon(Icons.thumb_up), findsOneWidget);
});
}
ところが、前述の通り、これをテストしようとすると、"No Directionality widget found."
と言われてしまいます。
これはつまり、「ウィジェットの並べる方向が分からないので初期化できない」と言われています。
なんじゃそりゃ、という感じなのですが、そうなのです。ウィジェットの並べる方向というのは、Android等をやってきている方はピンとくるかも知れませんが、言語によって左から並べるか右から並べるか変わるっていう、アレです。
詳しいことはこちらをみると分かりやすいかと思います。
[Flutter]MyAppのbuildのなかでいきなりTextだけ返してみた
解決策は、以下のいずれかで囲むことです。
- MaterialApp
- CupertinoApp
- WidgetApp
ということで、こうなります。
void main() {
testWidgets('Widgetテスト', (WidgetTester tester) async {
// ウィジェットを起動させる
await tester.pumpWidget(MateriapApp(
home: MyWidget(),
));
....
}
Providerを使っているときのWidgetTest
Providerパッケージを使っていて、ウィジェット内からアクセスしているようなパターンがあると思います。
たとえば、こんな感じ。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider<MainViewModel>(
create: (context) => MainViewModel(),
child: MyWidget(),
),
);
}
class HomePage extends StatelessWidget {
....
}
class MainViewModel extends ChangeNotification {
....
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
var viewModel = Provider.of<MainViewModel>(context, listen: false);
....
}
こういう場合、テストコードが前述のままだと、以下のようなエラーが出ます。例えMateriapApp
などで囲っていてもです。
The following ProviderNotFoundException was thrown running a test:
Error: Could not find the correct Provider above this MyWidget Widget
解決策は、MyApp
でやっているようにProviderを初期化することです。
void main() {
testWidgets('Widgetテスト', (WidgetTester tester) async {
// ウィジェットを起動させる
await tester.pumpWidget(MateriaApp(
home: ChangeNotifierProvider(
create: (context) => MainViewModel(),
child: MyWidget(),
),
));
....
}
正直、ウィジェットのテストが同じコードの嵐になるので、なんとか上手い方法考えたいですね。
とりあえず、test_util.dart
とか作ってこうしておきました。
/// テスト用ウィジェットの起動
MaterialApp testMainViewWidget(Widget widget) {
return MaterialApp(
home: ChangeNotifierProvider<MainViewModel>(
create: (context) => MainViewModel.withDisplayDate("2020-03-01"),
child: widget,
),
);
}
クラスにしてもいいかも知れないけど面倒なのでただの関数。
使うときはこうなります。
await tester.pumpWidget(testMainViewWidget(MyWidget()));
ViewModel
クラスが増える度に関数も増えちゃうけど、それはまた後で考えます(汗)
色やテキストスタイルなどの確認
色やサイズを条件で変えることはよくあると思いますが、そのチェックの仕方です。
1. TextStyle
まずはText
のスタイルのチェック方法です。
フォントサイズ、色などの確認はこれで出来ます。
WidgetPredicate predicate = (Widget widget) =>
widget is Text &&
widget.data == "26" &&
widget.style ==
TextStyle(
fontSize: 12.0,
color: Colors.grey[500],
);
expect(find.byWidgetPredicate(predicate), findsOneWidget);
注意点としては、「fontSizeもcolorも指定しているけど、fontSizeだけ確認したい」みたいなことは出来ない点です。コードを見れば分かると思いますが、すべての要素が一致しないと引っかからないので、要注意です。
出来れば、使うStyleを定義しておいてそれと比較する方が良いでしょうね。そうしないと、スタイルを変えたときにテストもいちいち変えなければなりません。
final TextStyle myTextStyle = TextStyle(
fontSize: 12.0,
color: Colors.grey[500],
);
こんな風にオブジェクトを用意しておけば、スタイル設定をするところでも、テストでも、使えます。
/// Textウィジェットへの設定
Text("aaa",
style: myTextStyle,
);
/// WidgetPredicateで使う
WidgetPredicate predicate = (Widget widget) =>
widget is Text &&
widget.style == myTextStyle
2. ContainerのDecoration
Container
に枠や色を付けて、他のウィジェットの背景としていることも多いと思います。
やりかたはText
の時と全く同じで、WidgetPredicate
を使います。
WidgetPredicate predicate = (Widget widget) =>
widget is Container &&
widget.decoration ==
BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.3),
color: Colors.black12,
);
expect(find.byWidgetPredicate(predicate), findsOneWidget);
WidgetPredicate
を使えば、他のウィジェットにも同様の考え方で書けるでしょう。
まとめ
とりあえずウィジェットの単体テストについて書きました。
遷移のテストは分かり次第別途まとめます。(予定)
テストレポートがhtmlで見たい・・・
Gradleって優秀だなあ・・・
参考
[Flutter] Providerを使ってAndroidのViewModel-LiveDataっぽいのを実装する
https://friegen.xyz/fultter-provider-android-viewmodel-livedata/?fbclid=IwAR0dyobfqxAu5WJ9pMouITNk8XpNDFMSkLSLTvzlc63VsIGyaUkjS1goRKo
【Flutter】Widget テストの「あれ、これどうやるんだろう?」集
https://qiita.com/chooyan_eng/items/6ffd5b07de07edafd304
Flutterのプロバイダーの単体テスト
https://www.dev4app.com/archives/59735075-unit-testing-for-providers-in-flutter.html
Flutter widget tests: a practical example
https://cogitas.net/flutter-widget-tests-practical-example/