4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FlutterのWidgetTestの初歩の初歩だけど直ぐに使えるコード

Last updated at Posted at 2020-03-29

環境

訳あって最新版を使っていません(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とか作ってこうしておきました。

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/

4
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?