Assertions in Dart and Flutter tests: Flutter widgets matchers

This is the part of the ultimate cheat sheet dedicated to:

  • no / one / N widgets matchers,
  • color matcher,
  • parent matchers,
  • error matchers.

In this series:

Flutter widget tests use the same expect() method to verify the actual widget tree content matches expectations. However, the list of matchers is unique to this task.

No / One / N widgets matchers

This group of matchers is the most commonly used in Flutter widget tests.

findsNothing ensures no widget in the widget tree matches the first parameter of the expect() method:

testWidgets('widget test: findsNothing βœ…', (tester) async {
final content = Text('1');
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
expect(find.text('0'), findsNothing);
});

The findsWidgets/findsOneWidget matchers ensure at least/exactly one widget is present in the tree:

testWidgets('widget test: findsWidgets βœ…', (tester) async {
final content = Column(children: [Text('1'), Text('1')]);
await tester.pumpWidget(MaterialApp(home:Scaffold(body: content)));
expect(find.text('1'), findsWidgets);
});
testWidgets('widget test: findsOneWidget ❌', (tester) async {
final content = Column(children: [Text('1'), Text('1')]);
await tester.pumpWidget(MaterialApp(home:Scaffold(body: content)));
expect(find.text('1'), findsOneWidget);
});

In the example above, findsOneWidget matcher failed the test because the widget tree contains two widgets with the text '1'. The test output is:

Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<2 widgets with text "1" (ignoring offstage widgets): [
Text("1", dependencies:[DefaultSelectionStyle, DefaultTextStyle, MediaQuery]),
Text("1", dependencies:[DefaultSelectionStyle, DefaultTextStyle, MediaQuery])]>
Which: is too many

The findsAtLeastNWidgets/findsNWidgets matchers ensure at least/exactly N widgets are present in the widgets tree.

testWidgets('widget test: findsAtLeastNWidgets βœ…', (tester) async {
final content = Column(children: [Text('1'), Text('1')]);
await tester.pumpWidget(MaterialApp(home:Scaffold(body: content)));
expect(find.text('1'), findsAtLeastNWidgets(2));
});
testWidgets('widget test: findsNWidgets βœ…', (tester) async {
final content = Column(children: [Text('1'), Text('1')]);
await tester.pumpWidget(MaterialApp(home:Scaffold(body: content)));
expect(find.text('1'), findsNWidgets(2));
});

Color matcher

isSameColorAs helps verify properties of Color type of any widget:

testWidgets('widget test: isSameColorAs βœ…', (tester) async {
final content = Container(color: Colors.green);
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
expect(
tester.widget<Container>(find.byType(Container)).color,
isSameColorAs(Colors.green),
);
});
testWidgets('widget test: isSameColorAs βœ…', (tester) async {
final content = Text('1', style: TextStyle(color: Colors.green));
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
expect(
tester.widget<Text>(find.text('1')).style!.color,
isSameColorAs(Colors.green),
);
});

Parent matchers

isInCard/isNotInCard helps to verify that widget has at least one/no Card widget ancestor:

testWidgets('widget test: isInCard βœ…', (tester) async {
final content = Card(child: Text('1'));
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
expect(find.text('1'), isInCard);
});
testWidgets('widget test: isNotInCard βœ…', (tester) async {
final content = Text('1');
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
expect(find.text('1'), isNotInCard);
});

Error matchers

We all know that the Container widget cannot accept both color and decoration parameters because of the following assert in its constructor:

assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\\n'
'To provide both, use "decoration: BoxDecoration(color: color)".',
),

If you take a similar approach when implementing your widgets, the throwsAssertionError matcher can help with testing these asserts:

testWidgets('widget test: throwsAssertionError βœ…', (tester) async {
final builder = () => Container(color: Colors.red, decoration: BoxDecoration(color: Colors.red));
expect(() => builder(), throwsAssertionError);
});

Because throwsAssertionError uses throwsA matcher under the hood, the same principle applies here: the function that is expected to throw should be called under the expect() method call.

The throwsFlutterError matcher verifies that a function throws FlutterError. Here is an example from the Flutter framework tests:

testWidgets('widget test: throwsFlutterError βœ…', (tester) async {
final testKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(SizedBox(key: testKey));
expect(() => Navigator.of(testKey.currentContext!), throwsFlutterError);
});

Originally published at Invertase blog. Check out their awesome Authors Program!

Hi! πŸ‘‹πŸ» I’m Anna, Google Developer Expert in Flutter from Ukraine πŸ‡ΊπŸ‡¦ Follow me on Twitter, GitHub, YouTube, Medium to get notifications about my latest work.

It’s early 2023, and we in Ukraine are still fighting against russians committing genocide on our lands. If you find this content useful and have a coin to spare, support us with your donations. Stand with Ukraine!

--

--

Anna Leushchenko πŸ‘©β€πŸ’»πŸ’™πŸ“±πŸ‡ΊπŸ‡¦

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator