ちゃんと Widget Test を書く時に役立つ実践的な話。
書き方のヒントは「flutter 自体の test を読めば得られることが多い」のを覚えておくのも大事。
特定の Asset 画像を読み込んだ Image Widget を確認したい
こんな感じの widget があったとして、
class Sample extends StatelessWidget {
final Image foo;
}
foo が 'assets/images/foo.jpeg'
により生成されている Sample の数をかぞえる方法。
こんな感じ。
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 個あることを確認
親のプロパティを確認したい
例えばこんな感じの Widget があったとして、
Stack に MyCustomPainter がある Sample Widget の text は、'yeah' であることを確認したい。
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
を使う。
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');
スクリーンのサイズを変えたい
setUp(() {
WidgetsBinding.instance.renderView.configuration = TestViewConfiguration(size: const Size(650, 1100));
});
Flutter system の platform メソッドをどうするか
モックする。
例えば Clipboard。
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData') {
return const <String, dynamic>{'text': 'hoge'};
}
return null;
});
Clipboard の内部実装をチラ見すると、こうモックすべきと分かる。
実装に飛んで確認するの大事。
もう一つ例を挙げると、packageInfo はこうなる。
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 が用意されているおかげで、こう書ける。
SharedPreferences.setMockInitialValues({
'flutter.foo': 'bar',
});
独自 platform メソッドをどうするか
mockito を使うのが良い。
例えばこんな感じ。
import 'package:mockito/mockito.dart';
import 'package:foo/bar/othello_engine.dart';
class OthelloEngineMock extends Mock implements OthelloEngine {}
final othelloEngineMock = OthelloEngineMock();
when(othelloEngineMock.getMoves()).thenAnswer((_) => Future<String>.value('f5f6f7'));
ポップアップメニューボタンを押したい
失敗例: tester の気持ちをわかっていない。tester に見えていない 何らかを tap させようとする。
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 をタップしてからメニューの何らかをタップする。
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 させたい
await tester.fling(find.byType(Hoge), const Offset(0, 500), 2000);
await tester.pumpAndSettle(const Duration(seconds: 5));
特定の Icon を使ってる IconButton をタップしたい
hashcode を見ればいい
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 を使う
when(fooMockClass.echo(any)).thenReturn('yeah');
CachedNetworkImageProvider を中の url で判別したい
ここまでの記述を踏まえれば答えは簡単で、こう書けばいいだけ。
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 を使う。
await tester.runAsync(() async {
// pump を呼んでも同期的に実行できない非同期処理を含むテストコード
await tester.pump(Duration.zero);
});