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

# 環境

訳あって最新版を使っていません(macをCatalinaにアップデートできない==Xcodeの最新版を入れられないので)。
従って、バージョン違いによる不動作などあるかも知れません。お気づきの点があったら是非コメント下さい。

```console
$ 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テストをするみたいな感じですね。あれよりはずっと安定性が高いようです。（まだちょっと触っただけですけど)

# 目的
[チュートリアル](https://flutter.dev/docs/cookbook/testing/widget/introduction)にある書き方だと、あまり実用的じゃ無い(そのままコピペしてもエラーになる)ので、直ぐに使えるコード(説明)を目指します。

# 基本的な書き方(ウィジェットを直接起動する)

Appレベルではなく、下位の子ウィジェットを単独でテストできますが、実は、[チュートリアル](https://flutter.dev/docs/cookbook/testing/widget/introduction)のまんまでは、基本的に以下のようなエラーが出て、テストできません。

```console
No Directionality widget found.
```

順番に見ていきます。

`MyWidget`というウィジェットクラスを作ってあるとします。
※`StatelssWidget`で動作確認しているので、`StatefulWidget`だと若干挙動が異なる可能性があります。

`MyWidget`には、以下の子ウィジェットがあります。

- Text:整数`20`を表示する
- Text:小数`11.5`を表示する
- Icon:`Icons.thumb_up`を表示する

上記の表示をチェックするテストは、チュートリアル通りに書くと、こうなります。

```Dart
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だけ返してみた](https://qiita.com/nichiyoshi/items/f63fff2a051f87bd58ad)

解決策は、以下のいずれかで囲むことです。

- MaterialApp
- CupertinoApp
- WidgetApp

ということで、こうなります。

```Dart
void main() {
  testWidgets('Widgetテスト', (WidgetTester tester) async {
    // ウィジェットを起動させる
    await tester.pumpWidget(MateriapApp(
      home: MyWidget(),
    ));
    ....
}
```

# Providerを使っているときのWidgetTest

[Providerパッケージ](https://pub.dev/packages/provider)を使っていて、ウィジェット内からアクセスしているようなパターンがあると思います。
たとえば、こんな感じ。

```Dart
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`などで囲っていてもです。

```console
The following ProviderNotFoundException was thrown running a test:
Error: Could not find the correct Provider above this MyWidget Widget
```

解決策は、`MyApp`でやっているようにProviderを初期化することです。

```Dart
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,
    ),
  );
}
```

クラスにしてもいいかも知れないけど~~面倒なので~~ただの関数。
使うときはこうなります。

```Dart
    await tester.pumpWidget(testMainViewWidget(MyWidget()));
```

`ViewModel`クラスが増える度に関数も増えちゃうけど、それはまた後で考えます(汗)

# 色やテキストスタイルなどの確認

色やサイズを条件で変えることはよくあると思いますが、そのチェックの仕方です。

## 1. TextStyle
まずは`Text`のスタイルのチェック方法です。
フォントサイズ、色などの確認はこれで出来ます。

```Dart
      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を定義しておいてそれと比較する方が良いでしょうね。そうしないと、スタイルを変えたときにテストもいちいち変えなければなりません。

```Dart
final TextStyle myTextStyle = TextStyle(
  fontSize: 12.0,
  color: Colors.grey[500],
);
```

こんな風にオブジェクトを用意しておけば、スタイル設定をするところでも、テストでも、使えます。

```Dart
/// Textウィジェットへの設定
Text("aaa",
  style: myTextStyle,
);

/// WidgetPredicateで使う
WidgetPredicate predicate = (Widget widget) =>
      widget is Text &&
      widget.style == myTextStyle
```

## 2. ContainerのDecoration

`Container`に枠や色を付けて、他のウィジェットの背景としていることも多いと思います。
やりかたは`Text`の時と全く同じで、`WidgetPredicate`を使います。

```Dart
   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/