How to create a REST API client and its integration tests in Flutter

Jorge Sánchez (xurxodev)
Mar 6 · 7 min read
API Integration testing in Flutter

This article is an English translation from my original article in my blog xurxodev.com.


Most of the mobile applications are based on communication with an API Rest.

The integration with an external service is fundamental in mobile development. To have an optimal integration, it is necessary to ensure it by integration tests.

One year ago I wrote about this topic though it was applied to Kotlin multiplatform.

I have been working with Flutter for some time and I felt it was high time to write about this topic applied to Flutter.

In this article, we will see how to create a REST API client and its integration tests in Flutter necessary to verify that our integration works.

API REST

The client that we are going to create communicates with the following web service:

http://jsonplaceholder.typicode.com/

This REST API manages tasks, so you will be able to:

get all the existing tasks, get a task by identifier, add a new task, update a task or delete an existing task.

Creating the project

The first thing to do is to create the project. You can use Android Studio or Visual Studio Code.

Details on how to install Flutter and how to configure it to work is out of the scope of this post.

Here you have Flutter’s official documentation: Flutter get-started.

Setting up the continuous integration

The best time to set up CI for a project is the beginning, so this would be the next step.

I’ve used Codemagic. It is the continuous integration tool that I like the most for Flutter due to its simplicity with bare setup.

It’s only focused on Flutter; that’s the reason why it is very easy.

It is possible to make the set up through its website or through a YAML file.

Here I am leaving Codemagic documentation for the set up: Codemagic get-started.

Creating the client

In Flutter the libraries are named as packages and we are going to need:

  • Http, It’s a package developed by the Flutter team to make requests to a remote server. It would be like Retrofit for Android or Alamofire and AFNetworking for iOS.

Our client will be composed of an HTTP client, to which we will make the requests and receive the base address in the constructor. We will see why later.

We need to handle the errors that the API returns. On this occasion, we will generate an exception for each error.

In a future article, we will see how to do it in a more functional style by using an Either type as a return value. This strategy is equal to the one we used in the version of Kotlin multiplatform that I wrote last year.

class TodoApiClient {
...

http.Response returnResponseOrThrowException(http.Response response) {
if (response.statusCode == 404) {
throw ItemNotFoundException();
} else if (response.statusCode > 400) {
throw UnKnowApiException(response.statusCode);
} else {
return response;
}
}

...
}

In this class, we need some infrastructure. We will have a private method for each verb where we will make the request to the corresponding endpoint, we will assign headers and body if applicable.

class TodoApiClient {
final String _baseAddress;
TodoApiClient(this._baseAddress);...

Future<http.Response> _get(String endpoint) async {
try {
final response = await http.get(
'$_baseAddress$endpoint',
headers: {
HttpHeaders.acceptHeader: 'application/json',
},
);
return returnResponseOrThrowException(response);
} on IOException catch (e) {
print(e.toString());
throw NetworkException();
}
}
Future<http.Response> _post(Task task) async {
try {
final response = await http.post(
'$_baseAddress/todos',
body: json.encode(task.toJson()) ,
headers: {
HttpHeaders.acceptHeader: 'application/json',
HttpHeaders.contentTypeHeader: 'application/json',
},
);
return returnResponseOrThrowException(response);
} on IOException {
throw NetworkException();
}
}
Future<http.Response> _put(Task task) async {
try {
final response = await http.put(
'$_baseAddress/todos/${task.id}',
body: json.encode(task.toJson()) ,
headers: {
HttpHeaders.acceptHeader: 'application/json',
HttpHeaders.contentTypeHeader: 'application/json',
},
);
return returnResponseOrThrowException(response);
} on IOException {
throw NetworkException();
}
}
Future<http.Response> _delete(String id) async {
try {
final response = await http.delete(
'$_baseAddress/todos/$id',
headers: {
HttpHeaders.acceptHeader: 'application/json',
},
);
return returnResponseOrThrowException(response);
} on IOException {
throw NetworkException();
}
}

...
}

We will have a method for each of the actions in the remote service.

class TodoApiClient {...

Future<List<Task>> getAllTasks() async {
final response = await _get('/todos');
final decodedTasks = json.decode(response.body) as List;return decodedTasks.map((jsonTask) => Task.fromJson(jsonTask)).toList();
}
Future<Task> getTasksById(String id) async {
final response = await _get('/todos/$id');
return Task.fromJson(json.decode(response.body));
}
Future<Task> addTask(Task task) async {
final response = await _post(task);
return Task.fromJson(json.decode(response.body));
}
Future<Task> updateTask(Task task) async {
final response = await _put(task);
return Task.fromJson(json.decode(response.body));
}
Future<void> deleteTaskById(String id) async {
await _delete(id);
}

...
}

About serialization, flutter is disabled and there are two options:

  • Manual serialization, suitable for small projects like the one I created for this example.
  • Serialization through code generation, for medium or large projects.

Here I am leaving a link to the Flutter documentation about serialization so you can expand on this point: Flutter serialization.

Integration tests

To test the integration of our client with the Rest API, we need to verify the following:

  • The requests are sent correctly to the API: endpoint, verb, headers, body if applicable.
  • The server responses are parsed correctly.
  • The server error responses are handled correctly.

To perform these checks we have to simulate server responses and to be able to access in some way to the HTTP requests that we have sent.

