Testing with SynchronousFuture in Flutter

Posted on Jun 24, 2019

SynchronousFuture is a nifty little class that can facilitate testing anything related to Futures in unit tests.

For example, this UserService has a method that returns a Future<User>:

class UserService {  
  Future<User> getUser() { ... }  
}

UserService is being used inside another class or method, for example in Redux inside the Middleware, but also could be inside a BloC pattern, inside a Presenter in MVP, etc.

void loadUser(UserService service) {  
  service.getUser().then((user) {  
    // got the user  
  }).catchError((error) {  
    // got an error  
  });  
} 

When testing loadUser we can provide a Mock of UserService:

class MockUserService extends Mock implements UserService

test(“should load user”, () {  
  final mock = MockUserService();  
  final user = User();

  when(mock.getUser()).thenAnswer((\_) => **SynchronousFuture(user)**);

  loadUser(mock);

  // rest of the test  
});

In this part of the test, we:

  1. Provide a mock of UserService
  2. Define what UserService is going to answer when calling getUser, in this case, a SynchronousFuture that will run immediately.
  3. Calling to loadUser, that calls to getUser internally

SynchronousFuture is a Future that runs the then block immediately.

If instead of SynchronousFuture you used Future.value(user) it will not work, because the value will not be available in the same event-loop iteration.

With SynchronousFuture we were able to test a Future that returns a value immediately, we tested the then case, the “happy path”.

How can we test the catchError path?

It won’t be possible with SynchronousFuture, as it “always works”, but we can build our own:

I am going to call it SynchronousError:

class SynchronousError implements Future {}

There’s 5 methods we need to override from the Future abstract class:

First we are going to add a constructor and a member that will hold the error object:

final Object \_error;

SynchronousError(this.\_error);

For asStream, we want to return a Stream that emits our error:

@override  
Stream<T> asStream() {  
  final StreamController controller = StreamController<T>();  
  controller.addError(_error);  
  controller.close();  
  return controller.stream;  
}

For catchError, we want to run the onError block:

@override  
Future<T> catchError(Function onError, { bool test(dynamic error) }) {  
  onError(_error);  
  return Completer<T>().future;  
}

then is returning a new SynchronousError:

@override  
Future<E> then<E>(dynamic f(T value), { Function onError }) {  
  return SynchronousError<E>(_error);  
}

timeout needs to return a Future.error with an applied timeout:

@override  
Future<T> timeout(Duration timeLimit, { dynamic onTimeout() }) {  
  return Future<T>.error(_error)  
                  .timeout(timeLimit, onTimeout: onTimeout);  
}

And whenComplete can just run the provided action and then return itself:

@override  
Future<T> whenComplete(dynamic action()) {  
  action();  
  return this;  
}

You can find the whole class here:

Now we can write the “failed to load user” test:

test("should fail to load user", () {  
  final mock = MockUserService();  
  final error = "failed to load user";

  when(mock.getUser()).thenAnswer((_) => SynchronousError(error));

  loadUser(mock);

  // rest of the test  
});

The then block inside loadUser is going to be ignored, and instead, the catchError block will run.

Note that you should NOT use SynchronousError (or SynchronousFuture) outside of tests.

In this article I wanted to illustrate that is possible to write your own Future implementations in Dart which can be useful when testing your code.

In my case, I needed to run the catchError block inside a test and after looking at the code from SynchronousFuture, I saw that implementing the same for an error would be possible.

Feel free to modify and improve my implementation, and let me know if you have other ways of solving this problem.

Share Tweet