A Short Excursion Into The Pitfalls Of Flutter Widget Testing

Flutter category image

Introduction

Sometimes testing is a joy and sometimes not. Here is a short excursion into the pitfalls of Flutter widget testing.

I am currently developing a rather complex Flutter web app. Overall, it’s going great and I like the progress. But I wouldn’t write a short excursion into the pitfalls of Flutter widget testing if everything was perfect.

The project is rather complicated and the UI has a lot to offer. So I am investing heavily in testing it properly with widget tests.

Sadly, not everything is going great so far in that regard. I discover new problems with the tests on a daily basis. Here are some examples.

🔔 Hint

Fun fact: I asked ChatGPT for help. The answers were useless.

Overflow errors in tests

A common issue that I have in my tests are overflow errors. We all know overflows, why they happen, and how we can fix them. But the main problem is that those errors only happen in the tests.

When I run my app, there are no overflow errors. Everything is fine.

And I couldn’t find really find a satisfying explanation for this.

Since I am not the first one to run into that problem, there is a “solution”: Override the error handler and ignore everything related to overflow errors!

Here is the StackOverflow discussion and this is the code:

Dart
FlutterError.onError = _onError_ignoreOverflowErrors;

Function _onError_ignoreOverflowErrors = (
  FlutterErrorDetails details, {
  bool forceReport = false,
}) {
  assert(details != null);
  assert(details.exception != null);
  // ---

  bool ifIsOverflowError = false;

  // Detect overflow error.
  var exception = details.exception;
  if (exception is FlutterError)
    ifIsOverflowError = !exception.diagnostics
        .any((e) => e.value.toString().startsWith("A RenderFlex overflowed by"));

  // Ignore if is overflow error.
  if (ifIsOverflowError)
    print('Overflow error.');

  // Throw others errors.
  else
    FlutterError.dumpErrorToConsole(details, forceReport: forceReport);
};

Be aware that you need to assign this function in every test. It’s not enough to assign it in a setUp() or setUpAll() function!

SelectableText can be a pain

Another pitfall that I didn’t know about is SelectableText.

While this is usually not needed as much in mobile apps, I use it quite frequently in web apps so that users can copy stuff they need easily. But it’s not a good solution to just replace every Text widget with a SelectableText widget.

Let’s see this example here:

Dart
class SimpleTextField1 extends StatelessWidget {
  const SimpleTextField1({super.key});

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(label: Text("my label")),
    );
  }
}

class SimpleTextField2 extends StatelessWidget {
  const SimpleTextField2({super.key});

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(label: SelectableText("my label")),
    );
  }
}

Here I have two classes that wrap a TextField. The only difference is that the label is a Text widget in the first class and a SelectableText in the second one.

And here is the test code:

Dart
void main() {
  testWidgets("Testing", (tester) async {
    final sut = MaterialApp(home: Scaffold(body: SimpleTextField1()));

    await tester.pumpWidget(sut);
    await tester.pumpAndSettle();

    await tester.enterText(find.byType(TextField), "test");
    expect(find.text("test"), findsOneWidget);
    expect(find.text("my label"), findsOneWidget);
  });

  testWidgets("Testing", (tester) async {
    final sut = MaterialApp(home: Scaffold(body: SimpleTextField2()));

    await tester.pumpWidget(sut);
    await tester.pumpAndSettle();

    await tester.enterText(find.byType(TextField), "test");
    expect(find.text("test"), findsOneWidget);
    expect(find.text("my label"), findsOneWidget);
  });
}

The first text works fine, but the second fails. The enterText method has a problem to find a single target to put the text in. For some reason SelectableText is also a valid target which leads to the “Too many elements” exception.

Screenshot of an error in a widget test. The test tries to enter text in a TextField but fails because the TextField contains a label widget of SelectableText which causes the error to happen.
Screenshot of an error in a widget test. The test tries to enter text in a TextField but fails because the TextField contains a label widget of SelectableText which causes the error to happen.

Solution?!

You cannot fix the enterText method, so I just replaced the widget.

And let’s be honest: Who copies the label of a text field?

Shouldn’t be a loss for the user.

Be aware of buttons with icons

Oh yes, the Finder might surprise you in a negative way.

For example when it comes to buttons.

All Material buttons have named constructors to quickly set an icon and a label (TextButton, FilledButton, ElevatedButton, OutlinedButton).

But try to find them by type in a widget test:

Dart
TextButton.icon(...);

find.byType(TextButton); // finds nothing

The reason this fails is because the icon() constructors create a private type and not the actual button you expect. find.byType then of course fails because the types don’t match. It’s obvious once you know it. But this is part of the things you probably don’t expect in the first place.

You can however use find.byIcon() which will always work.

Conclusion

This was a short excursion into the pitfalls of Flutter widget testing. In general it works well. But sometimes, it’s a frustrating topic. I hope this article gives you some hints to ship around some hurdles.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Become A Firebase Expert!

Take a deep dive into Firebase and learn how to really handle their services. This ebook makes you an instant Firebase professional!

Flutter โค๏ธ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!