Floor: How to perform genuine async testing

113 views Asked by At

I am writing tests for a Widget that makes use of a Floor database to store and retrieve data. The test cases use a inMemory database instead of a mocked one. This means all the data operations are genuinely async in the tests as well. The problem I am facing is that my async method calls are being completed well after the tests completed.

I have the following unit test:

testWidgets('test add new set', (WidgetTester tester) async {
  await pumpApp(tester, exercisesProvider, setRepository, exerciseRepository);
  await tester.runAsync(() async {
    var bench = find.text('Bench Press');
    expect(bench, findsOneWidget);
    await tester.tap(bench);
    await tester.pump();
    await tester.pump();
    // Should be found if content is loaded
    expect(find.text('Clear'), findsOneWidget);
  });

The test fails with the following output:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown while running async test code:
Expected: exactly one matching node in the widget tree
  Actual: _TextFinder:<zero widgets with text "Clear" (ignoring offstage widgets)>
   Which: means none were found but one was expected
...
The exception was caught asynchronously.
════════════════════════════════════════════════════════════════════════════════════════════════════
FETCH COMPLETED

The widget under test:

@override
Widget build(BuildContext context) {
  // This async method is executed too late
  Provider.of<SetProvider>(context, listen: false)
      .fetchGroupedSetsByExercise(widget.selectedExercise.id!);

  return Column(
    children: [
      Expanded(
        child: Consumer<SetProvider>(builder: (context, setProvider, chil
          final groupedSets = setProvider.groupedSets;
          if (setProvider.groupedSetsState == IOState.loading ||
              setProvider.groupedSetsState == IOState.initial) {
            return ...;
          } else if (setProvider.groupedSetsState == IOState.error) {
            return ...;
          } else if (groupedSets.isEmpty) {
            return ...;
          }
          // Contains the clear button and more
          return buildSetList(groupedSets, setProvider);
        }),
      ),
    ],
  );

The provider:

Future<void> fetchGroupedSetsByExercise(int exerciseID,
    {bool? reverse}) async {
  groupedSets = groupSetsByDate(
      await fetchSetsByExercise(exerciseID, reverse: reverse));
  lastSetEntry = groupedSets.values.lastOrNull?.lastOrNull;
  groupedSetsState = IOState.success;
  notifyListeners();
  print("FETCH COMPLETED");
}

As you can see in the test output fetchGroupedSetsByExercise is always executed after the test is already over. I am aware that you need to use await to wait for a async function to complete however, how do I test my widget? My widget keeps track of the fetch state itself (in groupedSetsState) and I cannot await anything, rather the provider will notify the widget once the data arrived.

I have also tried using:

  • FutureBuilder
  • Awaiting multiple pump calls
  • Awaiting pumpAndSettle
  • Removing the tester.runAsync line

All with the same result, the test fails because the async method is being executed too late. How do I write genuine async tests like in my case?

2

There are 2 answers

0
Josip Domazet On BEST ANSWER

Turns out my overall setup was correct but the Floor package requires you to add this line to the test whenever you need to await some async Floor operations:

await Future<void>.delayed(const Duration(milliseconds: 100));

The entire code looks like this now:

testWidgets('test add new set', (WidgetTester tester) async {
  await pumpApp(
      tester, exercisesProvider, setRepository, exerciseRepository,
      setProvider);
  await tester.runAsync(() async {
    var bench = find.text('Bench Press');
    expect(bench, findsOneWidget);
    await tester.tap(bench);
    await tester.pump();
    await Future<void>.delayed(const Duration(milliseconds: 100));
    await tester.pump();
    // Should be found if content is loaded
    expect(find.text('Clear'), findsOneWidget);

I found this info in the unit tests of their example project.

0
Abhi Tripathi On

True async testing can be achieved via mockito. You can create stubs for your DB call. You'll be able to intercept when that function completes using

unitlCalled()