如何对我的 class 收听流并在 Dart 中正确响应进行单元测试

How to unit test that my class listen to a stream and responds correctly in Dart

考虑以下 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;
      }
    });
  }
}

我想测试一下,当用户登录时状态由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);
  });
}

但是,由于异步行为,登录状态仍为 loggedOut。我怎样才能正确测试它?

我认为您无法以严格正确的方式进行测试。您的 StreamListener class 承诺更新 state 以响应 Stream 事件,但它是异步的,您无法正式保证这些更新何时会发生,您也没有办法当这些更新最终确实发生时通知呼叫者。您可以通过修改 StreamListener 以提供每当 _state 更改时发出的广播 Stream<LoginState> 来解决此问题。

从足够好的实际角度来看,您可以做以下几件事:

  • 依靠 Dart 的事件循环来同步调用所有 Stream 的侦听器。换句话说,在向 Stream 添加事件后,允许 Dart return 事件循环,以便 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);