From ea2a14ced6b3d3f94ed02ba4a8740319047195ed Mon Sep 17 00:00:00 2001 From: Luke Chi Date: Sun, 21 Jun 2020 12:00:56 +0800 Subject: [PATCH 1/5] adjust --- lib/core/util/input_converter.dart | 4 +++- .../number_trivia_local_data_source_test.dart | 2 +- .../number_trivia_remote_data_source_test.dart | 12 +++++------- .../usecases/get_random_number_trivia_test.dart | 1 - .../presentation/bloc/number_trivia_bloc_test.dart | 14 ++++++++++---- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/core/util/input_converter.dart b/lib/core/util/input_converter.dart index 49c4016..845a2be 100644 --- a/lib/core/util/input_converter.dart +++ b/lib/core/util/input_converter.dart @@ -5,7 +5,9 @@ class InputConverter { Either stringToUnsignedInteger(String str) { try { final integer = int.parse(str); - if (integer < 0) throw FormatException(); + if (integer < 0) { + throw FormatException(); + } return Right(integer); } on FormatException { return Left(InvalidInputFailure()); diff --git a/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart b/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart index 9ad69b1..d0552c1 100644 --- a/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart +++ b/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart @@ -42,7 +42,7 @@ void main() { ); test( - 'should throw a CacheExeption when there is not a cached value', + 'should throw a CacheException when there is not a cached value', () async { // arrange when(mockSharedPreferences.getString(any)).thenReturn(null); diff --git a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart index 9e1e3f5..91b20a8 100644 --- a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart +++ b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart @@ -16,6 +16,9 @@ void main() { NumberTriviaRemoteDataSourceImpl dataSource; MockHttpClient mockHttpClient; + final tNumberTriviaModel = + NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); + setUp(() { mockHttpClient = MockHttpClient(); dataSource = NumberTriviaRemoteDataSourceImpl(client: mockHttpClient); @@ -33,8 +36,6 @@ void main() { group('getConcreteNumberTrivia', () { final tNumber = 1; - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); test( '''should perform a GET request on a URL with number @@ -80,12 +81,9 @@ void main() { }); group('getRandomNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); - test( - '''should perform a GET request on a URL with number - being the endpoint and with application/json header''', + '''should perform a GET request on a URL with *random* endpoint + with application/json header''', () async { // arrange setUpMockHttpClientSuccess200(); diff --git a/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart b/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart index f07116b..bdcdcf5 100644 --- a/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart +++ b/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart @@ -1,7 +1,6 @@ import 'package:clean_architecture_tdd_course/core/usecases/usecase.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/entities/number_trivia.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/repositories/number_trivia_repository.dart'; -import 'package:clean_architecture_tdd_course/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/usecases/get_random_number_trivia.dart'; import 'package:dartz/dartz.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart b/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart index 87d9089..5e9ecc9 100644 --- a/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart +++ b/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart @@ -48,11 +48,18 @@ void main() { when(mockInputConverter.stringToUnsignedInteger(any)) .thenReturn(Right(tNumberParsed)); + void setUpMockGetConcreteNumberTriviaSuccess() => + when(mockGetConcreteNumberTrivia(any)) + .thenAnswer((_) async => Right(tNumberTrivia)); + test( 'should call the InputConverter to validate and convert the string to an unsigned integer', () async { // arrange setUpMockInputConverterSuccess(); + // MEMO: need add this, or Unhandled error NoSuchMethodError: The method 'fold' was called on null. + setUpMockGetConcreteNumberTriviaSuccess(); + // act bloc.add(GetTriviaForConcreteNumber(tNumberString)); await untilCalled(mockInputConverter.stringToUnsignedInteger(any)); @@ -83,8 +90,7 @@ void main() { () async { // arrange setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); + setUpMockGetConcreteNumberTriviaSuccess(); // act bloc.add(GetTriviaForConcreteNumber(tNumberString)); await untilCalled(mockGetConcreteNumberTrivia(any)); @@ -98,8 +104,8 @@ void main() { () async { // arrange setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); + setUpMockGetConcreteNumberTriviaSuccess(); + // assert later final expected = [ Empty(), From 006154bc847d472c9a7e730f63c54ecf868667ab Mon Sep 17 00:00:00 2001 From: Luke Chi Date: Sun, 21 Jun 2020 12:01:18 +0800 Subject: [PATCH 2/5] adjust --- lib/core/usecases/usecase.dart | 2 ++ .../presentation/bloc/number_trivia_bloc.dart | 4 +++- .../repositories/number_trivia_repository_impl_test.dart | 7 ++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/core/usecases/usecase.dart b/lib/core/usecases/usecase.dart index 94b12a0..a52017c 100644 --- a/lib/core/usecases/usecase.dart +++ b/lib/core/usecases/usecase.dart @@ -7,6 +7,8 @@ abstract class UseCase { Future> call(Params params); } +// This will be used by the code calling the use case whenever the use case +// doesn't accept any parameters. class NoParams extends Equatable { @override List get props => []; diff --git a/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart b/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart index 03c80ff..09dd42d 100644 --- a/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart +++ b/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:clean_architecture_tdd_course/core/error/failures.dart'; import 'package:clean_architecture_tdd_course/core/usecases/usecase.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/entities/number_trivia.dart'; @@ -39,6 +39,8 @@ class NumberTriviaBloc extends Bloc { Stream mapEventToState( NumberTriviaEvent event, ) async* { + // Immediately branching the logic with type checking, in order + // for the event to be smart casted if (event is GetTriviaForConcreteNumber) { final inputEither = inputConverter.stringToUnsignedInteger(event.numberString); diff --git a/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart b/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart index c4c9d61..7e90d82 100644 --- a/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart +++ b/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart @@ -27,6 +27,7 @@ void main() { mockRemoteDataSource = MockRemoteDataSource(); mockLocalDataSource = MockLocalDataSource(); mockNetworkInfo = MockNetworkInfo(); + repository = NumberTriviaRepositoryImpl( remoteDataSource: mockRemoteDataSource, localDataSource: mockLocalDataSource, @@ -77,7 +78,7 @@ void main() { 'should return remote data when the call to remote data source is successful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenAnswer((_) async => tNumberTriviaModel); // act final result = await repository.getConcreteNumberTrivia(tNumber); @@ -91,7 +92,7 @@ void main() { 'should cache the data locally when the call to remote data source is successful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenAnswer((_) async => tNumberTriviaModel); // act await repository.getConcreteNumberTrivia(tNumber); @@ -105,7 +106,7 @@ void main() { 'should return server failure when the call to remote data source is unsuccessful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenThrow(ServerException()); // act final result = await repository.getConcreteNumberTrivia(tNumber); From 1ae6cd3c9797aae910016b503abc622079178ddf Mon Sep 17 00:00:00 2001 From: Luke Chi Date: Sun, 21 Jun 2020 12:01:34 +0800 Subject: [PATCH 3/5] adjust --- lib/core/error/exceptions.dart | 24 +++++++++++ .../number_trivia_local_data_source.dart | 3 +- .../number_trivia_remote_data_source.dart | 3 +- .../number_trivia_repository_impl.dart | 41 ++++++++++--------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/core/error/exceptions.dart b/lib/core/error/exceptions.dart index 451f647..664705d 100644 --- a/lib/core/error/exceptions.dart +++ b/lib/core/error/exceptions.dart @@ -1,3 +1,27 @@ +import 'package:dartz/dartz.dart'; + +import 'failures.dart'; + class ServerException implements Exception {} class CacheException implements Exception {} + +typedef Future> _function(); + +Future> catchServerException( + _function functionBody) async { + try { + return await functionBody(); + } on ServerException { + return Left(ServerFailure()); + } +} + +Future> catchCacheException( + _function functionBody) async { + try { + return await functionBody(); + } on CacheException { + return Left(CacheFailure()); + } +} diff --git a/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart b/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart index 9a9bec4..eb094aa 100644 --- a/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart +++ b/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart @@ -28,9 +28,8 @@ class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource { final jsonString = sharedPreferences.getString(CACHED_NUMBER_TRIVIA); if (jsonString != null) { return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString))); - } else { - throw CacheException(); } + throw CacheException(); } @override diff --git a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart index 978c225..311e735 100644 --- a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart +++ b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart @@ -41,8 +41,7 @@ class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource { if (response.statusCode == 200) { return NumberTriviaModel.fromJson(json.decode(response.body)); - } else { - throw ServerException(); } + throw ServerException(); } } diff --git a/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart b/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart index eb79106..00f2cb0 100644 --- a/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart +++ b/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart @@ -9,7 +9,7 @@ import '../../domain/repositories/number_trivia_repository.dart'; import '../datasources/number_trivia_local_data_source.dart'; import '../datasources/number_trivia_remote_data_source.dart'; -typedef Future _ConcreteOrRandomChooser(); +typedef Future> _ConcreteOrRandomChooser(); class NumberTriviaRepositoryImpl implements NumberTriviaRepository { final NumberTriviaRemoteDataSource remoteDataSource; @@ -26,36 +26,37 @@ class NumberTriviaRepositoryImpl implements NumberTriviaRepository { Future> getConcreteNumberTrivia( int number, ) async { - return await _getTrivia(() { - return remoteDataSource.getConcreteNumberTrivia(number); + return await _handleException(() { + return saveToLocalCache(() => remoteDataSource.getConcreteNumberTrivia(number)); }); } @override Future> getRandomNumberTrivia() async { - return await _getTrivia(() { - return remoteDataSource.getRandomNumberTrivia(); + return await _handleException(() { + return saveToLocalCache(remoteDataSource.getRandomNumberTrivia); }); } - Future> _getTrivia( + Future> _handleException( _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()); - } + return catchServerException(getConcreteOrRandom); } + return catchCacheException(_getLastNumberTriviaInCache); + } + + Future> saveToLocalCache( + getConcreteOrRandom, + ) async { + final remoteTrivia = await getConcreteOrRandom(); + localDataSource.cacheNumberTrivia(remoteTrivia); + return Right(remoteTrivia); + } + + Future> _getLastNumberTriviaInCache() async { + final localTrivia = await localDataSource.getLastNumberTrivia(); + return Right(localTrivia); } } From c3e94eac9a0baf938166284ee317f6a5e68983b1 Mon Sep 17 00:00:00 2001 From: Luke Chi Date: Sun, 21 Jun 2020 12:22:51 +0800 Subject: [PATCH 4/5] fix /issues/27 --- lib/core/util/input_converter.dart | 2 ++ test/core/util/input_converter_test.dart | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/core/util/input_converter.dart b/lib/core/util/input_converter.dart index 845a2be..600efb1 100644 --- a/lib/core/util/input_converter.dart +++ b/lib/core/util/input_converter.dart @@ -9,6 +9,8 @@ class InputConverter { throw FormatException(); } return Right(integer); + } on ArgumentError { + return Left(InvalidInputFailure()); } on FormatException { return Left(InvalidInputFailure()); } diff --git a/test/core/util/input_converter_test.dart b/test/core/util/input_converter_test.dart index 78c18e8..c0158c5 100644 --- a/test/core/util/input_converter_test.dart +++ b/test/core/util/input_converter_test.dart @@ -22,6 +22,18 @@ void main() { }, ); + test( + 'should return a Failure when the string is null', + () async { + // arrange + final str = null; + // act + final result = inputConverter.stringToUnsignedInteger(str); + // assert + expect(result, Left(InvalidInputFailure())); + }, + ); + test( 'should return a Failure when the string is not an integer', () async { From a7a71114e82d78473624a8e584da536e7d643747 Mon Sep 17 00:00:00 2001 From: Luke Chi Date: Sun, 21 Jun 2020 14:03:44 +0800 Subject: [PATCH 5/5] update --- .../number_trivia_remote_data_source.dart | 22 +++++++++++-------- ...number_trivia_remote_data_source_test.dart | 18 +++++++++++++++ test/fixtures/trivia_infinity.json | 6 +++++ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/trivia_infinity.json diff --git a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart index 311e735..aaa5f31 100644 --- a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart +++ b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart @@ -32,15 +32,19 @@ class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource { _getTriviaFromUrl('http://numbersapi.com/random'); Future _getTriviaFromUrl(String url) async { - final response = await client.get( - url, - headers: { - 'Content-Type': 'application/json', - }, - ); - - if (response.statusCode == 200) { - return NumberTriviaModel.fromJson(json.decode(response.body)); + try { + final response = await client.get( + url, + headers: { + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + return NumberTriviaModel.fromJson(json.decode(response.body)); + } + } catch (e) { + print(e); } throw ServerException(); } diff --git a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart index 91b20a8..dd955a8 100644 --- a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart +++ b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart @@ -29,6 +29,11 @@ void main() { .thenAnswer((_) async => http.Response(fixture('trivia.json'), 200)); } + void setUpMockHttpClientSuccess200Infinity() { + when(mockHttpClient.get(any, headers: anyNamed('headers'))) + .thenAnswer((_) async => http.Response(fixture('trivia_infinity.json'), 200)); + } + void setUpMockHttpClientFailure404() { when(mockHttpClient.get(any, headers: anyNamed('headers'))) .thenAnswer((_) async => http.Response('Something went wrong', 404)); @@ -111,6 +116,19 @@ void main() { }, ); + test( + '''should throw a ServerException when the response code is 200 (success) + but the Trivia number is null (infinity)''', + () async { + // arrange + setUpMockHttpClientSuccess200Infinity(); + // act + final call = dataSource.getRandomNumberTrivia; + // assert + expect(() => call(), throwsA(TypeMatcher())); + }, + ); + test( 'should throw a ServerException when the response code is 404 or other', () async { diff --git a/test/fixtures/trivia_infinity.json b/test/fixtures/trivia_infinity.json new file mode 100644 index 0000000..6d30fba --- /dev/null +++ b/test/fixtures/trivia_infinity.json @@ -0,0 +1,6 @@ +{ + "text": "Test Text", + "number": null, + "found": true, + "type": "trivia" +} \ No newline at end of file