I'm trying to do a unit test on a repository class. In the stub when
, I called a method that requires a parameter of type GamesParams
class. This produces a MissingStubError
error. However, if I hardcoded the result of the mapped parameter (see toApiParams
in The Parameter below) as a String
in the client.get
request method, the test succeeds. How do I fix this issue?
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));
}
}
}
You probably want to register your stub to match the GamesParam
's page
member specifically:
when(
mockApi.getGames(
params: argThat(
named: 'params',
isA<GamesParams>().having(
(params) => params.page,
'page',
gamesEntityParams,
),
),
),
).thenAnswer(...);
If you register a stub with Mockito but get a MissingStubError
, that means that the mocked function is called with arguments that do not match the arguments the stub expected.
If you don't explicitly specify an argument matcher, Mockito will assume that you want to match based on object equality. If your arguments don't override operator ==
, then by default equality means object identity (i.e., objects are equal only if they are the exact same instance). As I noted in comments, your stub expects a GamesParam
argument, but GamesParam
does not override operator ==
, so it will match only mockApi.getGames
is called with the same GamesParam
object, but GamesRepositoryImp
calls source.getGames(params: GamesParams.getPage(page))
, nd the GamesParams.getPage
factory constructor always returns a new GamesParam
object.
Adding operator ==
to GamesParam
didn't help probably because your implementation (which you haven't shown) was incorrect. I'm guessing that you did something like:
@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;
}
But that would have a few problems:
The dates
and ordering
members are GamesParamsDates
and GamesParamsOrder
objects respectively, so either those classes also would need operator ==
implementations or GamesParam
's operator ==
would need to know how to compare their internals.
platforms
is a List<int>
, so either you need to perform a deep List
equality or GamesParam
's operator ==
needs to compare the elements manually.
The GamesParams.getPage
factory constructor initializes dates
using DateTime.now()
. Therefore if dates
is used to determine equality, two calls to GamesParams.getPage
can never reliably return different objects that compare equal.
You could fix this by either:
GamesParams
's operator ==
to ignore dates.package:clock
to allow tests to specify what "now" should be:
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.
},
),
});
}
and by replacing DateTime.now()
with clock.now()
everywhere.Now, all of that likely is way more work than you want, so you're probably better off registering your stub with a different Matcher
that doesn't want strict equality.