Flutterでアプリを作成する際に自動テストをしっかりと書くようにしています。
Flutterによる自動テストを3記事に分けて公開するうちの2記事目です。
本記事ではWidgetテストについての備忘を書いていきます。
1記事目「【Flutter】Riverpod使用時の単純なユニットテストの書き方」はこちらからご確認ください。
3記事目「【Flutter】Integrationテストのシンプルな書き方」はこちらからご確認ください。
FlutterにおけるWidgetテストとは
FlutterにおけるWidgetテストとは、割とそのままですがWidgetの表示や動きが正しいかどうかをテストしていきます。
ボタンであれば、ボタンのテキストの確認であったり、ボタンをタップしたときの動作を確認するようなテストを書いていきます。
Widgetテストをしっかりと書くことによって、見た目の部分についてテストをすることができます。
ユニットテストではロジックの動きをテストしましたが、Widgetテストでは動き+見た目のテストをすることができます。
アプリの基本的な動きをテストすることでスモークテストのような感じにするのも良い感じです。
WidgetテストするViewクラス
前回のユニットテスト編でテストしてたロジッククラスを組み込んだ、ボタンをテストしていきます。
ソースコードは以下のとおりです。
全体像
class MyHomePage extends HookWidget { Widget build(BuildContext context) { final text = useProvider(calcProvider.select((value) => value.text)); return Scaffold( backgroundColor: colorMain, body: Container( margin: const EdgeInsets.only(bottom: 20,), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Row( children: <Widget>[ Expanded( child: Text( text, style: const TextStyle( color: colorText, fontSize: 60, ), textAlign: TextAlign.right, maxLines: 1, ), ) ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Expanded(child: Button(text: "C", colorButton: colorFunc, colorText: colorMain)), Expanded(child: Button(text: "+/-", colorButton: colorFunc, colorText: colorMain)), Expanded(child: Button(text: "%", colorButton: colorFunc, colorText: colorMain)), Expanded(child: Button(text: "/", colorButton: colorCalc, colorText: colorText)), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Expanded(child: Button(text: "7", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "8", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "9", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "x", colorButton: colorCalc, colorText: colorText)), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Expanded(child: Button(text: "4", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "5", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "6", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "-", colorButton: colorCalc, colorText: colorText)), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Expanded(child: Button(text: "1", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "2", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "3", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "+", colorButton: colorCalc, colorText: colorText)), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Button(text: "0", colorButton: colorNum, colorText: colorText), Expanded(child: Button(text: ".", colorButton: colorNum, colorText: colorText)), Expanded(child: Button(text: "=", colorButton: colorCalc, colorText: colorText)), ], ), ], ), ), ); } // end of state class }
ボタンWidget
class Button extends HookWidget { final String text; final Color colorButton; final Color colorText; const Button({ Key? key, required this.text, required this.colorButton, required this.colorText, }) : super(key: key); @override Widget build(BuildContext context) { final controller = useProvider(calcProvider.notifier); return Container( padding: const EdgeInsets.symmetric(vertical: 5), child: ElevatedButton( child: Padding( padding: text == "0" ? const EdgeInsets.only(left: 20, top: 20, right: 120, bottom: 20) : text.length == 1 ? const EdgeInsets.all(22) : const EdgeInsets.symmetric(horizontal: 15, vertical: 22), child: mapIcon.containsKey(text) ? Icon( mapIcon[text], size: 30, ) : Text( text, style: const TextStyle(fontSize: 30), ), ), onPressed: () { controller.input(text); }, style: ElevatedButton.styleFrom( primary: colorButton, onPrimary: colorText, shape: text == "0" ? const StadiumBorder() : const CircleBorder(), ), ), ); } }
FlutterによるWidgetテストの書き方
早速Widgetテストを書いていきます。
void main() { group("Button Single Test", () { testWidgets("Button Single Test", (WidgetTester tester) async { await tester.pumpWidget( ProviderScope( child: MaterialApp( home: Button( text: "0", colorButton: Colors.white, colorText: Colors.black, ), ), ), ); await tester.pump(); expect(find.text("0"), findsOneWidget); expect(find.text("2"), findsNothing); }); }); group("Tap Button Test", () { testWidgets("Tap Button Test", (WidgetTester tester) async { await tester.pumpWidget( ProviderScope( child: MyApp(), ), ); await tester.pumpAndSettle(); expect(find.text("0"), findsNWidgets(2)); await tester.tap(find.text("1")); await tester.pump(); expect(find.text("0"), findsOneWidget); expect(find.text("1"), findsNWidgets(2)); await tester.tap(find.text("2")); await tester.pump(); expect(find.text("12"), findsOneWidget); }); }); }
Widgetテストでは「testWidgets」を使用してテストしていきます。
WidgetTesterのpumpWidgetで、テストする仮想のWidgetを準備します。
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: Button(
text: "0",
colorButton: Colors.white,
colorText: Colors.black,
),
),
),
);
準備ができたら早速検証していきます。
以下の1つ目のコードでは、Finderクラスを使用することでボタンのテキストに”0″が設定しているものを探し、1個見つかること検証しています。仮想のWidgetを準備したときに”0″を設定したWidgetを準備しているので、1個見つかりテストを通過することができます。
2つ目のコードでは、”2″が設定されているWidgetを探し、1つも見つからないことを検証します。
3つ目のコードのように書くと、複数存在することを検証できます。その場合は存在する件数を指定する必要があります。
expect(find.text("0"), findsOneWidget);
expect(find.text("2"), findsNothing);
expect(find.text("1"), findsNWidgets(2));
また、以下のように書くとWidgetをタップすることができます。
await tester.tap(find.text("2"));
このソースではタップすると一部のテキストが変わるので、変わった結果を検証することもできます。
このように、Widgetテストをすることで見た目や動きをテストしていきます。
まとめ
FlutterによるWidgetテストコードの書き方の備忘録でした。
書くのは少し大変そうですが、一度書いてしまえば以降は自動でテストできるようになるのでかなり効率化できそうです。
実際に画面をポチポチするのも大切ですが、自動テストできるようにすればデグレなども気付きやすくなり、品質の向上にも役立ちそうです。
是非、品質の向上のためにもWidgetテストを書いてみてください。
Flutter学習情報
Flutterを勉強するのに最適な参考書は、以下の「基礎から学ぶFlutter」です。
環境構築から、Dart/Flutterの基本/テストやパフォーマンスチューニングまで一通り学ぶことができます。
また、当ブログではFlutterを初心者が学ぶためにオススメな方法を「Flutter を初心者が学ぶおすすめの勉強法!【間違いない動画があります】」という記事で公開していますので、Flutterに興味がある方は是非読んでみてください。
動画学習がしたい!という方にオススメの記事がこちらです。
Flutterの将来性はどうなのか?をまとめた記事はこちらです。