Search code examples
flutterdartdart-asyncflutter-integration-test

Flutter integration tests - how to ignore exceptions thrown inside `Timer` callbacks?


I'm trying to write a test for an app which isn't well-implemented, and has an exception that is thrown inside a Timer callback. I'd like to ignore that exception inside my integration test, because I'm not interested in that exception.

That's a minimal example of the problem I'm having:

import 'dart:async';

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets(
    'exception test',
    (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: Text('Hello, World!'),
            ),
          ),
        ),
      );

      // Let's assume this Timer is created inside the app, and I do not
      // want to change it.
      Timer.periodic(
        const Duration(seconds: 2),
        (_) => throw Exception('thrown on purpose'),
      );

      await Future<void>.delayed(Duration(seconds: 3));
  );
}

Running the above code with flutter test integration_test/exception_test.dart gives me:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following _Exception was thrown running a test:
Exception: thrown on purpose

When the exception was thrown, this was the stack:
#0      main.<anonymous closure>.<anonymous closure> (file:///Users/bartek/example_project/integration_test/exception_test.dart:31:16)
#16     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)
(elided 15 frames from class _Timer, dart:async, and package:stack_trace)

The test description was:
  exception test
════════════════════════════════════════════════════════════════════════════════════════════════════
00:30 +0 -1: exception test [E]
  Test failed. See exception logs above.
  The test description was: exception test

Now, I'd like to ignore that exception. I tried doing 2 things:

but I have had no success.

I tried resetting the callback (remembering to restore it at the end of the test):

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets(
    'exception test',
    (tester) async {
      final oldCallback = FlutterError.onError;
      FlutterError.onError = (details) { /* do nothing on purpose */ };

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: Text('Hello, World!'),
            ),
          ),
        ),
      );

      Timer.periodic(
        const Duration(seconds: 2),
        (_) => throw Exception('thrown on purpose'),
      );

      await Future<void>.delayed(Duration(seconds: 3));
      FlutterError.onError = oldCallback;
    },
  );
}

but the internal assertion is still violated:

  'package:flutter_test/src/binding.dart': Failed assertion: line 954 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.
  dart:core                                                                                                              _AssertionError._throwNew
  package:flutter_test/src/binding.dart 954:14                                                                           TestWidgetsFlutterBinding._runTest.handleUncaughtError
  package:flutter_test/src/binding.dart 959:9                                                                            TestWidgetsFlutterBinding._runTest.<fn>

I also tried doing:

FlutterError.onError = (details) {
  tester.takeException();
  oldCallback!(details);
};

but this, understandably, gives the original error.


Solution

  • Thanks to @ermekk, who pointed me to this post, I was able to implement this:

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:integration_test/integration_test.dart';
    
    void main() {
      IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    
      testWidgets(
        'exception test',
        (tester) async {
          await runZonedGuarded(
            () async {
              final oldCallback = FlutterError.onError;
              FlutterError.onError = (details) {/* do nothing on purpose */};
    
              await tester.pumpWidget(
                MaterialApp(
                  home: Scaffold(
                    body: Center(
                      child: Text('Hello, World!'),
                    ),
                  ),
                ),
              );
    
              Timer.periodic(
                const Duration(seconds: 2),
                (_) => throw Exception('thrown on purpose'),
              );
    
              await Future<void>.delayed(Duration(seconds: 3));
              FlutterError.onError = oldCallback;
            },
            (error, stack) {
              print('zone caught error: $error');
            },
          );
        },
      );
    }
    

    It's the good-enough temporary solution I was looking for. Of course, it suppresses all errors, so be careful and remove it as soon as the original exception is fixed and no longer thrown.

    Also, for a reason which I didn't investigate, the test never finishes when there's a failing assertion, e.g. expect(true, false).