Question

catch/ignore async exceptions from futures

Here is a simple app that has 3 buttons that increment counter.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Counter: $counter'),
            TextButton(
              onPressed: () {
                setState(() {
                  counter++;
                });
              },
              child: const Text('Increment'),
            ),
            TextButton(
              onPressed: () {
                setState(() {
                  counter++;
                  throw Exception('Exception to catch');
                });
              },
              child: const Text('Increment with Exception'),
            ),
            TextButton(
              onPressed: () async {
                await Future.delayed(Duration(seconds: 2)).then(
                  (value) => setState(() {
                    counter++;
                    throw Exception('Exception to catch');
                  }),
                );
              },
              child: const Text('Increment async with Exception'),
            ),
          ],
        ),
      ),
    );
  }
}

First button just increments counter, second is incrementing it but throws exception at the end and third one is doing it after short delay. Now i want to test this buttons with integration_test package. I've written those tests:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:exceptions_in_tests/main.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Increment counter', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.tap(find.text('Increment'));
    await tester.pumpAndSettle();
    expect(find.text('Counter: 1'), findsOneWidget);
  });

  testWidgets('Increment counter with Exception', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.tap(find.text('Increment with Exception'));
    await tester.pumpAndSettle();
    tester.takeException();
    expect(find.text('Counter: 0'), findsOneWidget);
  });

  testWidgets('Increment counter async with Exception', (WidgetTester tester) async {
    await tester.runAsync(() async {
      await tester.pumpWidget(const MyApp());
      await tester.tap(find.text('Increment async with Exception'));
      await tester.pump(Duration(seconds: 2));
      await tester.pumpAndSettle();
      tester.takeException();
      expect(find.text('Counter: 0'), findsOneWidget);
    });
  });
}

First and second tests are passing but third one is failing even when i call tester.takeException(). When i run same tests as unit test without integration_test package all are passing.

My question is how to catch or ignore async exception that came from futures in integration tests? I tried to run it in different zone without success. Overriding Flutter.onError is also not possible because it raises another exception 'package:flutter_test/src/binding.dart': Failed assertion: line 810 pos 14: '_pendingExceptionDetails != null': A test overrode FlutterError.onError but either failed to return it to its original state, or had unexpected additional errors that it could not handle. Typically, this is caused by using expect() before restoring FlutterError.onError.

 2  64  2
1 Jan 1970

Solution

 0

When running the third test, it throws this exception:

'package:flutter_test/src/binding.dart': Failed assertion: line 2034 pos 12: '!_expectingFrame': is not true.

which is because of these two lines

await tester.pump(Duration(seconds: 2));
await tester.pumpAndSettle();

You're trying to pump a frame after the exception is thrown, so you should only use await tester.pumpAndSettle();

2024-07-21
Michael Soliman

Solution

 0

As per documentation on runAsync, I believe you should move tester.takeException(); out of inner async block like this and get rid of pump(), like this:

  testWidgets('Increment counter async with Exception',
      (WidgetTester tester) async {
    await tester.runAsync(() async {
      await tester.pumpWidget(const MyApp());
      await tester.tap(find.text('Increment async with Exception'));
      await tester.pumpAndSettle();
    });
    tester.takeException();

    expect(find.text('Counter: 0'), findsOneWidget);
  });

Explanation: you can't catch exception inside actual running async callback. That's why runAsync will instead catch this exception internally and bring it "out" to be available to takeException after callback finished.

2024-07-23
Sameri11