If you plan to implement integration testing for your Flutter app, you may want to learn from our recent misadventure. Implementing integration testing can easily become challenging to downright maddening, especially when dealing with multiple libraries like Gherkin (BDD and functional testing) and Flutter Driver or Flutter integration test.
We recently ran into a particularly unusual situation where the Flutter Driver became completely unresponsive while using the Flutter Gherkin library.
// The following async always times out
await FlutterDriverUtils.isPresent(
context.world.driver,
find.byValueKey(key),
timeout: const Duration(seconds: 10),
);
The above code would consistently time out regardless of the duration settings.
The current stable release of flutter_gherkin, version 2.0.0, is almost 2 years old and still uses the flutter_driver library. A new release compatible with the newer integration_test package is currently in the works. We initially suspected that some edge case interaction between the two libraries was the root cause of our problem, but we managed to replicate the issue with the Flutter Driver alone, which meant something else was going on.
We were still seeing the same problematic behaviour after switching to the more recent Flutter integration test library. However, the WidgetTester provided us with a valuable clue. We noticed that sometimes, a call to “pumpAndSettle” would never complete.
// The following async never resolves
await tester.pumpAndSettle();
expect(find.byKey(key), findsOneWidget);
This suggested that the app was maybe continuously scheduling frames and never actually settling down.
Our suspicions were confirmed after profiling the app in Flutter Dev Tools. It became clear that a hidden animation was running repeatedly, forcing constant repaints and causing the Flutter Driver to become unresponsive.
So, how do you deal with this kind of problem?
For starters, it is probably best to avoid repeating animations whenever possible, as it could cause performance degradation.
If a looped animation is absolutely required, it is possible to invoke the runUnsynchronized method on the Flutter Driver to carry out commands immediately without waiting for the frame scheduler to stabilise.
// Usage example in a Flutter Gherkin step
final driver = context.world.driver;
await driver?.runUnsynchronized(() async {
await driver.waitFor(find.byValueKey(key));
});
This can help mitigate the problem of the driver becoming unresponsive.
Finally, if you’re using the integration test library, you can simply pump frames without waiting for the frame scheduler to settle by using “pump” instead of “pumpAndSettle.”
The pump method is responsible for advancing the virtual time in the test environment. It allows the test to simulate the passage of time and trigger any pending asynchronous operations, such as network requests or animations. When you call “pump,” it processes any scheduled tasks, updates the widget tree, and allows any pending microtasks or timers to execute.
Conversely, pumpAndSettle extends the functionality of “pump” by not only advancing time but also waiting for all animations and microtasks to complete before returning control to the test. This method is useful when you want to ensure that all asynchronous operations have finished and the widget tree has settled into its final state before making any assertions or proceeding with the test. It helps avoid potential flakiness in the test results by waiting for all effects of asynchronous operations to be applied.
await tester.pump(const Duration(milliseconds: 100));
expect(find.byKey(key), findsOneWidget);