Jonathan
Jonathan

Reputation: 341

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

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.

Upvotes: 5

Views: 6804

Answers (1)

Nkosi
Nkosi

Reputation: 247133

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

Upvotes: 3

Related Questions