我正在尝试对存储库类进行单元测试。在存根
when
中,我调用了一个需要 GamesParams
类类型参数的方法。这会产生 MissingStubError
错误。但是,如果我将映射参数 的结果(请参阅下面的 参数中的
toApiParams
) 硬编码为 String
请求方法中的 client.get
,则测试会成功。我该如何解决这个问题?
Expected: Right<Failure, GamesEntity>:<Right(GamesEntity([], false))>
Actual: MissingStubError:<MissingStubError: 'getGames'
No stub was found which matches the arguments of this method call:
getGames({params: Instance of 'GamesParams'})
import 'games_repository_imp_test.mocks.dart';
import 'mock_data/actual_games_data.dart';
import 'mock_data/expected_games_data.dart';
@GenerateMocks([GamesImpApi])
void main() {
// Api Params
int gamesEntityParams = 1;
GamesParams gamesParams = GamesParams.getPage(gamesEntityParams);
// Mocked GamesImpApi class
late MockGamesImpApi mockApi;
// Our Repository class that we need to test it.
// The dependency for this class will get from the mocked GamesImpApi class
// not from real GamesImpApi class
late AbstractGamesRepository gamesRepositoryImp;
setUp(() {
mockApi = MockGamesImpApi();
gamesRepositoryImp = GamesRepositoryImp(source: mockApi);
});
group('Test Games Repository Implementation', () {
test('Get All Games - Failed Case, Empty Or Null Api response', () async {
when(mockApi.getGames(params: gamesParams))
.thenAnswer((realInvocation) async {
return actualGamesFailedOrEmptyListData;
});
Object result;
try {
result = await gamesRepositoryImp.getGames(gamesEntityParams);
} catch (e) {
result = e;
}
expect(result, expectedGamesEmptyListData);
});
test('Get All Games - Success Case', () async {
when(mockApi.getGames(params: gamesParams))
.thenAnswer((realInvocation) async {
return actualGamesListData;
});
Object result;
try {
result = await gamesRepositoryImp.getGames(gamesEntityParams);
} catch (e) {
result = e;
}
expect(result, expectedGamesListData);
});
});
}
class GamesImpApi extends AbstractGamesApi {
final Dio client;
GamesImpApi(this.client);
@override
Future<GamesResponseModel<GameModel>> getGames({
required GamesParams params,
}) {
return clientExecutor<GamesResponseModel<GameModel>>(
execute: () async {
final res = await client.get(
NetworkUtils.getApiPath(
GamesImpApiConst.games,
params: params,
),
);
return GamesResponseModel.fromJson(
res.data,
(json) => GameModel.fromJson(json as Map<String, dynamic>),
);
},
);
}
}
class GamesImpApiConst {
GamesImpApiConst._();
static const games = '/games';
}
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
String toApiParams({bool fromStart = false}) {
return '${fromStart ? '?' : '&'}'
'page=$page'
'&pageSize=$pageSize'
'&platforms=${platforms.join(',')}'
'&dates=${dates.toValue()}'
'&ordering=${ordering.toValue()}';
}
/// Parameter to set page only while other values are set with the defaults.
factory GamesParams.getPage(int page) {
var date = 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,
),
);
}
}
class NetworkUtils {
static String getApiKeyParam() => '?key=${NetworkConst.apiKey}';
static String getApiPath(
String path, {
AbstractParamsModel? params,
}) {
return '${NetworkConst.apiUrl}'
'$path'
'${getApiKeyParam()}'
'${params != null ? params.toApiParams() : ''}';
}
}
class GamesRepositoryImp implements AbstractGamesRepository {
final GamesImpApi source;
GamesRepositoryImp({
required this.source,
});
@override
Future<Either<Failure, GamesEntity>> getGames(int page) async {
try {
final result = await source.getGames(
params: GamesParams.getPage(page),
);
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));
}
}
}
您可能想要注册您的存根以专门匹配
GamesParam
的 page
成员:
when(
mockApi.getGames(
params: argThat(
isA<GamesParams>().having(
(params) => params.page,
'page',
gamesEntityParams,
),
),
),
).thenAnswer(...);
如果您使用 Mockito 注册了一个存根,但得到了
MissingStubError
,这意味着调用模拟函数的参数与存根期望的参数不匹配。
如果您没有显式指定参数匹配器,Mockito 将假定您希望基于对象相等性进行匹配。如果您的参数不覆盖
operator ==
,则默认情况下相等意味着对象同一性(即,仅当对象是完全相同的实例时,对象才相等)。正如我在评论中指出的,您的存根需要一个 GamesParam
参数,但 GamesParam
不会覆盖 operator ==
,因此它将仅匹配使用相同 mockApi.getGames
对象调用 GamesParam
,但 GamesRepositoryImp
调用 source.getGames(params: GamesParams.getPage(page))
,GamesParams.getPage
工厂构造函数总是返回一个新的GamesParam
对象。
将
operator ==
添加到 GamesParam
没有帮助,可能是因为您的实现(您尚未显示)不正确。我猜你做了类似的事情:
@override
bool operator ==(Object other) {
if (other is! GamesParam) {
return false;
}
return page == other.page &&
pageSize == other.pageSize &&
platforms == other.platforms &&
dates == other.dates &&
ordering == other.ordering;
}
但这会带来一些问题:
dates
和 ordering
成员分别是 GamesParamsDates
和 GamesParamsOrder
对象,因此这些类也需要 operator ==
实现,或者 GamesParam
的 operator ==
需要知道如何比较它们的内部结构。
platforms
是一个 List<int>
,因此您需要 执行深度 List
相等 或 GamesParam
的 operator ==
需要手动比较元素。
GamesParams.getPage
工厂构造函数使用dates
初始化DateTime.now()
。因此,如果使用 dates
来确定相等性,则对 GamesParams.getPage
的两次调用永远无法可靠地返回比较相等的不同对象。
您可以通过以下任一方式解决此问题:
GamesParams
的 operator ==
以忽略日期。package:clock
允许测试指定“现在”应该是什么:
void main() {
test('...', () {
withClock<void>(
Clock.fixed(DateTime.now()),
() {
// This depends on the clock so must be executed within the
// `withClock` callback.
GamesParams gamesParams = GamesParams.getPage(gamesEntityParams);
// Other test code goes here.
},
),
});
}
并在各处将 DateTime.now()
替换为 clock.now()
。现在,所有这些工作可能比您想要的要多得多,因此您最好使用不需要严格平等的不同
Matcher
来注册您的存根。