There are two possible strategies at this point:

  • Use an HTTP client mock, the flutter HTTP package provides a MockClient class to do this. This client should be passed as an argument to our TodoApiClient. But this mock client is a bit limited for more complex scenarios, where a call to TodoApiClient involves several calls to the server. (for example, to renew the token). For simple examples, this option is valid.
  • Use a mock server, this is the option I usually use. The idea is to start an embedded HTTP server. We can configure our responses and validate the sent requests. The mock server URL must be passed as an argument in the TodoApiClient constructor.

We need the next package:

Let’s see some tests that we can create.

The first tests we could try are to verify the todos endpoint, for example:

  • To verify the response is parsed correctly.
  • To verify that accept header is sent.
  • To verify in case of an error, it is processed properly.

What infrastructure do we need? We need to have a way to enqueue responses on the mock server and verify the sent request. We will pass the mock server URL as an argument in the TodoApiClient constructor.

We need a JSON that represents the server’s response:

// get_tasks_response.json
[
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
{
"userId": 1,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"completed": false
},
{
"userId": 1,
"id": 3,
"title": "fugiat veniam minus",
"completed": false
}
]

Now we need to be able to read this file and enqueue it as the next mock server response, then to get the last request against the mock server to validate it.

We can create a base class for our tests or create a specific class to realize this work. I like to favour the composition over the inheritance also in the tests.

class MockApi {
MockWebServer _server;
Future<void> start() async {
_server = MockWebServer();
await _server.start();
}
void shutdown() {
_server.shutdown();
}
String get baseAddress => _server.url.substring(0, _server.url.length - 1);Future<void> enqueueMockResponse(
{String fileName = '',
int httpCode = 200,
Map<String, String> headers}) async {
final content = await _getContentFromFile(fileName: fileName);
_server.enqueue(body: content, httpCode: httpCode, headers: headers);
}
void expectRequestSentTo(String endpoint) {
final StoredRequest storedRequest = _server.takeRequest();
expect(storedRequest.uri.path, endpoint);
}
void expectRequestContainsHeader([String key, String expectedValue, int requestIndex = 0]) {
final StoredRequest storedRequest = _getRecordedRequestAtIndex(requestIndex);
final value = storedRequest.headers[key];
expect(value, contains(expectedValue));
}
Future<void> expectRequestContainsBody(String fileName) async {
final expectedBody = await _getContentFromFile(fileName: fileName);
final StoredRequest storedRequest = _getRecordedRequestAtIndex(0);
expect(storedRequest.body, expectedBody);
}
...
}

This class is a wrapper on the mock server.

This class add specific infrastructure to our needs:

  • enqueue a response reading the contents from a file
  • performing validations about the endpoint, headers and body in the request made to the mock server.

And finally, we write our tests:

TodoApiClient _todoApiClient;
MockApi mockApi = MockApi();
String anyTaskId = '1';
Task anyTask =
Task(id: 1, userId: 1, title: 'Finish this kata', completed: false);
void main() {
setUp(() async {
await mockApi.start();
_todoApiClient = TodoApiClient(mockApi.baseAddress);
});
tearDown(() {
mockApi.shutdown();
});
group('TodoApiClient', () {group('GetAllTasks should', () {test('sends get request to the correct endpoint', () async {
await mockApi.enqueueMockResponse(fileName: getTasksResponse);
await _todoApiClient.getAllTasks();mockApi.expectRequestSentTo('/todos');
});
test('sends accept header', () async {
await mockApi.enqueueMockResponse(fileName: getTasksResponse);
await _todoApiClient.getAllTasks();mockApi.expectRequestContainsHeader('accept', 'application/json');
});
test('parse current news properly getting all current news', () async {
await mockApi.enqueueMockResponse(fileName: getTasksResponse);
final tasks = await _todoApiClient.getAllTasks();expectTasksContainsExpectedValues(tasks[0]);
});
test(
'throws UnknownErrorException if there is not handled error getting news',
() async {
await mockApi.enqueueMockResponse(httpCode: 454);
expect(() => _todoApiClient.getAllTasks(),
throwsA(isInstanceOf<UnKnowApiException>()));
});
});

...

});
}
void expectTasksContainsExpectedValues(Task task) {
expect(task, isNotNull);
expect(task.userId, 1);
expect(task.id, 1);
expect(task.title, 'delectus aut autem');
expect(task.completed, false);
}

As you can see, to write test on Flutter is very similar to Javascript with Jest for example.

Kata and source code

You can find the source code here.

The master branch contains all the kata solved by me.

The best way to learn is by practising so I recommend using the kata branch and doing the exercise by yourself.

Related Course

Testing Course

Conclusions

In this post, we have seen how to create a REST API client on Flutter. Also how to create tests that allow us to validate the integration with the remote service.

Thanks for reading this article! If you liked it, press the 👏🏼 button so other people can read it too :)

Flutter Community

Articles and Stories from the Flutter Community

Jorge Sánchez (xurxodev)

Written by

Freelance developer xurxodev.com and co-founder kirei.io

Flutter Community

Articles and Stories from the Flutter Community

More From Medium

More from Flutter Community

More from Flutter Community

More from Flutter Community

More from Flutter Community

DateField in Flutter Made Easy

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade