Flutter Retrofit, implementation in Clean Architecture with unit tests
Introduction
We will see how to implement Retrofit in a clean architecture way. The implementation will be set in the data source part of the feature. To understand what is a feature or the data source see this schema below:
So our code will be in the remote data source part. Let's start!
The Remote data source
We assume that all code is just an example, so we will imagine a Home feature.
The remote data source contract will be:
Then the implementation:
We have not implemented the getArticles right now, but we will do in the next chapters.
The HomeClient, Retrofit part
Now we will implement the Retrofit, I assume here that you already know how to use retrofit but this is the HomeClient:
If you want to know more about Retrofit follow this link:
Unit tests
Now we have all set up to begin the unit test and fill the getArticles() implementation. We will use the TDD approach to make the unit test.
Our first test will be to verify if we call the HomeClient, we have to set up some code before the test:
Like you can see here we just test if the homeClient.getArticles()
is called. So now we can run the test and see:
➜ retrofit dart test
00:01 +0 -1: test/features/home/data/datasources/home_remote_data_source_test.dart: get articles should perform a GET request on /articles [E]
UnimplementedError
bin/features/home/data/datasources/home_remote_data_source.dart 21:5 HomeRemoteDataSourceImpl.getArticles
test/features/home/data/datasources/home_remote_data_source_test.dart 50:20 main.<fn>.<fn>
test/features/home/data/datasources/home_remote_data_source_test.dart 41:7 main.<fn>.<fn>
00:01 +0 -1: loading test/features/home/data/datasources/home_remote_data_source_test.dart
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
00:01 +0 -1: Some tests failed.
➜ retrofit
Like excepted we have an error on the test (TDD), UnimplementedError
. So now we have to pass the test. Back to the HomeRemoteDataSourceImpl
:
Now pass the test again:
➜ retrofit dart test
00:01 +1: All tests passed!
➜ retrofit
Yes like you see we return a fake Article, but we want to have the list returned by getArticles() in the return. Let's make a test for it!
And we can again pass the tests:
00:00 +1 -1: test/features/home/data/datasources/home_remote_data_source_test.dart: get articles should return List<Articles> when the response is 200 (success) [E]
Expected: [Instance of 'Article']
Actual: [Instance of 'Article']
Which: at location [0] is <Instance of 'Article'> instead of <Instance of 'Article'>
package:test_api expect
test/features/home/data/datasources/home_remote_data_source_test.dart 69:9 main.<fn>.<fn>
00:00 +1 -1: loading test/features/home/data/datasources/home_remote_data_source_test.dart
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
00:00 +1 -1: Some tests failed.
➜ retrofit
And the implementation:
@override
Future<List<Article>> getArticles() async {
return await homeClient.getArticles();
}
Now the test should pass because we return directly the list from the homeClient
.
➜ retrofit dart test
00:00 +2: All tests passed!
➜ retrofit
Now we want to test if the call got a problem like an Exception. Retrofit returns a DioError when something gets wrong on a call. So we will mock that in the next test:
Here we mock the exception with a DioError because we know Retrofit returns a DioError. We can customize the Error with the status code for example. If you want to unit test a particular status code response you could.
Now again we pass the test:
➜ retrofit dart test
00:00 +2 -1: test/features/home/data/datasources/home_remote_data_source_test.dart: get articles should throw a ServerException when the response code is 404 or other (unsuccess) [E]
Expected: throws <Instance of 'ServerException'>
Actual: <Closure: () => Future<List<Article>>>
Which: threw DioError:<DioError [DioErrorType.other]: >
stack package:mocktail/src/mocktail.dart 249:7 When.thenThrow.<fn>
package:mocktail/src/mocktail.dart 132:37 Mock.noSuchMethod
bin/features/home/domain/entities/home_client.dart 12:25 MockHomeClient.getArticles
bin/features/home/data/datasources/home_remote_data_source.dart 20:29 HomeRemoteDataSourceImpl.getArticles
test/features/home/data/datasources/home_remote_data_source_test.dart 82:21 main.<fn>.<fn>.<fn>
package:test_api expect
test/features/home/data/datasources/home_remote_data_source_test.dart 81:9 main.<fn>.<fn>
test/features/home/data/datasources/home_remote_data_source_test.dart 66:7 main.<fn>.<fn>
which is not an instance of 'ServerException'
dart:async _CustomZone.runUnary
00:00 +2 -1: loading test/features/home/data/datasources/home_remote_data_source_test.dart
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
00:00 +2 -1: Some tests failed.
➜ retrofit
Ok, we got the error let's implement the code source:
And pass the test:
➜ retrofit dart test
00:00 +3: All tests passed!
➜ retrofit
Well done! Everything is tested now and you can replicate that for every future function you have to implement like a POST/PUT etc...
Conclusion
With this approach, we have a way to test Retrofit in a clean architecture project.
You can find all the codes/tests on my repo Github: