考虑以下类:
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;
}
});
}
}
我想测试当用户登录时状态从 loggedOut 更改为 loggedIn,请参阅以下测试代码:
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);
});
}
但是,由于异步行为,登录状态仍处于注销状态。我怎样才能正确测试?
我不认为你可以以一种严格正确的方式进行测试。 您的StreamListener
类承诺更新state
以响应Stream
事件,但它是异步的,您无法正式保证这些更新何时发生,并且您无法在这些更新最终发生时通知调用方。 您可以通过修改StreamListener
来提供每当_state
更改时发出的广播Stream<LoginState>
来解决此问题。
从足够好的实际角度来看,您可以做一些事情:
-
依靠 Dart 的事件循环来同步调用所有
Stream
的侦听器。 换句话说,在将事件添加到Stream
后,允许 Dart 返回事件循环,以便Stream
的侦听器可以执行:test('Stream listen test', () async { ... StreamListener subject = StreamListener(auth: mockAuth); controller.add(value); await Future<void>.value(); expect(subject.state, LoginState.loggedIn);
-
由于您使用的是广播
Stream
,因此您也可以依靠多个Stream
侦听器按注册顺序触发。(我没有看到任何正式的文档保证这种排序,但我认为这是最明智的行为。在对象注册其侦听器后,测试可以注册自己的侦听器,使用expectAsync1
验证测试的侦听器是否已调用,并让测试的侦听器验证对象的状态:StreamListener subject = StreamListener(auth: mockAuth); controller.stream.listen(expectAsync1((event) { expect(event, value); expect(subject.state, LoginState.loggedIn); })); controller.add(value);
-
或者结合这些方法:
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);