22

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

3
  • (Never mind, disregard my earlier comments.) Commented Jun 14, 2019 at 15:54
  • @Muldec what happens if you wrap the tester.tap in runAsync? Commented Jun 25, 2019 at 19:55
  • @RémiRousselet same error plus a new one The following assertion was thrown running a test (but after the test had completed): 'package:flutter_test/src/binding.dart': Failed assertion: line 1020 pos 12: '_currentFakeAsync !=null': is not true. (meaning that two error are raised by the test) Commented Jun 26, 2019 at 7:38

3 Answers 3

15
+300

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();
        },
      ));

    });

  });

test passed

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'));});
});
});

screenshot

Edit 2:

It was later found that

Future.delayed(Duration.zero, () { tester.tap(find.text('GO')); });

is not being called.

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you for your answer. This solves the initial question asked. I edited the post to credit your answer and add a code where this solution does not work (the problem has been over simplified initially). What do you think of it ?
the test doesn't fail but the Future in the Future.delayed isn't called either. I've added a print statement to check if the code is run, but nothing is displayed on the console.
@Muldec yeah Looks so. I've edited the answer accordingly
Have you tried awaiting the future? Also instead of Future.delayed, consider Future.microtask.
@RémiRousselet Future.microtask does execute but it doesn't work either
12

Futures report errors to their listeners. If a Future doesn't have listeners it informs the Zone about the uncaught error (src). This is where the testing framework gets the error from.

One way to overcome this error is to wait for listeners before erroring the Future.

  testWidgets('This passes', (tester) async {
    final Completer completer = Completer();
    await tester.pumpWidget(FutureBuilder(
      future: completer.future,
      builder: (_, snapshot) {
        return Container();
      },
    ));

    // has subscribers, doesn't inform Zone about uncought error
    completer.completeError(42);
    tester.pumpAndSettle();
  });

1 Comment

This is true if you have the control over the error. How about a code that throw an error depending of a specific case ? How would you handle it ? I updated my question with a slightly more complex code.
3

I think that the problem is that you're not catching the error and that makes the app crash.

I've tried catching the error and the test passes:

here is the code:


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).catchError((error) {});
            },
          ),
          FutureBuilder(
            future: future,
            builder: (_, snapshot) {
              return Container();
            },
          ),
        ],
      ),
    ));

    await tester.tap(find.text('GO'));
  });

5 Comments

Yes and no. When there is an error in the future builder, it takes the data as null. But the Future itself should use a catchError for that case.
@SergioBernal's point seems valid. the test passes for Future.value(42);
Problem is : the catchError on the Future handles the error and ruins the point of the snapshot.hasError in the FutureBuilder.builder. This will return false has the error has been handled. So testing (and using) the behavior of the FutureBuilder becomes unclear as to why the error message displayed to the user is done on !snapshot.hasData and not on snapshot.hasError.
@SergioBernal FutureBuilder already calls catchError on the Future. The issue is not that it's not caught, but caught asynchronously.
This code just suppresses the error which is not normally an acceptable solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.