[Flutter] Widget Test の逆引き Tips 

ちゃんと [Widget Test](https://flutter.dev/docs/testing#widget-tests) を書く時に役立つ実践的な話。

書き方のヒントは「**[flutter 自体の test](https://github.com/flutter/flutter/tree/master/packages/flutter/test)** を読めば得られることが多い」のを覚えておくのも大事。

## 特定の Asset 画像を読み込んだ Image Widget を確認したい

こんな感じの widget があったとして、

```sample.dart
class Sample extends StatelessWidget {
  final Image foo;
}
```

foo が `'assets/images/foo.jpeg'` により生成されている Sample の数をかぞえる方法。
こんな感じ。

```foo_test.dart
const fooImageAsset = 'assets/images/foo.jpeg'
final sampleList = find.byWidgetPredicate((Widget widget) {
      if (widget is Sample && widget.foo.image is AssetImage) {
        final assetImage = widget.foo.image as AssetImage;
        return assetImage.keyName == fooImageAsset;
      }
      return false;
    });
expect(sampleList, findsNWidgets(5)); // 5 個あることを確認
```

参考: https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/image_package_asset_test.dart

## 親のプロパティを確認したい

例えばこんな感じの Widget があったとして、
Stack に MyCustomPainter がある Sample Widget の text は、'yeah' であることを確認したい。

```sample.dart

class Sample extends StatelessWidget {
  Sample({@required this.text});

  final text;

  @override
  Widget build(BuildContext context) {
    final painter = somethingCondition ? MyCustomPainter(): BarWidget(); 
    return Stack(
      children: <Widget>[
        const Text('foo'),
        CustomPaint(painter: painter)
      ],
    );
  }
}

class MyCustomPainter extends CustomPainter {
  // 省略
}
```

`ancestorWidgetOfExactType` を使う。

```foo_test.dart
final myCustomPainter = find.byWidgetPredicate((Widget widget) => widget is CustomPaint && widget.painter is MyCustomPainter);
final sample = tester.element(myCustomPainter).ancestorWidgetOfExactType(Sample) as Sample;
expect(Sample.text, 'yeah');
```

## スクリーンのサイズを変えたい
```foo_test.dart
setUp(() {
    WidgetsBinding.instance.renderView.configuration = TestViewConfiguration(size: const Size(650, 1100));
  });
```

## Flutter system の platform メソッドをどうするか
モックする。
例えば Clipboard。

```foo_test.dart
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
  if (methodCall.method == 'Clipboard.getData') {
    return const <String, dynamic>{'text': 'hoge'};
  }
  return null;
});
```

Clipboard の内部実装をチラ見すると、こうモックすべきと分かる。
実装に飛んで確認するの大事。

もう一つ例を挙げると、[packageInfo](https://pub.dartlang.org/packages/package_info) はこうなる。

```foo_test.dart
const MethodChannel('plugins.flutter.io/package_info').setMockMethodCallHandler((MethodCall methodCall) async {
    if (methodCall.method == 'getAll') {
      return <String, dynamic>{
        'appName': 'foo',
        'packageName': 'foo',
        'version': '0.0.1',
        'buildNumber': 'foo',
      };
    }
    return null;
  });
```

ちなみに、SharedPreference には [setMockInitialValues](https://pub.dev/documentation/shared_preferences/latest/shared_preferences/SharedPreferences/setMockInitialValues.html) が用意されているおかげで、こう書ける。

```foo_test.dart
SharedPreferences.setMockInitialValues({
  'flutter.foo': 'bar',
});
```

## 独自 platform メソッドをどうするか
[mockito](https://pub.dartlang.org/packages/mockito) を使うのが良い。

例えばこんな感じ。

```othello_engine_mock.dart
import 'package:mockito/mockito.dart';
import 'package:foo/bar/othello_engine.dart';

class OthelloEngineMock extends Mock implements OthelloEngine {}
```

```othello_engine_test.dart
final othelloEngineMock = OthelloEngineMock();
when(othelloEngineMock.getMoves()).thenAnswer((_) => Future<String>.value('f5f6f7'));
```

## ポップアップメニューボタンを押したい
失敗例: tester の気持ちをわかっていない。**tester に見えていない** 何らかを tap させようとする。

```foo_test.dart
final fooButton = find.byWidgetPredicate((Widget widget) => widget is Text && widget.data == 'foo');
expect(fooButton, findsOneWidget); // popupMenu を開いてないから tester はこれを見つけられない
await tester.tap(fooButton);
await tester.pumpAndSettle();
```


成功例: popupMenuButton をタップしてからメニューの何らかをタップする。

```foo_test.dart
final popupMenuButton = find.byType(PopupMenuButton);
await tester.tap(popupMenuButton);
await tester.pumpAndSettle();

final fooButton = find.byWidgetPredicate((Widget widget) => widget is Text && widget.data == 'foo');
expect(fooButton, findsOneWidget);
await tester.tap(fooButton);
await tester.pumpAndSettle();
```

## Pull Refresh させたい

```foo_test.dart
await tester.fling(find.byType(Hoge), const Offset(0, 500), 2000);
await tester.pumpAndSettle(const Duration(seconds: 5));
```

## 特定の Icon を使ってる IconButton をタップしたい
hashcode を見ればいい

```foo_test.dart
final searchIconButton = find.byWidgetPredicate((Widget widget) {
  if (widget is IconButton) {
    final icon = widget.icon as Icon;
    return icon.icon.hashCode == Icons.search.hashCode;
  }
  return false;
});
await tester.tap(searchIconButton);
```

## 引数の値に関わらずモックする
any を使う

```foo_test.dart
when(fooMockClass.echo(any)).thenReturn('yeah');
```

## CachedNetworkImageProvider を中の url で判別したい
ここまでの記述を踏まえれば答えは簡単で、こう書けばいいだけ。

```foo_test.dart
final userImage = find.byWidgetPredicate((widget) {
  if (widget is Image && widget.image is CachedNetworkImageProvider) {
    final imageProvider = widget.image as CachedNetworkImageProvider;
    return imageProvider.url == "https://example.com/foo.png";
  }
  return false;
});
```

## `pump` を呼んでも同期的に実行できない非同期処理をテストしたい時
[runAsync](https://api.flutter.dev/flutter/flutter_test/WidgetTester/runAsync.html) を使う。

```foo_test.dart
await tester.runAsync(() async {
  
  // pump を呼んでも同期的に実行できない非同期処理を含むテストコード

  await tester.pump(Duration.zero);
});
```