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

Flutter单元测试传入Class类型参数触发MissingStubError的解决方法

问题原因

出现MissingStubError的核心原因有两个:

  1. GamesParams未实现值相等判断:Mockito默认通过对象的==运算符和hashCode匹配方法调用参数。由于GamesParams及其嵌套类(GamesParamsDates、GamesParamsOrder)没有重写这两个方法,即使字段值完全相同,两个实例也会被视为不同对象,导致Stub匹配失败。
  2. 动态生成的DateTime导致参数不匹配:GamesParams.getPage工厂方法使用DateTime.now()生成日期,测试中提前创建的gamesParams实例与仓库方法调用时生成的实例存在时间差,进一步导致参数不匹配。

解决方案

方案1:实现值相等判断+固定日期注入

步骤1:为参数类添加相等判断

重写==运算符和hashCode,确保值相等的实例被视为相同对象:

// GamesParams.dart
import 'package:flutter/foundation.dart';

class GamesParams extends AbstractParamsModel {
  final int page;
  final int pageSize;
  final List<int> platforms;
  final GamesParamsDates dates;
  final GamesParamsOrder ordering;

  GamesParams({
    required this.page,
    required this.pageSize,
    required this.platforms,
    required this.dates,
    required this.ordering,
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is GamesParams &&
          runtimeType == other.runtimeType &&
          page == other.page &&
          pageSize == other.pageSize &&
          listEquals(platforms, other.platforms) &&
          dates == other.dates &&
          ordering == other.ordering;

  @override
  int get hashCode =>
      Object.hash(page, pageSize, Object.hashAll(platforms), dates, ordering);

  // 修改工厂方法,支持传入自定义日期
  factory GamesParams.getPage(int page, [DateTime? currentDate]) {
    var date = currentDate ?? DateTime.now();
    var startDate = DateTime(date.year - 1);

    return GamesParams(
      page: page,
      pageSize: 20,
      platforms: [187],
      dates: GamesParamsDates.range(startDate: startDate, date: date),
      ordering: GamesParamsOrder(
        type: GamesParamsOrderType.released,
        sort: GamesParamsOrderSort.desc,
      ),
    );
  }

  // ... 其他原有方法 ...
}

// GamesParamsDates.dart
class GamesParamsDates {
  final DateTime startDate;
  final DateTime date;

  GamesParamsDates.range({required this.startDate, required this.date});

  String toValue() => "${startDate.toIso8601String()},${date.toIso8601String()}";

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is GamesParamsDates &&
          runtimeType == other.runtimeType &&
          startDate == other.startDate &&
          date == other.date;

  @override
  int get hashCode => Object.hash(startDate, date);
}

// GamesParamsOrder.dart
class GamesParamsOrder {
  final GamesParamsOrderType type;
  final GamesParamsOrderSort sort;

  GamesParamsOrder({required this.type, required this.sort});

  String toValue() => "${sort == GamesParamsOrderSort.desc ? '-' : ''}${type.toValue()}";

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is GamesParamsOrder &&
          runtimeType == other.runtimeType &&
          type == other.type &&
          sort == other.sort;

  @override
  int get hashCode => Object.hash(type, sort);
}

步骤2:修改仓库类支持日期注入

让仓库类接收日期提供函数,方便测试时传入固定日期:

// GamesRepositoryImp.dart
class GamesRepositoryImp implements AbstractGamesRepository {
  final GamesImpApi source;
  final DateTime Function() dateProvider;

  GamesRepositoryImp({
    required this.source,
    this.dateProvider = DateTime.now, // 默认使用当前时间
  });

  @override
  Future<Either<Failure, GamesEntity>> getGames(int page) async {
    try {
      final result = await source.getGames(
        params: GamesParams.getPage(page, dateProvider()), // 使用注入的日期
      );

      var dataMapped = GamesEntity(
        results: (result.results ?? []).map((e) => e.mapToEntity()).toList(),
        hasMore: result.next != null,
      );

      return Right(dataMapped);
    } on ApiException catch (e) {
      return Left(
        ServerFailure(
          e.response?.message ?? e.error?.message ?? 'Something went wrong',
        ),
      );
    } on Failure catch (e) {
      return Left(ServerFailure(e.errorMessage));
    }
  }
}

步骤3:更新测试代码使用固定日期

确保测试和仓库使用相同的参数实例:

// 测试代码
@GenerateMocks([GamesImpApi])
void main() {
  // 固定日期,避免时间差导致参数不匹配
  final fixedDate = DateTime(2024, 1, 1);
  int gamesEntityParams = 1;
  GamesParams gamesParams = GamesParams.getPage(gamesEntityParams, fixedDate);

  late MockGamesImpApi mockApi;
  late AbstractGamesRepository gamesRepositoryImp;

  setUp(() {
    mockApi = MockGamesImpApi();
    // 注入固定日期提供函数
    gamesRepositoryImp = GamesRepositoryImp(
      source: mockApi,
      dateProvider: () => fixedDate,
    );
  });

  group('Test Games Repository Implementation', () {
    test('Get All Games - Failed Case, Empty Or Null Api response', () async {
      when(mockApi.getGames(params: gamesParams))
          .thenAnswer((_) async => actualGamesFailedOrEmptyListData);

      final result = await gamesRepositoryImp.getGames(gamesEntityParams);
      expect(result, expectedGamesEmptyListData);
    });

    test('Get All Games - Success Case', () async {
      when(mockApi.getGames(params: gamesParams))
          .thenAnswer((_) async => actualGamesListData);

      final result = await gamesRepositoryImp.getGames(gamesEntityParams);
      expect(result, expectedGamesListData);
    });
  });
}

方案2:使用自定义Matcher快速匹配(无需修改类结构)

如果不想修改业务代码,可使用Mockito的argThat结合自定义Matcher,只匹配关键字段:

// 测试代码
@GenerateMocks([GamesImpApi])
void main() {
  int gamesEntityParams = 1;

  late MockGamesImpApi mockApi;
  late AbstractGamesRepository gamesRepositoryImp;

  setUp(() {
    mockApi = MockGamesImpApi();
    gamesRepositoryImp = GamesRepositoryImp(source: mockApi);
  });

  // 自定义Matcher:检查GamesParams的page字段是否匹配
  Matcher matchesPage(int expectedPage) => predicate(
    (GamesParams params) => params.page == expectedPage,
    'page equals $expectedPage',
  );

  group('Test Games Repository Implementation', () {
    test('Get All Games - Failed Case, Empty Or Null Api response', () async {
      when(mockApi.getGames(params: argThat(matchesPage(gamesEntityParams))))
          .thenAnswer((_) async => actualGamesFailedOrEmptyListData);

      final result = await gamesRepositoryImp.getGames(gamesEntityParams);
      expect(result, expectedGamesEmptyListData);
    });

    test('Get All Games - Success Case', () async {
      when(mockApi.getGames(params: argThat(matchesPage(gamesEntityParams))))
          .thenAnswer((_) async => actualGamesListData);

      final result = await gamesRepositoryImp.getGames(gamesEntityParams);
      expect(result, expectedGamesListData);
    });
  });
}

这种方式无需修改业务代码,但匹配精度较低(忽略其他参数字段),适合快速解决测试问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:44:50