I'm facing an issue while doing widget testing on a widget that throws an exception during a Future
.
Code to reproduce the problem
Here's a simple testwidget that reproduces the problem in a very simple way (thanks to Remi Rousselet for the simplification of the problem).
testWidgets('This test should pass but fails', (tester) async {
final future = Future<void>.error(42);
await tester.pumpWidget(FutureBuilder(
future: future,
builder: (_, snapshot) {
return Container();
},
));
});
Expected result
I expected the test to complete without error. Instead it fails with the following error :
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The number 42 was thrown running a test.
When the exception was thrown, this was the stack:
#2 main.<anonymous closure> (file:///C:/Projects/projet_65/mobile_app/test/ui/exception_test.dart:79:18)
#5 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:0:0)
#8 TestWidgetsFlutterBinding._runTest (package:flutter_test/src/binding.dart:577:14)
#9 AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:993:24)
#15 AutomatedTestWidgetsFlutterBinding.runTest (package:flutter_test/src/binding.dart:990:15)
#16 testWidgets.<anonymous closure> (package:flutter_test/src/widget_tester.dart:106:22)
#17 Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:168:27)
#20 Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:0:0)
#21 Invoker.waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:250:15)
#27 Invoker._onRun.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/invoker.dart:399:21)
(elided 17 frames from class _FakeAsync, package dart:async, and package dart:async-patch)
The test description was:
This test should pass but fails
════════════════════════════════════════════════════════════════════════════════════════════════════
What I've tried
I've tried to expect
the error like I would have done if it wasn't in a Future
with:
expect(tester.takeException(), equals(42));
But that statement fails with the following error
The following TestFailure object was thrown running a test (but after the test had completed):
Expected: 42
Actual: null
edit:
@shb answer is correct for the case exposed. Here's a slight modification that break it and gives back the initial error. This case is more susceptible to happen in real apps (and that's the case for me)
testWidgets('This test should pass but fails', (tester) async {
Future future;
await tester.pumpWidget(
MaterialApp(
home: Row(
children: <Widget>[
FlatButton(
child: const Text('GO'),
onPressed: () { future = Future.error(42);},
),
FutureBuilder(
future: future,
builder: (_, snapshot) {
return Container();
},
),
],
),
));
await tester.tap(find.text('GO'));
});
note: I have voluntarily ommitted the tester.runAsync
proposed by @shb to match the initial question as it does not work in that particular case
Wrap your code with await tester.runAsync(() async { .. }
From the official documentation runAsync<T>
Runs a callback that performs real asynchronous work.
This is intended for callers who need to call asynchronous methods where the methods spawn isolates or OS threads and thus cannot be executed synchronously by calling pump.
see below
testWidgets('This test should pass but fails', (tester) async {
await tester.runAsync(() async {
final future = Future<void>.error(42);
await tester.pumpWidget(FutureBuilder(
future: future,
builder: (_, snapshot) {
return Container();
},
));
});
});
EDIT:
(second issue OP raised)
in such cases use
Future.delayed(Duration.zero, () {
tester.tap(find.text('GO'));
});
Full snippet below
testWidgets('2nd try This test should pass but fails', (tester) async {
Future future;
await tester.runAsync(() async {
await tester.pumpWidget(
MaterialApp(
home: Row(
children: <Widget>[
FlatButton(
child: const Text('GO'),
onPressed: () {
future = Future.error(42);
},
),
FutureBuilder(
future: future,
builder: (_, snapshot) {
return Container();
},
),
],
),
),
);
Future.delayed(Duration.zero, () {tester.tap(find.text('GO'));});
});
});
Edit 2:
It was later found that
Future.delayed(Duration.zero, () { tester.tap(find.text('GO')); });
is not being called.