As I tried to upgrade my template app repo, https://github.com/aramadsanar/flutter_retrofit_example with my friend to use Flutter 3.22 and Dart 3 (as it is mandatory with given Flutter version and has mandatory Null Safety), I found some issues integrating with my existing mocktail-based example unit tests.
One of them is that I cannot use any
to express that I did not care what is being passed to the function being called and just return the desired value. Another one is that to use anyNamed()
, I need to turn the function parameter into nullable, which is not desired as it is clearly required.
While the one mentioned is kind of a pet peeve, yet here comes another one: mocking HTTP APIs, mocking HTTP APIs in Dart 3 unit test through the tried-and-true ways of
class MockClassName extends Mock implements ClassName {}
no longer works. Attempting to do so will result in error:
type 'Null' is not a subtype of type 'Future<HttpResponse<Post>>'
test/core/view_models/views/home_view_model_test.dart 12:7 _MockPostApi.getPostByID
package:retrofit_dio_example/core/viewmodels/home_view_model.dart 35:56 HomeViewModel.fetchSinglePost
package:retrofit_dio_example/core/viewmodels/home_view_model.dart 19:11 HomeViewModel.initModel
With these breaking changes out in the way of our unit tests, then how do I solve it and even retrofit our existing ones?
Worry not, turns out there is a package which enables us to do all those stuff we love in unit tests with very minimal changes 💆, enter mockito.
First, let’s install mockito in our pubspec.yaml
mockito: ^5.4.4
then replace our instance of mocktail imports to mockito and import mockito annotations;
// remove this
import 'mocktail/mocktail.dart'
// replace with these
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
with the necessary replacements in place, we then can move the class mocking process into @GenerateMocks
annotation placed before main function declaration;
@GenerateMocks(<Type>[PostApi])
import 'home_view_model_test.mocks.dart';
after declaring which classes to generate mocks of, run the usual fvm flutter pub run build_runner build --delete-conflicting-outputs
to let the mocks be generated by mockito package.
With all these preparations in place, we may notice some of syntax errors in the unit tests, such as:
When declarations for async function calls no longer accepts it in function wrapped function, eg.
// no longer accepted
when(
() => postApi.getPostByID(
id: anyNamed('id'),
),
).thenAnswer(
(_) async => successFetchSinglePostResponse,
);
// mockito accepts this
when(
postApi.getPostByID(
id: anyNamed('id'),
),
).thenAnswer(
(_) async => successFetchSinglePostResponse,
);
and another one, is that any(named: 'name')
are no longer accepted, use anyNamed('name')
instead
// no longer accepted
when(
postApi.getPostByID(
id: any(named: 'id'),
),
).thenAnswer(
(_) async => successFetchSinglePostResponse,
);
// mockito accepts this
when(
postApi.getPostByID(
id: anyNamed('id'),
),
).thenAnswer(
(_) async => successFetchSinglePostResponse,
);
and here is the example unit test which conforms to the latest breaking changes of Dart 3 and its mandatory Null Safety
import 'dart:io';
import 'package:dio/dio.dart' as dio;
import 'package:flutter_test/flutter_test.dart';
import 'package:retrofit_dio_example/core/api/post_api.dart';
import 'package:retrofit_dio_example/core/models/post.dart';
import 'package:retrofit_dio_example/core/viewmodels/home_view_model.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:retrofit/dio.dart';
@GenerateMocks(<Type>[PostApi])
import 'home_view_model_test.mocks.dart';
void main() {
final MockPostApi postApi = MockPostApi();
final HttpResponse<List<Post>> successFetchPostResponse =
HttpResponse<List<Post>>(
<Post>[
Post(id: 1),
Post(id: 2),
],
dio.Response<dynamic>(
statusCode: HttpStatus.ok,
requestOptions: dio.RequestOptions(path: ''),
),
);
final HttpResponse<List<Post>?> failFetchPostResponse =
HttpResponse<List<Post>?>(
null,
dio.Response<dynamic>(
statusCode: HttpStatus.internalServerError,
requestOptions: dio.RequestOptions(path: ''),
),
);
final HttpResponse<Post> successFetchSinglePostResponse = HttpResponse<Post>(
Post(id: 1),
dio.Response<dynamic>(
statusCode: HttpStatus.ok,
requestOptions: dio.RequestOptions(path: ''),
),
);
late HomeViewModel model;
group('init model HomeViewModel test', () {
setUp(() {
when(
postApi.getPostByID(
id: anyNamed('id'),
),
).thenAnswer(
(_) async => successFetchSinglePostResponse,
);
model = HomeViewModel(
postApi: postApi,
);
});
test('success init HomeViewModel', () async {
when(postApi.getPosts()).thenAnswer(
(_) async => successFetchPostResponse,
);
await model.initModel();
expect(model.posts.length, 2);
expect(model.posts[0].id, 1);
expect(model.posts[1].id, 2);
verify(
postApi.getPostByID(
id: anyNamed('id'),
),
).called(1);
});
test('fail init HomeViewModel', () async {
when(postApi.getPosts()).thenAnswer((_) async => failFetchPostResponse);
await model.initModel();
expect(model.posts.length, 0);
});
});
}
That’s all for today and see you later on another post.