Flutter单元测试传入Class类型参数触发MissingStubError的解决方法
问题原因
出现MissingStubError的核心原因有两个:
GamesParams未实现值相等判断:Mockito默认通过对象的==运算符和hashCode匹配方法调用参数。由于GamesParams及其嵌套类(GamesParamsDates、GamesParamsOrder)没有重写这两个方法,即使字段值完全相同,两个实例也会被视为不同对象,导致Stub匹配失败。- 动态生成的
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
相关产品推荐
相关产品推荐

