You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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];
}
问题根因

按出现概率从高到低排序:

  1. Bloc默认过滤重复状态
    Bloc 核心逻辑默认不会发射与当前状态通过==判定完全相等的新状态。当前测试中,mock的disconnectFromGrantor返回的用户列表和seed设置的初始状态里的用户列表完全一致,copyWith生成的新ShareLoadSuccess实例和初始状态值完全相等,被框架直接过滤,因此实际收集到的状态序列为空。
    只要ConnectedUsersModel、ConnectedUserModel、ConnectModel都正确继承Equatable并在props中列出所有比较字段,新旧状态就会被判定为相等,不会触发发射。
  2. AuthenticationBloc状态覆盖初始seed状态
    build阶段手动实例化的AuthenticationBloc未做完整mock,如果ShareBloc构造函数中订阅了AuthenticationBloc的状态流,AuthenticationBloc初始化时发出的非预期状态(比如未认证状态、初始状态)会覆盖seed设置的ShareLoadSuccess状态,导致事件处理函数中if (state is ShareLoadSuccess)判断不成立,直接跳过emit逻辑。
  3. 事件未正确注册
    如果ShareDisconnectGrantor事件没有在ShareBloc构造函数中通过on<ShareDisconnectGrantor>(_onDisconnectGrantor)绑定处理函数,事件被添加后不会执行任何逻辑,也不会发射新状态。
修复方案
  • 针对重复状态被过滤的问题:
    • 调整mock返回值,构造和初始状态有明确差异的ConnectedUsersModel(比如移除对应id的授权方用户、修改列表长度),保证新状态和初始状态不相等——这也符合真实业务逻辑:断开授权方成功后,授权方列表必然会减少对应用户,不可能和原列表完全一致。
    • 同步修改expect中的预期状态,和mock返回的业务结果保持一致,不要预期返回和初始状态完全相同的状态。
  • 针对AuthenticationBloc干扰的问题:
    • 提前在setUp阶段mock好AuthenticationBloc的初始状态和流行为,不要在blocTest的build方法中临时创建未配置完成的AuthenticationBloc实例。
    • 如果测试场景不需要认证逻辑参与,mock AuthenticationBloc的状态流为空流,避免非预期状态修改ShareBloc的当前状态。
  • 额外检查项:
    • 确认ShareDisconnectGrantor事件已经正确绑定到对应的处理函数。
    • 确认ConnectedUsersModel、ConnectedUserModel、ConnectModel都正确继承Equatable,props中列出所有参与相等判断的字段,避免值比较逻辑异常。
    • 确认测试中使用的dateTime是固定的测试常量,不要在测试执行中调用DateTime.now()生成新的时间值,导致状态比较失败。

内容的提问来源于stack exchange,提问作者Nik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 09:06:52