Josip Domazet
Josip Domazet

Reputation: 2890

Floor: How to perform genuine async testing

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:

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?

Upvotes: 2

Views: 277

Answers (2)

Josip Domazet
Josip Domazet

Reputation: 2890

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.

Upvotes: 1

Abhi Tripathi
Abhi Tripathi

Reputation: 546

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()

Upvotes: 0

Related Questions