Flutter blocTest单元测试返回空actual数组问题排查
问题描述
在项目中实现Bloc单元测试时,针对指定功能执行测试过程中,blocTest 函数返回的实际(actual)状态数组为空,无法和预期状态序列匹配。
相关代码
share_bloc_test.dart
返回空actual数组的blocTest测试代码如下:
group('ShareDisconnectGrantor', () { blocTest<ShareBloc, ShareState>( 'emits [ShareLoadSuccess] ' 'state when successfully disconnected grantor', seed: () => ShareLoadSuccess( ConnectedUsersModel( List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')), List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')) ), connectModel: ConnectModel('1234', dateTime, dateTime, dateTime, '1234', '1234') ), setUp: () { when( () => shareRepositoryMock.disconnectFromGrantor(1234, 1234), ).thenAnswer( (_) => Future<ConnectedUsersModel>.value( ConnectedUsersModel( List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')), List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')) ) ), ); }, build: () => ShareBloc( authenticationBloc: AuthenticationBloc( authenticationRepository: authenticationRepositoryMock), shareRepository: shareRepositoryMock), act: (ShareBloc bloc) => bloc.add( const ShareDisconnectGrantor(1234, 1234), ), expect: () => [ ShareLoadSuccess( ConnectedUsersModel( List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')), List<ConnectedUserModel>.filled(1, ConnectedUserModel(1234, '1234', '1234')) ), connectModel: ConnectModel('1234', dateTime, dateTime, dateTime, '1234', '1234') ), ], ); });
share_bloc.dart
对应测试用例的事件处理逻辑代码:
Future<void> _onDisconnectGrantor( ShareDisconnectGrantor event, Emitter<ShareState> emit) async { if (state is ShareLoadSuccess) { final ShareLoadSuccess _currentState = state as ShareLoadSuccess; final ConnectedUsersModel _shareUsers = await shareRepository .disconnectFromGrantor(event.userId, event.grantorId); return emit(_currentState.copyWith(shareUsers: _shareUsers)); } }
share_state.dart
对应测试相关的状态类定义代码:
part of 'share_bloc.dart'; abstract class ShareState extends Equatable { const ShareState(); @override List<Object?> get props => <Object?>[]; } class ShareInitial extends ShareState {} class ShareGranteeSuccess extends ShareState {} class ShareGranteeFailure extends ShareState { final KBMException exception; const ShareGranteeFailure({required this.exception}); @override List<Object?> get props => <Object?>[exception]; } class ShareGrantorSuccess extends ShareState {} class ShareGrantorFailure extends ShareState { final KBMException exception; const ShareGrantorFailure({required this.exception}); @override List<Object?> get props => <Object?>[exception]; } class ShareConnectSuccess extends ShareState {} class ShareConnectFailure extends ShareState { final KBMException exception; const ShareConnectFailure({required this.exception}); @override List<Object?> get props => <Object?>[exception]; } class ShareLoadSuccess extends ShareState { final ConnectedUsersModel shareUsers; final ConnectModel? connectModel; final bool isGrantor; const ShareLoadSuccess(this.shareUsers, {this.connectModel, this.isGrantor = false}); @override List<Object?> get props => <Object?>[shareUsers, connectModel, isGrantor]; ShareLoadSuccess copyWith({ ConnectedUsersModel? shareUsers, ConnectModel? connectModel, bool? isGrantor, }) { return ShareLoadSuccess( shareUsers ?? this.shareUsers, connectModel: connectModel ?? this.connectModel, isGrantor: isGrantor ?? this.isGrantor, ); } } class ShareGrantorAborted extends ShareState {} class ShareSyncInProgress extends ShareState { final ConnectModel? connectModel; const ShareSyncInProgress({this.connectModel}); @override List<Object?> get props => <Object?>[connectModel]; }
问题根因
按出现概率从高到低排序:
- Bloc默认过滤重复状态
Bloc 核心逻辑默认不会发射与当前状态通过==判定完全相等的新状态。当前测试中,mock的disconnectFromGrantor返回的用户列表和seed设置的初始状态里的用户列表完全一致,copyWith生成的新ShareLoadSuccess实例和初始状态值完全相等,被框架直接过滤,因此实际收集到的状态序列为空。
只要ConnectedUsersModel、ConnectedUserModel、ConnectModel都正确继承Equatable并在props中列出所有比较字段,新旧状态就会被判定为相等,不会触发发射。 - AuthenticationBloc状态覆盖初始seed状态
build阶段手动实例化的AuthenticationBloc未做完整mock,如果ShareBloc构造函数中订阅了AuthenticationBloc的状态流,AuthenticationBloc初始化时发出的非预期状态(比如未认证状态、初始状态)会覆盖seed设置的ShareLoadSuccess状态,导致事件处理函数中if (state is ShareLoadSuccess)判断不成立,直接跳过emit逻辑。 - 事件未正确注册
如果ShareDisconnectGrantor事件没有在ShareBloc构造函数中通过on<ShareDisconnectGrantor>(_onDisconnectGrantor)绑定处理函数,事件被添加后不会执行任何逻辑,也不会发射新状态。
修复方案
- 针对重复状态被过滤的问题:
- 调整mock返回值,构造和初始状态有明确差异的
ConnectedUsersModel(比如移除对应id的授权方用户、修改列表长度),保证新状态和初始状态不相等——这也符合真实业务逻辑:断开授权方成功后,授权方列表必然会减少对应用户,不可能和原列表完全一致。 - 同步修改expect中的预期状态,和mock返回的业务结果保持一致,不要预期返回和初始状态完全相同的状态。
- 调整mock返回值,构造和初始状态有明确差异的
- 针对AuthenticationBloc干扰的问题:
- 提前在setUp阶段mock好AuthenticationBloc的初始状态和流行为,不要在blocTest的build方法中临时创建未配置完成的AuthenticationBloc实例。
- 如果测试场景不需要认证逻辑参与,mock AuthenticationBloc的状态流为空流,避免非预期状态修改ShareBloc的当前状态。
- 额外检查项:
- 确认
ShareDisconnectGrantor事件已经正确绑定到对应的处理函数。 - 确认
ConnectedUsersModel、ConnectedUserModel、ConnectModel都正确继承Equatable,props中列出所有参与相等判断的字段,避免值比较逻辑异常。 - 确认测试中使用的
dateTime是固定的测试常量,不要在测试执行中调用DateTime.now()生成新的时间值,导致状态比较失败。
- 确认
内容的提问来源于stack exchange,提问作者Nik
相关产品推荐
相关产品推荐

