Curiosity in Flutter is at an all-time excessive—and it’s lengthy overdue. Google’s open-source SDK is appropriate with Android, iOS, macOS, net, Home windows, and Linux. A single Flutter codebase helps all of them. And unit testing is instrumental in delivering a constant and dependable Flutter app, making certain in opposition to errors, flaws, and defects by preemptively enhancing the high quality of code earlier than it’s assembled.
On this tutorial, we share workflow optimizations for Flutter unit testing, exhibit a fundamental Flutter unit take a look at, then transfer on to extra advanced Flutter take a look at circumstances and libraries.
The Move of Unit Testing in Flutter
We implement unit testing in Flutter in a lot the identical approach that we do in different expertise stacks:
- Consider the code.
- Arrange information mocking.
- Outline the take a look at group(s).
- Outline take a look at operate signature(s) for every take a look at group.
- Write the checks.
To exhibit unit testing, I’ve ready a pattern Flutter challenge and encourage you to make use of and take a look at the code at your leisure. The challenge makes use of an exterior API to fetch and show an inventory of universities that we are able to filter by nation.
A number of notes about how Flutter works: The framework facilitates testing by autoloading the flutter_test
library when a challenge is created. The library permits Flutter to learn, run, and analyze unit checks. Flutter additionally autocreates the take a look at
folder during which to retailer checks. It’s essential to keep away from renaming and/or transferring the take a look at
folder, as this breaks its performance and, therefore, our skill to run checks. Additionally it is important to incorporate _test.dart
in our take a look at file names, as this suffix is how Flutter acknowledges take a look at recordsdata.
Check Listing Construction
To advertise unit testing in our challenge, we carried out MVVM with clear structure and dependency injection (DI), as evidenced within the names chosen for supply code subfolders. The mix of MVVM and DI rules ensures a separation of considerations:
- Every challenge class helps a single goal.
- Every operate inside a category fulfills solely its personal scope.
We’ll create an organized space for storing for the take a look at recordsdata we’ll write, a system the place teams of checks could have simply identifiable “houses.” In mild of Flutter’s requirement to find checks throughout the take a look at
folder, let’s mirror our supply code’s folder construction underneath take a look at
. Then, once we write a take a look at, we’ll retailer it within the acceptable subfolder: Simply as clear socks go within the sock drawer of your dresser and folded shirts go within the shirt drawer, unit checks of Mannequin
courses go in a folder named mannequin
, for instance.
Adopting this file system builds transparency into the challenge and affords the staff a simple approach to view which parts of our code have related checks.
We are actually able to put unit testing into motion.
A Easy Flutter Unit Check
We’ll start with the mannequin
courses (within the information
layer of the supply code) and can restrict our instance to incorporate only one mannequin
class, ApiUniversityModel
. This class boasts two features:
- Initialize our mannequin by mocking the JSON object with a
Map
. - Construct the
College
information mannequin.
To check every of the mannequin’s features, we’ll customise the common steps described beforehand:
- Consider the code.
- Arrange information mocking: We’ll outline the server response to our API name.
- Outline the take a look at teams: We’ll have two take a look at teams, one for every operate.
- Outline take a look at operate signatures for every take a look at group.
- Write the checks.
After evaluating our code, we’re prepared to perform our second goal: to arrange information mocking particular to the 2 features throughout the ApiUniversityModel
class.
To mock the primary operate (initializing our mannequin by mocking the JSON with a Map
), fromJson
, we’ll create two Map
objects to simulate the enter information for the operate. We’ll additionally create two equal ApiUniversityModel
objects to signify the anticipated results of the operate with the offered enter.
To mock the second operate (constructing the College
information mannequin), toDomain
, we’ll create two College
objects, that are the anticipated outcome after having run this operate within the beforehand instantiated ApiUniversityModel
objects:
void major() {
Map<String, dynamic> apiUniversityOneAsJson = {
"alpha_two_code": "US",
"domains": ["marywood.edu"],
"nation": "United States",
"state-province": null,
"web_pages": ["http://www.marywood.edu"],
"identify": "Marywood College"
};
ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel(
alphaCode: "US",
nation: "United States",
state: null,
identify: "Marywood College",
web sites: ["http://www.marywood.edu"],
domains: ["marywood.edu"],
);
College expectedUniversityOne = College(
alphaCode: "US",
nation: "United States",
state: "",
identify: "Marywood College",
web sites: ["http://www.marywood.edu"],
domains: ["marywood.edu"],
);
Map<String, dynamic> apiUniversityTwoAsJson = {
"alpha_two_code": "US",
"domains": ["lindenwood.edu"],
"nation": "United States",
"state-province":"MJ",
"web_pages": null,
"identify": "Lindenwood College"
};
ApiUniversityModel expectedApiUniversityTwo = ApiUniversityModel(
alphaCode: "US",
nation: "United States",
state:"MJ",
identify: "Lindenwood College",
web sites: null,
domains: ["lindenwood.edu"],
);
College expectedUniversityTwo = College(
alphaCode: "US",
nation: "United States",
state: "MJ",
identify: "Lindenwood College",
web sites: [],
domains: ["lindenwood.edu"],
);
}
Subsequent, for our third and fourth targets, we’ll add descriptive language to outline our take a look at teams and take a look at operate signatures:
void major() {
// Earlier declarations
group("Check ApiUniversityModel initialization from JSON", () {
take a look at('Check utilizing json one', () {});
take a look at('Check utilizing json two', () {});
});
group("Check ApiUniversityModel toDomain", () {
take a look at('Check toDomain utilizing json one', () {});
take a look at('Check toDomain utilizing json two', () {});
});
}
Now we have outlined the signatures of two checks to test the fromJson
operate, and two to test the toDomain
operate.
To meet our fifth goal and write the checks, let’s use the flutter_test library’s count on
methodology to match the features’ outcomes in opposition to our expectations:
void major() {
// Earlier declarations
group("Check ApiUniversityModel initialization from json", () {
take a look at('Check utilizing json one', () {
count on(ApiUniversityModel.fromJson(apiUniversityOneAsJson),
expectedApiUniversityOne);
});
take a look at('Check utilizing json two', () {
count on(ApiUniversityModel.fromJson(apiUniversityTwoAsJson),
expectedApiUniversityTwo);
});
});
group("Check ApiUniversityModel toDomain", () {
take a look at('Check toDomain utilizing json one', () {
count on(ApiUniversityModel.fromJson(apiUniversityOneAsJson).toDomain(),
expectedUniversityOne);
});
take a look at('Check toDomain utilizing json two', () {
count on(ApiUniversityModel.fromJson(apiUniversityTwoAsJson).toDomain(),
expectedUniversityTwo);
});
});
}
Having completed our 5 targets, we are able to now run the checks, both from the IDE or from the command line.
At a terminal, we are able to run all checks contained throughout the take a look at
folder by getting into the flutter take a look at
command, and see that our checks go.
Alternatively, we may run a single take a look at or take a look at group by getting into the flutter take a look at --plain-name "ReplaceWithName"
command, substituting the identify of our take a look at or take a look at group for ReplaceWithName
.
Unit Testing an Endpoint in Flutter
Having accomplished a easy take a look at with no dependencies, let’s discover a extra fascinating instance: We’ll take a look at the endpoint
class, whose scope encompasses:
- Executing an API name to the server.
- Remodeling the API JSON response into a unique format.
After having evaluated our code, we’ll use flutter_test library’s setUp
methodology to initialize the courses inside our take a look at group:
group("Check College Endpoint API calls", () {
setUp(() {
baseUrl = "https://take a look at.url";
dioClient = Dio(BaseOptions());
endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
});
}
To make community requests to APIs, I choose utilizing the retrofit library, which generates a lot of the needed code. To correctly take a look at the UniversityEndpoint
class, we’ll pressure the dio library—which Retrofit
makes use of to execute API calls—to return the specified outcome by mocking the Dio
class’s habits by way of a customized response adapter.
Customized Community Interceptor Mock
Mocking is feasible because of our having constructed the UniversityEndpoint
class by way of DI. (If the UniversityEndpoint
class had been to initialize a Dio
class by itself, there can be no approach for us to mock the category’s habits.)
With the intention to mock the Dio
class’s habits, we have to know the Dio
strategies used throughout the Retrofit
library—however we wouldn’t have direct entry to Dio
. Due to this fact, we’ll mock Dio
utilizing a customized community response interceptor:
class DioMockResponsesAdapter extends HttpClientAdapter {
ultimate MockAdapterInterceptor interceptor;
DioMockResponsesAdapter(this.interceptor);
@override
void shut({bool pressure = false}) {}
@override
Future<ResponseBody> fetch(RequestOptions choices,
Stream<Uint8List>? requestStream, Future? cancelFuture) {
if (choices.methodology == interceptor.sort.identify.toUpperCase() &&
choices.baseUrl == interceptor.uri &&
choices.queryParameters.hasSameElementsAs(interceptor.question) &&
choices.path == interceptor.path) {
return Future.worth(ResponseBody.fromString(
jsonEncode(interceptor.serializableResponse),
interceptor.responseCode,
headers: {
"content-type": ["application/json"]
},
));
}
return Future.worth(ResponseBody.fromString(
jsonEncode(
{"error": "Request would not match the mock interceptor particulars!"}),
-1,
statusMessage: "Request would not match the mock interceptor particulars!"));
}
}
enum RequestType { GET, POST, PUT, PATCH, DELETE }
class MockAdapterInterceptor {
ultimate RequestType sort;
ultimate String uri;
ultimate String path;
ultimate Map<String, dynamic> question;
ultimate Object serializableResponse;
ultimate int responseCode;
MockAdapterInterceptor(this.sort, this.uri, this.path, this.question,
this.serializableResponse, this.responseCode);
}
Now that we’ve created the interceptor to mock our community responses, we are able to outline our take a look at teams and take a look at operate signatures.
In our case, we’ve got just one operate to check (getUniversitiesByCountry
), so we’ll create only one take a look at group. We’ll take a look at our operate’s response to a few conditions:
- Is the
Dio
class’s operate really known as bygetUniversitiesByCountry
? - If our API request returns an error, what occurs?
- If our API request returns the anticipated outcome, what occurs?
Right here’s our take a look at group and take a look at operate signatures:
group("Check College Endpoint API calls", () {
take a look at('Check endpoint calls dio', () async {});
take a look at('Check endpoint returns error', () async {});
take a look at('Check endpoint calls and returns 2 legitimate universities', () async {});
});
We’re prepared to put in writing our checks. For every take a look at case, we’ll create an occasion of DioMockResponsesAdapter
with the corresponding configuration:
group("Check College Endpoint API calls", () {
setUp(() {
baseUrl = "https://take a look at.url";
dioClient = Dio(BaseOptions());
endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
});
take a look at('Check endpoint calls dio', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
200,
[],
);
var outcome = await endpoint.getUniversitiesByCountry("us");
count on(outcome, <ApiUniversityModel>[]);
});
take a look at('Check endpoint returns error', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
404,
{"error": "Not discovered!"},
);
Record<ApiUniversityModel>? response;
DioError? error;
attempt {
response = await endpoint.getUniversitiesByCountry("us");
} on DioError catch (dioError, _) {
error = dioError;
}
count on(response, null);
count on(error?.error, "Http standing error [404]");
});
take a look at('Check endpoint calls and returns 2 legitimate universities', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
200,
generateTwoValidUniversities(),
);
var outcome = await endpoint.getUniversitiesByCountry("us");
count on(outcome, expectedTwoValidUniversities());
});
});
Now that our endpoint testing is full, let’s take a look at our information supply class, UniversityRemoteDataSource
. Earlier, we noticed that the UniversityEndpoint
class is part of the constructor UniversityRemoteDataSource({UniversityEndpoint? universityEndpoint})
, which signifies that UniversityRemoteDataSource
makes use of the UniversityEndpoint
class to satisfy its scope, so that is the category we are going to mock.
Mocking With Mockito
In our earlier instance, we manually mocked our Dio
consumer’s request adapter utilizing a customized NetworkInterceptor
. Right here we’re mocking a complete class. Doing so manually—mocking a category and its features—can be time-consuming. Fortuitously, mock libraries are designed to deal with such conditions and might generate mock courses with minimal effort. Let’s use the mockito library, the trade customary library for mocking in Flutter.
To mock by way of Mockito
, we first add the annotation “@GenerateMocks([class_1,class_2,…])
” earlier than the take a look at’s code—simply above the void major() {}
operate. Within the annotation, we’ll embrace an inventory of sophistication names as a parameter (rather than class_1,class_2…
).
Subsequent, we run Flutter’s flutter pub run build_runner construct
command that generates the code for our mock courses in the identical listing because the take a look at. The resultant mock file’s identify can be a mix of the take a look at file identify plus .mocks.dart
, changing the take a look at’s .dart
suffix. The file’s content material will embrace mock courses whose names start with the prefix Mock
. For instance, UniversityEndpoint
turns into MockUniversityEndpoint
.
Now, we import university_remote_data_source_test.dart.mocks.dart
(our mock file) into university_remote_data_source_test.dart
(the take a look at file).
Then, within the setUp
operate, we’ll mock UniversityEndpoint
through the use of MockUniversityEndpoint
and initializing the UniversityRemoteDataSource
class:
import 'university_remote_data_source_test.mocks.dart';
@GenerateMocks([UniversityEndpoint])
void major() {
late UniversityEndpoint endpoint;
late UniversityRemoteDataSource dataSource;
group("Check operate calls", () {
setUp(() {
endpoint = MockUniversityEndpoint();
dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
});
}
We efficiently mocked UniversityEndpoint
after which initialized our UniversityRemoteDataSource
class. Now we’re able to outline our take a look at teams and take a look at operate signatures:
group("Check operate calls", () {
take a look at('Check dataSource calls getUniversitiesByCountry from endpoint', () {});
take a look at('Check dataSource maps getUniversitiesByCountry response to Stream', () {});
take a look at('Check dataSource maps getUniversitiesByCountry response to Stream with error', () {});
});
With this, our mocking, take a look at teams, and take a look at operate signatures are arrange. We’re prepared to put in writing the precise checks.
Our first take a look at checks whether or not the UniversityEndpoint
operate is named when the information supply initiates the fetching of nation info. We start by defining how every class will react when its features are known as. Since we mocked the UniversityEndpoint
class, that’s the category we’ll work with, utilizing the when( function_that_will_be_called ).then( what_will_be_returned )
code construction.
The features we’re testing are asynchronous (features that return a Future
object), so we’ll use the when(operate identify).thenanswer( (_) {modified operate outcome} )
code construction to change our outcomes.
To test whether or not the getUniversitiesByCountry
operate calls the getUniversitiesByCountry
operate throughout the UniversityEndpoint
class, we’ll use when(...).thenAnswer( (_) {...} )
to mock the getUniversitiesByCountry
operate throughout the UniversityEndpoint
class:
when(endpoint.getUniversitiesByCountry("take a look at"))
.thenAnswer((realInvocation) => Future.worth(<ApiUniversityModel>[]));
Now that we’ve mocked our response, we name the information supply operate and test—utilizing the confirm
operate—whether or not the UniversityEndpoint
operate was known as:
take a look at('Check dataSource calls getUniversitiesByCountry from endpoint', () {
when(endpoint.getUniversitiesByCountry("take a look at"))
.thenAnswer((realInvocation) => Future.worth(<ApiUniversityModel>[]));
dataSource.getUniversitiesByCountry("take a look at");
confirm(endpoint.getUniversitiesByCountry("take a look at"));
});
We are able to use the identical rules to put in writing further checks that test whether or not our operate accurately transforms our endpoint outcomes into the related streams of information:
import 'university_remote_data_source_test.mocks.dart';
@GenerateMocks([UniversityEndpoint])
void major() {
late UniversityEndpoint endpoint;
late UniversityRemoteDataSource dataSource;
group("Check operate calls", () {
setUp(() {
endpoint = MockUniversityEndpoint();
dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
});
take a look at('Check dataSource calls getUniversitiesByCountry from endpoint', () {
when(endpoint.getUniversitiesByCountry("take a look at"))
.thenAnswer((realInvocation) => Future.worth(<ApiUniversityModel>[]));
dataSource.getUniversitiesByCountry("take a look at");
confirm(endpoint.getUniversitiesByCountry("take a look at"));
});
take a look at('Check dataSource maps getUniversitiesByCountry response to Stream',
() {
when(endpoint.getUniversitiesByCountry("take a look at"))
.thenAnswer((realInvocation) => Future.worth(<ApiUniversityModel>[]));
count on(
dataSource.getUniversitiesByCountry("take a look at"),
emitsInOrder([
const AppResult<List<University>>.loading(),
const AppResult<List<University>>.data([])
]),
);
});
take a look at(
'Check dataSource maps getUniversitiesByCountry response to Stream with error',
() {
ApiError mockApiError = ApiError(
statusCode: 400,
message: "error",
errors: null,
);
when(endpoint.getUniversitiesByCountry("take a look at"))
.thenAnswer((realInvocation) => Future.error(mockApiError));
count on(
dataSource.getUniversitiesByCountry("take a look at"),
emitsInOrder([
const AppResult<List<University>>.loading(),
AppResult<List<University>>.apiError(mockApiError)
]),
);
});
});
}
Now we have executed a variety of Flutter unit checks and demonstrated completely different approaches to mocking. I invite you to proceed to make use of my pattern Flutter challenge to run further testing.
Flutter Unit Exams: Your Key to Superior UX
If you happen to already incorporate unit testing into your Flutter initiatives, this text could have launched some new choices you possibly can inject into your workflow. On this tutorial, we demonstrated how easy it could be to include unit testing into your subsequent Flutter challenge and the best way to deal with the challenges of extra nuanced take a look at eventualities. It’s possible you’ll by no means need to skip over unit checks in Flutter once more.
The editorial staff of the Toptal Engineering Weblog extends its gratitude to Matija Bečirević and Paul Hoskins for reviewing the code samples and different technical content material offered on this article.