fake_async
fake_async copied to clipboard
stream.firstOrNull with timeout confusing behavior
I have stumbled across a difference in behavior between stream.first and stream.firstOrNull when using timeouts with fakeAsync:
// Passes as expected
test('stream.first with timeout', () async {
fakeAsync((FakeAsync async) {
expect(
Stream<int>.value(0).first.timeout(const Duration(seconds: 1)),
completion(0),
);
async.elapse(const Duration(seconds: 10));
});
});
// Fails with timeout exception
test('stream.firstOrNull with timeout', () async {
fakeAsync((FakeAsync async) {
expect(
Stream<int>.value(0).firstOrNull.timeout(const Duration(seconds: 1)),
completion(0),
);
async.elapse(const Duration(seconds: 10));
});
});
I would expect these tests to behave identically given that both streams are guaranteed to have a value. Could this be the intended behavior? My main concern is that fakeAsync is sensitive to the implementation details of methods that otherwise behave identically (in this case because the stream has a value). This would make refactoring tested code a real pain. But maybe I am missing something.
I only ran into this issue while writing tests with fakeAsync. The actual code being tested, which uses firstOrNull, seems to work fine while debugging, but the tests fail because of the timeout.
As a workaround, I am using a custom firstOrNull with a slightly different implementation that makes it work like I would expect:
extension FirstOrNull2Extension<T> on Stream<T> {
Future<T?> get firstOrNull2 async {
final Completer<T?> completer = Completer<T?>.sync();
final StreamSubscription<T> subscription = listen(
completer.complete,
onError: completer.completeError,
onDone: completer.complete,
cancelOnError: true,
);
return completer.future.whenComplete(subscription.cancel);
}
}
(Not sure if this mimics firstOrNull 100%, but dropping it in my code fixed my issues)
My first attempt at an alternate firstOrNull implementation gave me the same problem when applying timeouts in a fakeAsync:
extension FirstOrNull2Extension<T> on Stream<T> {
Future<T?> get firstOrNull2 async {
await for (final T event in this) {
return event;
}
return null;
}
}