Dart 3 Null Safety-compatible Unit Test with Mocks

Madacode
3 min readAug 2, 2024

--

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.

--

--

Madacode

Senior Software Engineer @ Pinhome with 5+ years experience in various technologies including Go and Flutter. All posts are of my own opinion.