Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to catch an error coming from a Future in flutter widget test?

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

like image 927
Muldec Avatar asked Jun 14 '19 14:06

Muldec


People also ask

How do you catch future error Flutter?

That error is handled by catchError() . If myFunc() 's Future completes with an error, then() 's Future completes with that error. The error is also handled by catchError() . Regardless of whether the error originated within myFunc() or within then() , catchError() successfully handles it.

How do you show error messages in Flutter?

We already have a SnackBar widget on Flutter to show such errors or warning messages. To display it using ScaffoldMessenger . Inside the SnackBar, the content is a simple text. If you click on the show message button.

How do you use future widget in Flutter?

In Flutter, the FutureBuilder Widget is used to create widgets based on the latest snapshot of interaction with a Future. It is necessary for Future to be obtained earlier either through a change of state or change in dependencies.


3 Answers

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.

like image 167
shb Avatar answered Oct 12 '22 21:10

shb


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();
  });
like image 10
passsy Avatar answered Oct 12 '22 22:10

passsy


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

like image 4
Sergio Bernal Avatar answered Oct 12 '22 20:10

Sergio Bernal