Consider the following class:
enum LoginState { loggedOut, loggedIn }
class StreamListener {
final FirebaseAuth _auth;
LoginState _state = LoginState.loggedOut;
LoginState get state => _state;
StreamListener({required FirebaseAuth auth}) : _auth = auth {
_auth.userChanges().listen((user) {
if (user != null) {
_state = LoginState.loggedIn;
} else {
_state = LoginState.loggedOut;
}
});
}
}
I would like to test that when a user login the state changes from loggedOut to loggedIn, see the following test code:
class FakeUser extends Fake implements User {}
@GenerateMocks([FirebaseAuth])
void main() {
StreamController<User?> controller = StreamController<User?>();
final User value = FakeUser();
setUp(() {
controller = StreamController.broadcast();
});
tearDown(() {
controller.close();
});
test('Stream listen test', () {
final MockFirebaseAuth mockAuth = MockFirebaseAuth();
when(mockAuth.userChanges()).thenAnswer((_) => controller.stream);
StreamListener subject = StreamListener(auth: mockAuth);
controller.add(value);
expect(subject.state, LoginState.loggedIn);
});
}
However, due to the async behaviour the login state is still loggedOut. How could I test this properly?
CodePudding user response:
I don't think that you can test that in a way that is strictly correct. Your StreamListener
class promises to update state
in response to Stream
events, but it's asynchronous, you have no formal guarantee when those updates might happen, and you have no way to notify callers when those updates eventually do occur. You could solve that by modifying StreamListener
to provide a broadcast Stream<LoginState>
that is emitted whenever _state
changes.
From a practical perspective of being good enough, there are a few things you could do:
Rely on Dart's event loop to invoke all of the
Stream
's listeners synchronously. In other words, after adding an event to yourStream
, allow Dart to return the event loop so that theStream
's listeners can execute:test('Stream listen test', () async { ... StreamListener subject = StreamListener(auth: mockAuth); controller.add(value); await Future<void>.value(); expect(subject.state, LoginState.loggedIn);
Since you are using a broadcast
Stream
, you alternatively could rely on multipleStream
listeners firing in order of registration. (I don't see any formal documentation guaranteeing that ordering, but I think it is the sanest behavior.) Your test could register its own listener after your object has registered its listener, useexpectAsync1
to verify that the test's listener is called, and have the test's listener verify the state of your object:StreamListener subject = StreamListener(auth: mockAuth); controller.stream.listen(expectAsync1((event) { expect(event, value); expect(subject.state, LoginState.loggedIn); })); controller.add(value);
Or combine the approaches:
test('Stream listen test', () async { ... var eventReceived = Completer<void>(); StreamListener subject = StreamListener(auth: mockAuth); controller.stream.listen((_) => eventReceived.complete()); controller.add(value); await eventReceived.future; expect(subject.state, LoginState.loggedIn);