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

Flutter blocTest单元测试状态相等性对比失败问题排查

问题根因

测试失败是两个问题叠加导致的:

  1. ViewData 未实现值相等判断:HomeState 虽然继承了Equatable,但作为属性的ViewData既没有继承Equatable,也没有重写==和hashCode,Dart默认的对象相等判断只校验两个实例是否是同一个内存地址的对象。测试里expect写的ViewData.loading()、ViewData.loaded()是测试代码里新创建的实例,和Bloc里创建的ViewData实例内存地址不同,哪怕属性完全一致,相等判断也会返回false,这就是报错里Expected和Actual打印内容完全相同但判断不匹配的核心原因。
  2. 预期状态和Bloc实际发射的状态不匹配:看_onSuccessRepositories逻辑,发射完loaded状态后,会等待500ms再发射一个isShowTitle: true的新状态,你当前测试的expect列表只写了loading、loaded两个状态,少了最后这个状态,就算解决了相等判断问题,测试依然会因为状态数量不对报错。
修复方案

1. 给ViewData补全值相等逻辑

修改ViewData代码,继承Equatable并把所有属性加入props列表,参与值比较:

import 'package:equatable/equatable.dart';

// 注意:ViewState是枚举本身支持值比较,如果你自定义的Failure、RepositoriesEntity类也需要做相等判断,同样要继承Equatable补全props
class ViewData<T> extends Equatable {
  final ViewState status;
  final T? data;
  final String message;
  final Failure? failure;

  const ViewData._({
    required this.status,
    this.data,
    this.message = "",
    this.failure,
  });

  factory ViewData.loaded({T? data}) {
    return ViewData._(status: ViewState.loaded, data: data);
  }

  factory ViewData.error({required Failure? failure}) {
    return ViewData._(status: ViewState.error, failure: failure);
  }

  factory ViewData.loading({required String message}) {
    return ViewData._(status: ViewState.loading, message: message);
  }

  factory ViewData.initial() {
    return ViewData._(status: ViewState.initial);
  }

  factory ViewData.empty({required String message}) {
    return ViewData._(status: ViewState.empty, message: message);
  }

  @override
  List<Object?> get props => [status, data, message, failure];
}

2. 修正测试用例

补全Bloc实际会发射的第三个状态,同时加wait参数等待Bloc内的延迟异步任务执行完成,确保能捕获到最后一个状态:

blocTest(
  'emits [loading, loaded, showTitle=true] when GetRepositories is added success',
  build: () {
    when(
      () => useCase.getRepositories(),
    ).thenAnswer((_) async => Right(repositoriesDummyEntityFromRemote));
    return homeBloc;
  },
  act: (HomeBloc bloc) => bloc.add(GetRepositories()),
  // 等待内部500ms延迟执行完,留100ms冗余
  wait: const Duration(milliseconds: 600),
  expect: () => [
    HomeState(
      statusRepositories: ViewData.loading(message: 'Loading'),
      isShowTitle: false,
    ),
    HomeState(
      statusRepositories: ViewData.loaded(data: repositoriesDummyEntityFromRemote),
      isShowTitle: false,
    ),
    // 补全延迟发射的显示标题状态
    HomeState(
      statusRepositories: ViewData.loaded(data: repositoriesDummyEntityFromRemote),
      isShowTitle: true,
    ),
  ],
);

可选替代方案(无需修改ViewData代码)

如果不想改动业务层的ViewData实现,可以按照报错提示,使用类型匹配器逐个校验状态属性,不直接比较实例:

expect: () => [
  isA<HomeState>()
    .having((state) => state.statusRepositories.status, 'request status', ViewState.loading)
    .having((state) => state.isShowTitle, 'title visibility', false),
  isA<HomeState>()
    .having((state) => state.statusRepositories.status, 'request status', ViewState.loaded)
    .having((state) => state.statusRepositories.data, 'response data', repositoriesDummyEntityFromRemote)
    .having((state) => state.isShowTitle, 'title visibility', false),
  isA<HomeState>()
    .having((state) => state.statusRepositories.status, 'request status', ViewState.loaded)
    .having((state) => state.isShowTitle, 'title visibility', true),
],

这种方式不需要修改业务类,但长期维护成本更高,项目里所有用于状态传递的数据类、状态类统一继承Equatable是更稳妥的做法。

内容的提问来源于stack exchange,提问作者R Rifa Fauzi Komara

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:12:17