After the previous part, we now have all the contracts of the Repository's dependencies in place. Those dependencies are the local and remote Data Source and also the NetworkInfo class, for finding out if the user is online. Mocking these dependencies will allow us to implement the Repository class using test-driven development.
Implementing the Repository
If you need a quick refresher regarding where we finished in the previous part, we currently have a test file with mocked dependencies...
number_trivia_repository_impl_test.dart
class MockRemoteDataSource extends Mock
implements NumberTriviaRemoteDataSource {}
class MockLocalDataSource extends Mock implements NumberTriviaLocalDataSource {}
class MockNetworkInfo extends Mock implements NetworkInfo {}
void main() {
NumberTriviaRepositoryImpl repository;
MockRemoteDataSource mockRemoteDataSource;
MockLocalDataSource mockLocalDataSource;
MockNetworkInfo mockNetworkInfo;
setUp(() {
mockRemoteDataSource = MockRemoteDataSource();
mockLocalDataSource = MockLocalDataSource();
mockNetworkInfo = MockNetworkInfo();
repository = NumberTriviaRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
localDataSource: mockLocalDataSource,
networkInfo: mockNetworkInfo,
);
});
}
...and also a plain, unimplemented Repository.
number_trivia_repository_impl.dart
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
final NumberTriviaRemoteDataSource remoteDataSource;
final NumberTriviaLocalDataSource localDataSource;
final NetworkInfo networkInfo;
NumberTriviaRepositoryImpl({
@required this.remoteDataSource,
@required this.localDataSource,
@required this.networkInfo,
});
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
return null;
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
return null;
}
}
Let's start with implementing the getConcreteNumberTrivia method first.
getConcreteNumberTrivia
It's the job of the Repository to get fresh data from the API when there is an Internet connection (and then to cache it locally), or to get the cached data when the user is offline.
Getting to know the network status of the device is therefore the first thing that should happen inside this method. Let's write a test following the Arrange - Act - Assert flow. We're mocking the NetworkInfo and verifying if its isConnected property has been called.
number_trivia_repository_impl_test.dart
group('getConcreteNumberTrivia', () {
// DATA FOR THE MOCKS AND ASSERTIONS
// We'll use these three variables throughout all the tests
final tNumber = 1;
final tNumberTriviaModel =
NumberTriviaModel(number: tNumber, text: 'test trivia');
final NumberTrivia tNumberTrivia = tNumberTriviaModel;
test('should check if the device is online', () {
//arrange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
// act
repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockNetworkInfo.isConnected);
});
});
I don't think I need to write it anymore, but this test will fail. We have to implement the needed functionality. With the other tests in this part, I'll just show you the production code immediately after the test code.
number_trivia_repository_impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
networkInfo.isConnected;
return null;
}
As you already know, the functionality of the method will differ based on whether or not the user is online. We will therefore "branch" the tests into two categories - online and offline. Let's start of with the online branch.
Online Behavior
test.dart
group('device is online', () {
// This setUp applies only to the 'device is online' group
setUp(() {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
});
test(
'should return remote data when the call to remote data source is successful',
() async {
// arrange
when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
.thenAnswer((_) async => tNumberTriviaModel);
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
expect(result, equals(Right(tNumberTrivia)));
},
);
});
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
networkInfo.isConnected;
return Right(await remoteDataSource.getConcreteNumberTrivia(number));
}
The Right side of the Either is the "success side" returning a NumberTrivia entity. The implementation still doesn't look like much but we're getting there.
Whenever trivia is successfully gotten from the API, we should cache it locally. That's what we're going to implement next (still within the online group) - you can always get the full project from GitHub, if you're lost, to see the code all in one place after going through the tutorial.
test.dart
test(
'should cache the data locally when the call to remote data source is successful',
() async {
// arrange
when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
.thenAnswer((_) async => tNumberTriviaModel);
// act
await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
verify(mockLocalDataSource.cacheNumberTrivia(tNumberTrivia));
},
);
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
networkInfo.isConnected;
final remoteTrivia = await remoteDataSource.getConcreteNumberTrivia(number);
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
}
Finally when we're online and the remote Data Source throws a ServerException, we should convert it to a ServerFailure and return it from the method. In such case, nothing should be cached locally (thus verifyZeroInteractions).
test.dart
test(
'should return server failure when the call to remote data source is unsuccessful',
() async {
// arrange
when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
.thenThrow(ServerException());
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
verifyZeroInteractions(mockLocalDataSource);
expect(result, equals(Left(ServerFailure())));
},
);
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
networkInfo.isConnected;
try {
final remoteTrivia =
await remoteDataSource.getConcreteNumberTrivia(number);
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
} on ServerException {
return Left(ServerFailure());
}
}
Offline Behavior
The tests above are enough for when the device is online, now comes the time to implement the offline behavior. Let's again create a new test group along with the first test. The Repository should return the last locally cached trivia when not online.
test.dart
group('device is offline', () {
setUp(() {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
});
test(
'should return last locally cached data when the cached data is present',
() async {
// arrange
when(mockLocalDataSource.getLastNumberTrivia())
.thenAnswer((_) async => tNumberTriviaModel);
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verifyZeroInteractions(mockRemoteDataSource);
verify(mockLocalDataSource.getLastNumberTrivia());
expect(result, equals(Right(tNumberTrivia)));
},
);
});
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
// Finally doing something with the value of isConnected
if (await networkInfo.isConnected) {
try {
final remoteTrivia =
await remoteDataSource.getConcreteNumberTrivia(number);
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
} on ServerException {
return Left(ServerFailure());
}
} else {
final localTrivia = await localDataSource.getLastNumberTrivia();
return Right(localTrivia);
}
}
We also have to handle the case when the local Data Source throws a CacheException by returning a CacheFailure through the Left "error" side of Either. As it writes in the documentation of the getLastNumberTrivia method, CacheException will happen whenever there is nothing present inside the cache.
test.dart
test(
'should return CacheFailure when there is no cached data present',
() async {
// arrange
when(mockLocalDataSource.getLastNumberTrivia())
.thenThrow(CacheException());
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verifyZeroInteractions(mockRemoteDataSource);
verify(mockLocalDataSource.getLastNumberTrivia());
expect(result, equals(Left(CacheFailure())));
},
);
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
if (await networkInfo.isConnected) {
try {
final remoteTrivia =
await remoteDataSource.getConcreteNumberTrivia(number);
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final localTrivia = await localDataSource.getLastNumberTrivia();
return Right(localTrivia);
} on CacheException {
return Left(CacheFailure());
}
}
}
getRandomNumberTrivia
The way we will build getRandomNumberTrivia will be almost identical to getConcreteNumberTrivia. We can even refactor some parts of the tests into named methods - namely the online and offline groups. Refactored using the two new methods, the current test code will look like this (truncated for brevity, get the full code on GitHub).
test.dart
void main() {
...
void runTestsOnline(Function body) {
group('device is online', () {
setUp(() {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
});
body();
});
}
void runTestsOffline(Function body) {
group('device is offline', () {
setUp(() {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
});
body();
});
}
group('getConcreteNumberTrivia', () {
...
runTestsOnline(() {
test(
...
});
runTestsOffline(() {
test(
...
});
});
}
The semi-official laws of TDD say that you should always write and implement tests one by one. I, however, like to be practical, especially when it comes to coding in tutorials like this one.
Since the getRandomNumberTrivia method will differ only in a single call to the remote Data Source, we're going to copy all the tests we currently have for the concrete method and slightly modify them to work with the random method.
test.dart
group('getRandomNumberTrivia', () {
final tNumberTriviaModel =
NumberTriviaModel(number: 123, text: 'test trivia');
final NumberTrivia tNumberTrivia = tNumberTriviaModel;
test('should check if the device is online', () {
//arrange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
// act
repository.getRandomNumberTrivia();
// assert
verify(mockNetworkInfo.isConnected);
});
runTestsOnline(() {
test(
'should return remote data when the call to remote data source is successful',
() async {
// arrange
when(mockRemoteDataSource.getRandomNumberTrivia())
.thenAnswer((_) async => tNumberTriviaModel);
// act
final result = await repository.getRandomNumberTrivia();
// assert
verify(mockRemoteDataSource.getRandomNumberTrivia());
expect(result, equals(Right(tNumberTrivia)));
},
);
test(
'should cache the data locally when the call to remote data source is successful',
() async {
// arrange
when(mockRemoteDataSource.getRandomNumberTrivia())
.thenAnswer((_) async => tNumberTriviaModel);
// act
await repository.getRandomNumberTrivia();
// assert
verify(mockRemoteDataSource.getRandomNumberTrivia());
verify(mockLocalDataSource.cacheNumberTrivia(tNumberTrivia));
},
);
test(
'should return server failure when the call to remote data source is unsuccessful',
() async {
// arrange
when(mockRemoteDataSource.getRandomNumberTrivia())
.thenThrow(ServerException());
// act
final result = await repository.getRandomNumberTrivia();
// assert
verify(mockRemoteDataSource.getRandomNumberTrivia());
verifyZeroInteractions(mockLocalDataSource);
expect(result, equals(Left(ServerFailure())));
},
);
});
runTestsOffline(() {
test(
'should return last locally cached data when the cached data is present',
() async {
// arrange
when(mockLocalDataSource.getLastNumberTrivia())
.thenAnswer((_) async => tNumberTriviaModel);
// act
final result = await repository.getRandomNumberTrivia();
// assert
verifyZeroInteractions(mockRemoteDataSource);
verify(mockLocalDataSource.getLastNumberTrivia());
expect(result, equals(Right(tNumberTrivia)));
},
);
test(
'should return CacheFailure when there is no cached data present',
() async {
// arrange
when(mockLocalDataSource.getLastNumberTrivia())
.thenThrow(CacheException());
// act
final result = await repository.getRandomNumberTrivia();
// assert
verifyZeroInteractions(mockRemoteDataSource);
verify(mockLocalDataSource.getLastNumberTrivia());
expect(result, equals(Left(CacheFailure())));
},
);
});
});
In the spirit of TDD, we won't do any premature refactoring. Let's first implement the code naively, although we already know it will be littered with duplication.
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() async {
if (await networkInfo.isConnected) {
try {
final remoteTrivia = await remoteDataSource.getRandomNumberTrivia();
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final localTrivia = await localDataSource.getLastNumberTrivia();
return Right(localTrivia);
} on CacheException {
return Left(CacheFailure());
}
}
}
The literal single difference between concrete and random is just calling for this code to be refactored. Most of the logic can be shared between the concrete and random methods and we'll handle the single different call to the local Data Source with a higher-order function. The final version of the NumberTriviaRepositoryImpl will look like the following:
impl.dart
typedef Future<NumberTrivia> _ConcreteOrRandomChooser();
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
final NumberTriviaRemoteDataSource remoteDataSource;
final NumberTriviaLocalDataSource localDataSource;
final NetworkInfo networkInfo;
NumberTriviaRepositoryImpl({
@required this.remoteDataSource,
@required this.localDataSource,
@required this.networkInfo,
});
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number,
) async {
return await _getTrivia(() {
return remoteDataSource.getConcreteNumberTrivia(number);
});
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() async {
return await _getTrivia(() {
return remoteDataSource.getRandomNumberTrivia();
});
}
Future<Either<Failure, NumberTrivia>> _getTrivia(
_ConcreteOrRandomChooser getConcreteOrRandom,
) async {
if (await networkInfo.isConnected) {
try {
final remoteTrivia = await getConcreteOrRandom();
localDataSource.cacheNumberTrivia(remoteTrivia);
return Right(remoteTrivia);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final localTrivia = await localDataSource.getLastNumberTrivia();
return Right(localTrivia);
} on CacheException {
return Left(CacheFailure());
}
}
}
}
If this isn't a thing of beauty, I don't know what is! All the tests are still passing, so we can be confident that this refactoring didn't break anything.
What's Next
Now that we have the Repository fully implemented, we are going start working on the low-level parts of the data layer by implementing the Data Sources and Network Info. Subscribe below, grow your Flutter skills and never miss a tutorial which will always make you more prepared for real app development.
This part was hard to keep up! I made it to the end but I am still not feeling confident! I noticed that I really need to go deeper into TDD. Can you recommend any specific stuff to dive into this content with focus on Flutter?
Agree, he keep giving us test code then impl code without asking us to run the test.
I will try re read this with different approach to make me more confident.
write one test,
run test and see how the test fail
write impl
run test, see how the test fail and write impl until it pass
repeat step above for each test
Here is my strategy
write a test as indicated by Reso Coder.
run the test and see how it fails.
Debug the error with the AI or by looking at the Mockito updates.
Edit repository_impl until it passes
Excelent, this example is witch Future repository, i like view one sample witch Stream…
thanks!
Hey,
Thanks for the great tutorial. I’m facing this small issue.
Test is not passing for server failure.
ERROR: Expected: Left:
Actual: Left:
Hope you can help me.
Regards
Hi Matej,
Very nice tutorials. I am following one by one.
During my learning Things got more clear when I set yhe NumberTrivia to NumberTriviaEntiry. It have made more clear in the code when I was using one or another.
[…] aclarar que el contenido original es de Resocoder, lo que he hecho es una traducción al español del contenido. Al final de este artículo está […]
I hate every refractor that you make in this part 6.
So many different name from part 5.
Nice tutorial!
But My return Right(await …) is returning Right instead of Right.
What can I do?
Thanks
For the purists, the return type of ‘_ConcreteOrRandomChooser’ should be NumberTriviaModel not NumberTrivia, same as the return type of data source get* methods. Just a thought.
I thought the compiler would complain there, but it seems to be pretty clever.
It certainly breaks if I put a “real” NumberTrivia into localDataSource.cacheNumberTrivia(remoteTrivia)
it gave me an error lol but i solved it by
localDataSource.cacheNumberTrivia
(remoteTrivia as NumberTriviaModel)
I thought the same thing. I didn’t see the point of using the entity, let’s remember that it is numbertriviamodel that extends from the entity, also that with that object they will do procedures for the repo such as saving it and I didn’t see the point of using the commercial object.
Hey there! Really loving your advanced Flutter videos. They really help a lot.
How can I do TDD on something like Flutter Local Notifications? Would I have to use Mockito, or leave it altogether?
Je suis avec passion ces tutoriels.
Merci pour le partage
Very good tutorial!
One question: how to handle the case of the repository returning a Stream instead of a Future? What would the tests be like? Thanks in advance!
Hi,
the github code and the one is written here does not match, like test groups. And then you change the file names to so it makes it difficult to follow up. Anyway I already donated because of your work. Thanks.
Hi – I’ve been following these tutorials and they’ve been great so far. Thank you so much for the effort of making these.
This one was tricker and harder to follow. Specifically, I don’t understand how the test ‘should check if the device is online’ is passing, if you have not stubbed mockRemoteDataSource.getConcreteNumberTrivia after adding it to the repository implementation of getConcreteNumberTrivia. I am getting an error that it is not stubbed and the test fails.
Why you casting NumberTriviaModel to an Entity?
I’m also wondering the same thing. When a NumberTriviaModel extends NumberTrivia, do we still need to cast it?
I fetched error in ‘should check if the device is online’
The error was – MissingStubError
I searched in google for solution but I didn’t get right solution.
After researching my code I got the problem.
test(
‘should check if the device is online’,
() async {
// arrange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
// Add this line
when(mockRemoteDataSource.getConcreteNumberTrivia(any))
.thenAnswer((_) async => tNumberTriviaModel);
// act
await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockNetworkInfo.isConnected);
},
);
Thanks a lot for your solution.
I also added a similar line to the getRandomTrivia online test.
… when(mockRemoteDataSource.getRandomNumberTrivia())
.thenAnswer((realInvocation) async => tNumberTrivia);
…
Thank you very much for posting this answer, much appreciated!
great answer, thank you!
also i think no need to await the repository.getConcreteNumberTrivia(tNumber)
This part was a bit hard to follow! I got to the end and I really feel like I’ve learnt something new! Thank you very much. I’m already trying to execute all this in a travel App with Flutter. With the usecase getAvialableFlightOffer(). Here is my strategy
write a test as indicated by Reso Coder.
run the test and see how it fails.
Debug the error with the AI or by looking at the Mockito updates.
Edit repository_impl until it passes
At the end of each chapter I feel more confident. I can only recommend this site.
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.