diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 227689c..539a668 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -73,8 +73,8 @@ Future setup() async { ..registerLazySingleton( () => ThemeRepositoryImpl(service: getIt()), ) - ..registerLazySingleton( - () => NumberRepositoryImpl(service: getIt()), + ..registerLazySingleton( + () => NumbersRepositoryImpl(service: getIt()), ) ..registerLazySingleton( () => GetThemeUseCase(repository: getIt()), diff --git a/lib/src/app/domain/domain.dart b/lib/src/app/domain/domain.dart index 5e7bf10..3d91876 100644 --- a/lib/src/app/domain/domain.dart +++ b/lib/src/app/domain/domain.dart @@ -1,2 +1,2 @@ export 'repositories/repositories.dart'; -export 'usecases/usecases.dart'; +export 'use_cases/use_cases.dart'; diff --git a/lib/src/app/domain/usecases/get_theme_usecase.dart b/lib/src/app/domain/use_cases/get_theme_use_case.dart similarity index 100% rename from lib/src/app/domain/usecases/get_theme_usecase.dart rename to lib/src/app/domain/use_cases/get_theme_use_case.dart diff --git a/lib/src/app/domain/usecases/save_theme_usecase.dart b/lib/src/app/domain/use_cases/save_theme_use_case.dart similarity index 100% rename from lib/src/app/domain/usecases/save_theme_usecase.dart rename to lib/src/app/domain/use_cases/save_theme_use_case.dart diff --git a/lib/src/app/domain/use_cases/use_cases.dart b/lib/src/app/domain/use_cases/use_cases.dart new file mode 100644 index 0000000..bdd96a0 --- /dev/null +++ b/lib/src/app/domain/use_cases/use_cases.dart @@ -0,0 +1,2 @@ +export 'get_theme_use_case.dart'; +export 'save_theme_use_case.dart'; diff --git a/lib/src/app/domain/usecases/usecases.dart b/lib/src/app/domain/usecases/usecases.dart deleted file mode 100644 index a2fdf1d..0000000 --- a/lib/src/app/domain/usecases/usecases.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'get_theme_usecase.dart'; -export 'save_theme_usecase.dart'; diff --git a/lib/src/core/core.dart b/lib/src/core/core.dart index a81cde4..67a8588 100644 --- a/lib/src/core/core.dart +++ b/lib/src/core/core.dart @@ -3,4 +3,4 @@ export 'extensions/extensions.dart'; export 'l10n/l10n.dart'; export 'params/params.dart'; export 'resources/resources.dart'; -export 'usecases/usecase.dart'; +export 'use_case/use_case.dart'; diff --git a/lib/src/core/params/number_request_params.dart b/lib/src/core/params/get_number_params.dart similarity index 100% rename from lib/src/core/params/number_request_params.dart rename to lib/src/core/params/get_number_params.dart diff --git a/lib/src/core/params/params.dart b/lib/src/core/params/params.dart index 184bb49..01044df 100644 --- a/lib/src/core/params/params.dart +++ b/lib/src/core/params/params.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; +export 'get_number_params.dart'; export 'get_theme_params.dart'; -export 'number_request_params.dart'; export 'save_theme_params.dart'; @immutable diff --git a/lib/src/core/resources/data_state.dart b/lib/src/core/resources/data_state.dart index ab1d689..ce501dc 100644 --- a/lib/src/core/resources/data_state.dart +++ b/lib/src/core/resources/data_state.dart @@ -1,12 +1,14 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:equatable/equatable.dart'; import 'package:numbers/src/core/core.dart'; -@immutable -sealed class DataState { +sealed class DataState extends Equatable { const DataState({this.data, this.details}); final T? data; final ErrorDetails? details; + + @override + List get props => [data, details]; } final class DataSuccess extends DataState { diff --git a/lib/src/core/resources/error_details.dart b/lib/src/core/resources/error_details.dart index eb6a226..15557e1 100644 --- a/lib/src/core/resources/error_details.dart +++ b/lib/src/core/resources/error_details.dart @@ -1,4 +1,6 @@ -final class ErrorDetails { +import 'package:equatable/equatable.dart'; + +final class ErrorDetails extends Equatable { const ErrorDetails({ required this.error, required this.message, @@ -8,4 +10,7 @@ final class ErrorDetails { final String error; final String message; final String stackTrace; + + @override + List get props => [error, message, stackTrace]; } diff --git a/lib/src/core/usecases/usecase.dart b/lib/src/core/use_case/use_case.dart similarity index 100% rename from lib/src/core/usecases/usecase.dart rename to lib/src/core/use_case/use_case.dart diff --git a/lib/src/features/number/data/repositories/number_repository_impl.dart b/lib/src/features/number/data/repositories/numbers_repository_impl.dart similarity index 91% rename from lib/src/features/number/data/repositories/number_repository_impl.dart rename to lib/src/features/number/data/repositories/numbers_repository_impl.dart index dbd8f58..747facf 100644 --- a/lib/src/features/number/data/repositories/number_repository_impl.dart +++ b/lib/src/features/number/data/repositories/numbers_repository_impl.dart @@ -1,8 +1,8 @@ import 'package:numbers/src/core/core.dart'; import 'package:numbers/src/features/number/number.dart'; -final class NumberRepositoryImpl implements NumberRepository { - const NumberRepositoryImpl({ +final class NumbersRepositoryImpl implements NumbersRepository { + const NumbersRepositoryImpl({ required NumbersService service, }) : _numbersService = service; diff --git a/lib/src/features/number/data/repositories/repositories.dart b/lib/src/features/number/data/repositories/repositories.dart index 4860252..c3ad8d5 100644 --- a/lib/src/features/number/data/repositories/repositories.dart +++ b/lib/src/features/number/data/repositories/repositories.dart @@ -1 +1 @@ -export 'number_repository_impl.dart'; +export 'numbers_repository_impl.dart'; diff --git a/lib/src/features/number/domain/domain.dart b/lib/src/features/number/domain/domain.dart index ff77d1c..cc16ca3 100644 --- a/lib/src/features/number/domain/domain.dart +++ b/lib/src/features/number/domain/domain.dart @@ -1,3 +1,3 @@ export 'entities/entities.dart'; export 'repositories/repositories.dart'; -export 'usecases/usecases.dart'; +export 'use_cases/use_cases.dart'; diff --git a/lib/src/features/number/domain/repositories/number_repository.dart b/lib/src/features/number/domain/repositories/numbers_repository.dart similarity index 82% rename from lib/src/features/number/domain/repositories/number_repository.dart rename to lib/src/features/number/domain/repositories/numbers_repository.dart index 1de268b..77edac1 100644 --- a/lib/src/features/number/domain/repositories/number_repository.dart +++ b/lib/src/features/number/domain/repositories/numbers_repository.dart @@ -1,7 +1,7 @@ import 'package:numbers/src/core/core.dart'; import 'package:numbers/src/features/number/number.dart'; -abstract interface class NumberRepository { +abstract interface class NumbersRepository { Future> getNumber(GetNumberParams params); Future> getRandomNumber(); } diff --git a/lib/src/features/number/domain/repositories/repositories.dart b/lib/src/features/number/domain/repositories/repositories.dart index 8df4e0f..97b6503 100644 --- a/lib/src/features/number/domain/repositories/repositories.dart +++ b/lib/src/features/number/domain/repositories/repositories.dart @@ -1 +1 @@ -export 'number_repository.dart'; +export 'numbers_repository.dart'; diff --git a/lib/src/features/number/domain/usecases/get_number_usecase.dart b/lib/src/features/number/domain/use_cases/get_number_use_case.dart similarity index 50% rename from lib/src/features/number/domain/usecases/get_number_usecase.dart rename to lib/src/features/number/domain/use_cases/get_number_use_case.dart index 2877d82..05cd7aa 100644 --- a/lib/src/features/number/domain/usecases/get_number_usecase.dart +++ b/lib/src/features/number/domain/use_cases/get_number_use_case.dart @@ -1,15 +1,19 @@ +import 'package:flutter/foundation.dart'; import 'package:numbers/src/core/core.dart'; import 'package:numbers/src/features/number/number.dart'; final class GetNumberUseCase implements UseCase, GetNumberParams> { const GetNumberUseCase({ - required NumberRepository repository, - }) : _numberRepository = repository; + required NumbersRepository repository, + }) : _numbersRepository = repository; - final NumberRepository _numberRepository; + final NumbersRepository _numbersRepository; @override Future> call({required GetNumberParams params}) => - _numberRepository.getNumber(params); + _numbersRepository.getNumber(params); } + +@visibleForTesting +base mixin MockGetNumberUseCaseMixin implements GetNumberUseCase {} diff --git a/lib/src/features/number/domain/use_cases/get_random_number_use_case.dart b/lib/src/features/number/domain/use_cases/get_random_number_use_case.dart new file mode 100644 index 0000000..facc3d9 --- /dev/null +++ b/lib/src/features/number/domain/use_cases/get_random_number_use_case.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +final class GetRandomNumberUseCase + implements UseCase, Params> { + const GetRandomNumberUseCase({ + required NumbersRepository repository, + }) : _numbersRepository = repository; + + final NumbersRepository _numbersRepository; + + @override + Future> call({Params params = const Params()}) => + _numbersRepository.getRandomNumber(); +} + +@visibleForTesting +base mixin MockGetRandomNumberUseCaseMixin implements GetRandomNumberUseCase {} diff --git a/lib/src/features/number/domain/use_cases/use_cases.dart b/lib/src/features/number/domain/use_cases/use_cases.dart new file mode 100644 index 0000000..fbe0c4f --- /dev/null +++ b/lib/src/features/number/domain/use_cases/use_cases.dart @@ -0,0 +1,2 @@ +export 'get_number_use_case.dart' show GetNumberUseCase; +export 'get_random_number_use_case.dart' show GetRandomNumberUseCase; diff --git a/lib/src/features/number/domain/usecases/get_random_number_usecase.dart b/lib/src/features/number/domain/usecases/get_random_number_usecase.dart deleted file mode 100644 index 816621a..0000000 --- a/lib/src/features/number/domain/usecases/get_random_number_usecase.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:numbers/src/core/core.dart'; -import 'package:numbers/src/features/number/number.dart'; - -final class GetRandomNumberUseCase - implements UseCase, Params> { - const GetRandomNumberUseCase({ - required NumberRepository repository, - }) : _numberRepository = repository; - - final NumberRepository _numberRepository; - - @override - Future> call({Params params = const Params()}) => - _numberRepository.getRandomNumber(); -} diff --git a/lib/src/features/number/domain/usecases/usecases.dart b/lib/src/features/number/domain/usecases/usecases.dart deleted file mode 100644 index 0140fb2..0000000 --- a/lib/src/features/number/domain/usecases/usecases.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'get_number_usecase.dart'; -export 'get_random_number_usecase.dart'; diff --git a/test/helpers/extensions/extensions.dart b/test/helpers/extensions/extensions.dart new file mode 100644 index 0000000..7ea732b --- /dev/null +++ b/test/helpers/extensions/extensions.dart @@ -0,0 +1 @@ +export 'widget_tester_ext.dart'; diff --git a/test/helpers/pump_app.dart b/test/helpers/extensions/widget_tester_ext.dart similarity index 90% rename from test/helpers/pump_app.dart rename to test/helpers/extensions/widget_tester_ext.dart index d120468..f05253c 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/extensions/widget_tester_ext.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:numbers/src/core/l10n/l10n.dart'; -extension PumpApp on WidgetTester { +extension WidgetTesterExt on WidgetTester { Future pumpApp(Widget widget) { return pumpWidget( MaterialApp( diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index b15fe65..d31d42e 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1 +1,2 @@ -export 'pump_app.dart'; +export 'extensions/extensions.dart'; +export 'mocks/mocks.dart'; diff --git a/test/helpers/mocks/mock_dio.dart b/test/helpers/mocks/mock_dio.dart new file mode 100644 index 0000000..c3fe066 --- /dev/null +++ b/test/helpers/mocks/mock_dio.dart @@ -0,0 +1,4 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; + +final class MockDio extends Mock implements Dio {} diff --git a/test/helpers/mocks/mock_get_number_use_case.dart b/test/helpers/mocks/mock_get_number_use_case.dart new file mode 100644 index 0000000..75fffe0 --- /dev/null +++ b/test/helpers/mocks/mock_get_number_use_case.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/features/number/domain/use_cases/get_number_use_case.dart'; + +final class MockGetNumberUseCase extends Mock with MockGetNumberUseCaseMixin {} diff --git a/test/helpers/mocks/mock_get_random_number_use_case.dart b/test/helpers/mocks/mock_get_random_number_use_case.dart new file mode 100644 index 0000000..cb7b7ef --- /dev/null +++ b/test/helpers/mocks/mock_get_random_number_use_case.dart @@ -0,0 +1,5 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/features/number/domain/use_cases/get_random_number_use_case.dart'; + +final class MockGetRandomNumberUseCase extends Mock + with MockGetRandomNumberUseCaseMixin {} diff --git a/test/helpers/mocks/mock_numbers_repository.dart b/test/helpers/mocks/mock_numbers_repository.dart new file mode 100644 index 0000000..635d689 --- /dev/null +++ b/test/helpers/mocks/mock_numbers_repository.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/features/number/number.dart'; + +final class MockNumbersRepository extends Mock implements NumbersRepository {} diff --git a/test/helpers/mocks/mock_numbers_service.dart b/test/helpers/mocks/mock_numbers_service.dart new file mode 100644 index 0000000..b622765 --- /dev/null +++ b/test/helpers/mocks/mock_numbers_service.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/features/number/number.dart'; + +final class MockNumbersService extends Mock implements NumbersService {} diff --git a/test/helpers/mocks/mocks.dart b/test/helpers/mocks/mocks.dart new file mode 100644 index 0000000..5312268 --- /dev/null +++ b/test/helpers/mocks/mocks.dart @@ -0,0 +1,5 @@ +export 'mock_dio.dart'; +export 'mock_get_number_use_case.dart'; +export 'mock_get_random_number_use_case.dart'; +export 'mock_numbers_repository.dart'; +export 'mock_numbers_service.dart'; diff --git a/test/src/core/params/get_number_params_test.dart b/test/src/core/params/get_number_params_test.dart new file mode 100644 index 0000000..d7b6ed0 --- /dev/null +++ b/test/src/core/params/get_number_params_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/core/core.dart'; + +void main() { + group('GetNumberParams', () { + test('extends from Params', () { + expect( + const GetNumberParams(number: 0), + isA(), + ); + }); + }); +} diff --git a/test/src/core/params/get_theme_params_test.dart b/test/src/core/params/get_theme_params_test.dart new file mode 100644 index 0000000..7fc7f9c --- /dev/null +++ b/test/src/core/params/get_theme_params_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/core/core.dart'; + +void main() { + group('GetThemeParams', () { + test('extends from Params', () { + expect( + const GetThemeParams(key: 'test'), + isA(), + ); + }); + }); +} diff --git a/test/src/core/params/save_theme_params_test.dart b/test/src/core/params/save_theme_params_test.dart new file mode 100644 index 0000000..0d7c147 --- /dev/null +++ b/test/src/core/params/save_theme_params_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/core/core.dart'; + +void main() { + group('SaveThemeParams', () { + test('extends from Params', () { + expect( + const SaveThemeParams(key: 'key', value: 'value'), + isA(), + ); + }); + }); +} diff --git a/test/src/core/resources/data_state_test.dart b/test/src/core/resources/data_state_test.dart new file mode 100644 index 0000000..7c065a0 --- /dev/null +++ b/test/src/core/resources/data_state_test.dart @@ -0,0 +1,58 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/core/core.dart'; + +void main() { + group('DataSuccess', () { + test('extends from DataState', () { + expect( + DataSuccess('test'), + isA>(), + ); + }); + + test('supports value comparisons', () { + expect( + DataSuccess('test'), + equals(DataSuccess('test')), + ); + }); + }); + + group('DataFailure', () { + test('extends from DataState', () { + expect( + DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + isA>(), + ); + }); + + test('supports value comparisons', () { + expect( + DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + equals( + DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ), + ); + }); + }); +} diff --git a/test/src/core/resources/error_details_test.dart b/test/src/core/resources/error_details_test.dart new file mode 100644 index 0000000..c60dbea --- /dev/null +++ b/test/src/core/resources/error_details_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/core/core.dart'; + +void main() { + group('ErrorDetails', () { + test('supports value comparisons', () { + expect( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + equals( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ); + }); + }); +} diff --git a/test/src/data/repositories/number_repository_impl_test.dart b/test/src/data/repositories/number_repository_impl_test.dart deleted file mode 100644 index 59ac156..0000000 --- a/test/src/data/repositories/number_repository_impl_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:numbers/src/core/core.dart'; -import 'package:numbers/src/data/data.dart'; -import 'package:numbers/src/domain/domain.dart'; - -class MockNumbersApiService extends Mock implements NumbersApiService {} - -void main() { - late MockNumbersApiService mockNumbersApiService; - late GetNumberParams numberRequestParams; - late NumberRepositoryImpl sut; - - setUp( - () { - mockNumbersApiService = MockNumbersApiService(); - numberRequestParams = const GetNumberParams(number: 0); - sut = NumberRepositoryImpl(service: mockNumbersApiService); - }, - ); - - void arrangeNumbersApiServiceGetNumberResponse() { - when(() => mockNumbersApiService.getNumber(params: numberRequestParams)) - .thenAnswer((_) async => const NumberModel(info: '')); - } - - void arrangeNumbersApiServiceGetRandomNumberResponse() { - when(() => mockNumbersApiService.getRandomNumber()) - .thenAnswer((_) async => const NumberModel(info: '')); - } - - group( - 'NumberRepositoryImpl', - () { - test( - 'calls "getNumber" function only one time', - () async { - arrangeNumbersApiServiceGetNumberResponse(); - - await sut.getNumber(numberRequestParams); - - verify( - () => mockNumbersApiService.getNumber(params: numberRequestParams), - ).called(1); - }, - ); - - test( - 'calls "getRandomNumber" function two times', - () async { - arrangeNumbersApiServiceGetRandomNumberResponse(); - - await sut.getRandomNumber(); - await sut.getRandomNumber(); - - verify( - () => mockNumbersApiService.getRandomNumber(), - ).called(2); - }, - ); - - test( - 'gets number from the service', - () async { - arrangeNumbersApiServiceGetNumberResponse(); - - final result = await sut.getNumber(numberRequestParams); - - expect( - result, - equals(DataSuccess(const NumberModel(info: '').toEntity())), - ); - verify( - () => mockNumbersApiService.getNumber(params: numberRequestParams), - ); - verifyNoMoreInteractions(mockNumbersApiService); - }, - ); - - test( - 'gets random number from the service', - () async { - arrangeNumbersApiServiceGetRandomNumberResponse(); - - final result = await sut.getRandomNumber(); - - expect( - result, - equals(const DataSuccess(Number(info: ''))), - ); - verify( - () => mockNumbersApiService.getRandomNumber(), - ); - verifyNoMoreInteractions(mockNumbersApiService); - }, - ); - }, - ); -} diff --git a/test/src/domain/usecases/get_number_usecase_test.dart b/test/src/domain/usecases/get_number_usecase_test.dart deleted file mode 100644 index a51ee75..0000000 --- a/test/src/domain/usecases/get_number_usecase_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:numbers/src/core/core.dart'; -import 'package:numbers/src/domain/domain.dart'; - -const String _kResponseInfo = - '0 is the atomic number of the theoretical element tetraneutron.'; - -class MockNumberRepository extends Mock implements NumberRepository {} - -void main() { - late MockNumberRepository mockNumberRepository; - late GetNumberParams numberRequestParams; - late GetNumberUseCase sut; - - setUp(() { - mockNumberRepository = MockNumberRepository(); - numberRequestParams = const GetNumberParams(number: 0); - sut = GetNumberUseCase(repository: mockNumberRepository); - }); - - void arrangeNumberRepositoryResponse(GetNumberParams params) { - when( - () => mockNumberRepository.getNumber(params), - ).thenAnswer((_) async => const DataSuccess(Number(info: _kResponseInfo))); - } - - group( - 'GetNumberUsecase', - () { - test( - 'calls "getNumber" function only one time', - () async { - arrangeNumberRepositoryResponse(numberRequestParams); - - await sut(params: numberRequestParams); - - verify(() => mockNumberRepository.getNumber(numberRequestParams)) - .called(1); - }, - ); - - test( - 'gets data from the repository', - () async { - arrangeNumberRepositoryResponse(numberRequestParams); - - final result = await sut(params: numberRequestParams); - - expect( - result, - equals(const DataSuccess(Number(info: _kResponseInfo))), - ); - verify(() => mockNumberRepository.getNumber(numberRequestParams)); - verifyNoMoreInteractions(mockNumberRepository); - }, - ); - }, - ); -} diff --git a/test/src/domain/usecases/get_random_number_usecase_test.dart b/test/src/domain/usecases/get_random_number_usecase_test.dart deleted file mode 100644 index 6ba2368..0000000 --- a/test/src/domain/usecases/get_random_number_usecase_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:numbers/src/core/core.dart'; -import 'package:numbers/src/domain/domain.dart'; - -class MockNumberRepository extends Mock implements NumberRepository {} - -void main() { - late MockNumberRepository mockNumberRepository; - late NoParams noParams; - late GetRandomNumberUseCase sut; - - setUp(() { - mockNumberRepository = MockNumberRepository(); - noParams = const NoParams(); - sut = GetRandomNumberUseCase(repository: mockNumberRepository); - }); - - void arrangeNumberRepositoryResponse() { - when(mockNumberRepository.getRandomNumber).thenAnswer( - (_) async => const DataFailure(Failure(title: '', message: '')), - ); - } - - group( - 'GetRandomNumberUseCase', - () { - test( - 'calls "getRandomNumber" function only one time', - () async { - arrangeNumberRepositoryResponse(); - - await sut(params: noParams); - - verify(mockNumberRepository.getRandomNumber).called(1); - }, - ); - - test( - 'gets failure from the repository', - () async { - arrangeNumberRepositoryResponse(); - - final result = await sut(params: noParams); - - expect( - result, - equals(const DataFailure(Failure(title: '', message: ''))), - ); - verify(mockNumberRepository.getRandomNumber); - verifyNoMoreInteractions(mockNumberRepository); - }, - ); - }, - ); -} diff --git a/test/src/features/number/data/datasources/numbers_service/dio_numbers_service_test.dart b/test/src/features/number/data/datasources/numbers_service/dio_numbers_service_test.dart new file mode 100644 index 0000000..17d7750 --- /dev/null +++ b/test/src/features/number/data/datasources/numbers_service/dio_numbers_service_test.dart @@ -0,0 +1,113 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +import '../../../../../../helpers/helpers.dart'; + +void main() { + late MockDio mockDio; + late String baseUrl; + late DioNumbersService sut; + + setUp(() { + mockDio = MockDio(); + baseUrl = 'testUrl'; + sut = DioNumbersService(dio: mockDio, baseUrl: baseUrl); + }); + + void fakeGetResponse(String? info) { + if (info == null) { + when(() => mockDio.get(any())).thenAnswer( + (_) async => Response( + statusCode: 500, + requestOptions: RequestOptions(), + ), + ); + } else { + when(() => mockDio.get(any())).thenAnswer( + (_) async => Response( + data: info, + statusCode: 200, + requestOptions: RequestOptions(), + ), + ); + } + } + + group('DioNumbersService', () { + test('is concrete implementation of NumbersService', () { + expect( + sut, + isA(), + ); + }); + + test('getNumber returns NumberDto', () async { + const info = 'test'; + const params = GetNumberParams(number: 0); + + fakeGetResponse(info); + + final result = await sut.getNumber(params: params); + + expect( + result, + equals( + NumberDto( + info: info, + ), + ), + ); + + verify(() => mockDio.get('$baseUrl/${params.number}')).called(1); + }); + + test('getNumber throws GetNumberException on non-200 response', () { + const params = GetNumberParams(number: 0); + + fakeGetResponse(null); + + expect( + sut.getNumber(params: params), + throwsA( + isA(), + ), + ); + }); + + test('getRandomNumber returns NumberDto', () async { + const info = 'test'; + + fakeGetResponse(info); + + final result = await sut.getRandomNumber(); + + expect( + result, + equals( + NumberDto( + info: info, + ), + ), + ); + + verify(() => mockDio.get('$baseUrl/random')).called(1); + }); + + test('getRandomNumber throws GetRandomNumberException on non-200 response', + () { + fakeGetResponse(null); + + expect( + sut.getRandomNumber(), + throwsA( + isA(), + ), + ); + }); + }); +} diff --git a/test/src/features/number/data/datasources/numbers_service/numbers_service_exception_test.dart b/test/src/features/number/data/datasources/numbers_service/numbers_service_exception_test.dart new file mode 100644 index 0000000..6b92ade --- /dev/null +++ b/test/src/features/number/data/datasources/numbers_service/numbers_service_exception_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/features/number/number.dart'; + +void main() { + group('GetNumberException', () { + test('extends from NumbersServiceException', () { + expect( + const GetNumberException('message', 'stackTrace'), + isA(), + ); + }); + }); + + group('GetRandomNumberException', () { + test('extends from NumbersServiceException', () { + expect( + const GetRandomNumberException('message', 'stackTrace'), + isA(), + ); + }); + }); +} diff --git a/test/src/features/number/data/models/number_dto_test.dart b/test/src/features/number/data/models/number_dto_test.dart new file mode 100644 index 0000000..e8eff14 --- /dev/null +++ b/test/src/features/number/data/models/number_dto_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/features/number/number.dart'; + +void main() { + group('NumberDto', () { + test('supports value comparisons', () { + expect( + NumberDto( + info: 'test', + ), + equals( + NumberDto( + info: 'test', + ), + ), + ); + }); + }); +} diff --git a/test/src/features/number/data/repositories/numbers_repository_impl_test.dart b/test/src/features/number/data/repositories/numbers_repository_impl_test.dart new file mode 100644 index 0000000..d02cfe6 --- /dev/null +++ b/test/src/features/number/data/repositories/numbers_repository_impl_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +import '../../../../../helpers/helpers.dart'; + +void main() { + late MockNumbersService mockNumbersService; + late GetNumberParams getNumberParams; + late NumbersRepositoryImpl sut; + + setUpAll( + () { + mockNumbersService = MockNumbersService(); + getNumberParams = const GetNumberParams(number: 0); + sut = NumbersRepositoryImpl(service: mockNumbersService); + }, + ); + + void fakeGetNumberResponse(String? info) { + if (info == null) { + when(() => mockNumbersService.getNumber(params: getNumberParams)) + .thenThrow(const GetNumberException('message', 'stackTrace')); + } else { + when(() => mockNumbersService.getNumber(params: getNumberParams)) + .thenAnswer((_) async => NumberDto(info: info)); + } + } + + void fakeGetRandomNumberResponse(String? info) { + if (info == null) { + when(() => mockNumbersService.getRandomNumber()) + .thenThrow(const GetRandomNumberException('message', 'stackTrace')); + } else { + when(() => mockNumbersService.getRandomNumber()) + .thenAnswer((_) async => NumberDto(info: info)); + } + } + + group( + 'NumbersRepositoryImpl', + () { + test( + 'getNumber returns DataSuccess that contains an info of a number', + () { + const info = 'test_info'; + + fakeGetNumberResponse(info); + + expect( + sut.getNumber(getNumberParams), + completion(const DataSuccess(Number(info: info))), + ); + }, + ); + + test( + 'getRandomNumber returns DataSuccess that contains an info of a random ' + 'number', + () { + const info = 'test_info'; + + fakeGetRandomNumberResponse(info); + + expect( + sut.getRandomNumber(), + completion(const DataSuccess(Number(info: info))), + ); + }, + ); + + test( + 'getNumber returns DataFailure when GetNumberException is thrown', + () { + fakeGetNumberResponse(null); + + expect( + sut.getNumber(getNumberParams), + completion(isA>()), + ); + }, + ); + + test( + 'getRandomNumber returns DataFailure when GetRandomNumberException is ' + 'thrown', + () { + fakeGetRandomNumberResponse(null); + + expect( + sut.getRandomNumber(), + completion(isA>()), + ); + }, + ); + }, + ); +} diff --git a/test/src/features/number/domain/entities/number_test.dart b/test/src/features/number/domain/entities/number_test.dart new file mode 100644 index 0000000..46d793d --- /dev/null +++ b/test/src/features/number/domain/entities/number_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:numbers/src/features/number/number.dart'; + +void main() { + group('Number', () { + test('supports value comparisons', () { + expect( + Number( + info: 'test', + ), + equals( + Number( + info: 'test', + ), + ), + ); + }); + }); +} diff --git a/test/src/features/number/domain/use_cases/get_number_use_case_test.dart b/test/src/features/number/domain/use_cases/get_number_use_case_test.dart new file mode 100644 index 0000000..4e7d99f --- /dev/null +++ b/test/src/features/number/domain/use_cases/get_number_use_case_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +import '../../../../../helpers/helpers.dart'; + +void main() { + late MockNumbersRepository mockNumbersRepository; + late GetNumberParams getNumberParams; + late GetNumberUseCase sut; + + setUpAll(() { + mockNumbersRepository = MockNumbersRepository(); + getNumberParams = const GetNumberParams(number: 0); + sut = GetNumberUseCase(repository: mockNumbersRepository); + }); + + void fakeNumberRepositoryResponse(String? info) { + if (info == null) { + when(() => mockNumbersRepository.getNumber(getNumberParams)).thenAnswer( + (_) async => const DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ); + } else { + when( + () => mockNumbersRepository.getNumber(getNumberParams), + ).thenAnswer((_) async => DataSuccess(Number(info: info))); + } + } + + group( + 'GetNumberUsecase', + () { + test( + 'returns DataSuccess that contains an info of a number', + () { + const info = 'test'; + + fakeNumberRepositoryResponse(info); + + expect( + sut(params: getNumberParams), + completion(const DataSuccess(Number(info: info))), + ); + }, + ); + + test( + 'returns DataFailure when error occurs', + () { + fakeNumberRepositoryResponse(null); + + expect( + sut(params: getNumberParams), + completion(isA>()), + ); + }, + ); + }, + ); +} diff --git a/test/src/features/number/domain/use_cases/get_random_number_use_case_test.dart b/test/src/features/number/domain/use_cases/get_random_number_use_case_test.dart new file mode 100644 index 0000000..53af4d2 --- /dev/null +++ b/test/src/features/number/domain/use_cases/get_random_number_use_case_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +import '../../../../../helpers/helpers.dart'; + +void main() { + late MockNumbersRepository mockNumbersRepository; + late GetRandomNumberUseCase sut; + + setUp(() { + mockNumbersRepository = MockNumbersRepository(); + sut = GetRandomNumberUseCase(repository: mockNumbersRepository); + }); + + void fakeNumberRepositoryResponse(String? info) { + if (info == null) { + when(() => mockNumbersRepository.getRandomNumber()).thenAnswer( + (_) async => const DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ); + } else { + when( + () => mockNumbersRepository.getRandomNumber(), + ).thenAnswer((_) async => DataSuccess(Number(info: info))); + } + } + + group( + 'GetRandomNumberUsecase', + () { + test( + 'returns DataSuccess that contains an info of a number', + () { + const info = 'test'; + + fakeNumberRepositoryResponse(info); + + expect( + sut(), + completion(const DataSuccess(Number(info: info))), + ); + }, + ); + + test( + 'returns DataFailure when error occurs', + () { + fakeNumberRepositoryResponse(null); + + expect( + sut(), + completion(isA>()), + ); + }, + ); + }, + ); +} diff --git a/test/src/features/number/presentation/cubit/number_cubit_test.dart b/test/src/features/number/presentation/cubit/number_cubit_test.dart new file mode 100644 index 0000000..8aca4e1 --- /dev/null +++ b/test/src/features/number/presentation/cubit/number_cubit_test.dart @@ -0,0 +1,125 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:numbers/src/core/core.dart'; +import 'package:numbers/src/features/number/number.dart'; + +import '../../../../../helpers/helpers.dart'; + +void main() { + late MockGetNumberUseCase mockGetNumberUseCase; + late MockGetRandomNumberUseCase mockGetRandomNumberUseCase; + late NumberCubit sut; + + setUp(() { + mockGetNumberUseCase = MockGetNumberUseCase(); + mockGetRandomNumberUseCase = MockGetRandomNumberUseCase(); + sut = NumberCubit( + getNumberUseCase: mockGetNumberUseCase, + getRandomNumberUseCase: mockGetRandomNumberUseCase, + ); + }); + + void fakeGetNumberUseCaseResponse(String? info) { + registerFallbackValue(const GetNumberParams(number: 0)); + + if (info == null) { + when(() => mockGetNumberUseCase(params: any(named: 'params'))).thenAnswer( + (_) async => const DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ); + } else { + when(() => mockGetNumberUseCase(params: any(named: 'params'))).thenAnswer( + (_) async => DataSuccess(Number(info: info)), + ); + } + } + + void fakeGetRandomNumberUseCaseResponse(String? info) { + if (info == null) { + when(() => mockGetRandomNumberUseCase()).thenAnswer( + (_) async => const DataFailure( + ErrorDetails( + error: 'error', + message: 'message', + stackTrace: 'stackTrace', + ), + ), + ); + } else { + when(() => mockGetRandomNumberUseCase()).thenAnswer( + (_) async => DataSuccess(Number(info: info)), + ); + } + } + + group('NumberCubit', () { + test('initial state is correct', () { + expect( + sut.state, + equals(const NumberState.initial()), + ); + }); + + blocTest( + 'emits correct states when getNumberInfo completes with success', + setUp: () => fakeGetNumberUseCaseResponse('test'), + build: () => NumberCubit( + getNumberUseCase: mockGetNumberUseCase, + getRandomNumberUseCase: mockGetRandomNumberUseCase, + ), + act: (cubit) => cubit.getNumberInfo(number: 0), + expect: () => [ + const NumberState.loading(), + const NumberState.loaded(info: 'test'), + ], + ); + + blocTest( + 'emits correct states when getNumberInfo completes with failure', + setUp: () => fakeGetNumberUseCaseResponse(null), + build: () => NumberCubit( + getNumberUseCase: mockGetNumberUseCase, + getRandomNumberUseCase: mockGetRandomNumberUseCase, + ), + act: (cubit) => cubit.getNumberInfo(number: 0), + expect: () => [ + const NumberState.loading(), + const NumberState.error(), + ], + ); + + blocTest( + 'emits correct states when getRandomNumberInfo completes with success', + setUp: () => fakeGetRandomNumberUseCaseResponse('test'), + build: () => NumberCubit( + getNumberUseCase: mockGetNumberUseCase, + getRandomNumberUseCase: mockGetRandomNumberUseCase, + ), + act: (cubit) => cubit.getRandomNumberInfo(), + expect: () => [ + const NumberState.loading(), + const NumberState.loaded(info: 'test'), + ], + ); + + blocTest( + 'emits correct states when getRandomNumberInfo completes with failure', + setUp: () => fakeGetRandomNumberUseCaseResponse(null), + build: () => NumberCubit( + getNumberUseCase: mockGetNumberUseCase, + getRandomNumberUseCase: mockGetRandomNumberUseCase, + ), + act: (cubit) => cubit.getRandomNumberInfo(), + expect: () => [ + const NumberState.loading(), + const NumberState.error(), + ], + ); + }); +}