24
17

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] Widget Test の逆引き Tips

Last updated at Posted at 2019-03-31

ちゃんと Widget Test を書く時に役立つ実践的な話。

書き方のヒントは「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 はこうなる。

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 が用意されているおかげで、こう書ける。

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

独自 platform メソッドをどうするか

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 を使う。

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

  await tester.pump(Duration.zero);
});
24
17
0

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
24
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?