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
Answers
When running the third test, it throws this exception:
which is because of these two lines
You’re trying to pump a frame after the exception is thrown, so you should only use
await tester.pumpAndSettle();
As per documentation on runAsync, I believe you should move
tester.takeException();
out of innerasync
block like this and get rid ofpump()
, like this: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 totakeException
after callback finished.