How do I write a unit test for a Flutter method that completes later with a future?

Issue

I’m writing a unit test for a Flutter method that calls an async method and then returns, leaving the async to complete as and when. My test fails “after it had already completed”.

Here’s my test:

    test('mark as viewed', () {
      final a = Asset();
      expect(a.viewed, false);
      a.markAsViewed();
      expect(a.viewed, true);
    });

and here’s the method it’s testing:

  void markAsViewed() {
    viewed = true;
    Repository.get().saveToStorage();
  }

The saveToStorage() method is an async that I just leave to execute in the background.

How do I make this work? The test failure tells me Make sure to use [expectAsync] or the [completes] matcher when testing async code. but I can’t see how to do that. Can anyone explain or else point me to the right documentation please? I can’t find anything about how to handle these asyncs when it’s not a Future that’s being returned, but just being left to complete separately.

To be clear – this unit test isn’t about testing whether it’s saved to storage, just a basic test on setting viewed to be true.

Edited

The error is as follows:

package:flutter/src/services/platform_channel.dart 319:7  MethodChannel.invokeMethod
===== asynchronous gap ===========================
dart:async                                                _asyncErrorWrapperHelper
package:exec_pointers/asset_details.dart                  Repository.saveToStorage
package:exec_pointers/asset_details.dart 64:22            Asset.markAsViewed
test/asset_details_test.dart 57:9                         main.<fn>.<fn>
This test failed after it had already completed. Make sure to use [expectAsync]
or the [completes] matcher when testing async code.

Solution

This code is tightly coupled to implementation concerns that make testing it in isolation difficult.

It should be refactored to follow a more SOLID design with explicit dependencies that can be replaced when testing in isolation (unit testing)

For example

class Asset {
    Asset({Repository repository}) {
        this.repository = repository;
    }

    final Repository repository;
    bool viewed;

    void markAsViewed() {
        viewed = true;
        repository.saveToStorage();
    }

    //...
}

That way when testing a mock/stub of the dependency can be used to avoid any unwanted behavior.

// Create a Mock Repository using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockRepository extends Mock implements Repository {}

main() {
  test('mark as viewed', () {
    final repo = MockRepository();
    // Use Mockito to do nothing when it calls the repository
    when(repo.saveToStorage())
      .thenAnswer((_) async => { });

    final subject = Asset(repo);
    expect(subject.viewed, false);
    subject.markAsViewed();
    expect(subject.viewed, true);
    //
    verify(repo.saveToStorage());
  });
}

The test should now be able to be exercised without unexpected behavior from the dependency.

Reference An introduction to unit testing
Reference Mock dependencies using Mockito
Reference mockito 4.1.1

Answered By – Nkosi

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.