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:
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:
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:
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.
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:
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.