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:
- Provide a mock of
UserService
- Define what
UserService
is going to answer when callinggetUser
, in this case, aSynchronousFuture
that will run immediately. - Calling to
loadUser
, that calls togetUser
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
There’s 5 methods we need to override from the Future
abstract class:
asStream
catchError
then
timeout
whenComplete
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.
Want to learn more Android and Flutter? Check my courses here.