From 78a968f27f687d67928d29203aaf255851133ff2 Mon Sep 17 00:00:00 2001 From: Federico Correa Date: Thu, 14 Dec 2023 10:07:56 -0300 Subject: [PATCH 01/10] feat: unit testing repositories --- lib/core/di/di_repository_module.dart | 3 +- lib/core/source/auth_remote_source.dart | 2 +- lib/core/source/project_remote_source.dart | 4 +- pubspec.lock | 8 + pubspec.yaml | 21 +-- .../project/project_local_source_test.dart | 111 ++++++++++++ .../project/project_remote_source_test.dart | 57 ++++++ .../project/project_repository_test.dart | 100 +++++++++++ .../session/session_local_source_test.dart | 108 ++++++++++++ .../session/session_remote_source_test.dart | 66 +++++++ .../session/session_repository_test.dart | 164 ++++++++++++++++++ 11 files changed, 630 insertions(+), 14 deletions(-) create mode 100644 test/repositories/project/project_local_source_test.dart create mode 100644 test/repositories/project/project_remote_source_test.dart create mode 100644 test/repositories/project/project_repository_test.dart create mode 100644 test/repositories/session/session_local_source_test.dart create mode 100644 test/repositories/session/session_remote_source_test.dart create mode 100644 test/repositories/session/session_repository_test.dart diff --git a/lib/core/di/di_repository_module.dart b/lib/core/di/di_repository_module.dart index a16ca042..45cfb22a 100644 --- a/lib/core/di/di_repository_module.dart +++ b/lib/core/di/di_repository_module.dart @@ -26,7 +26,8 @@ class RepositoryDiModule { extension _GetItDiModuleExtensions on GetIt { void _setupProvidersAndUtils() { - registerLazySingleton(() => HttpServiceDio([AuthInterceptor(get())])); + registerLazySingleton( + () => HttpServiceDio([AuthInterceptor(get())])); } void _setupRepositories() { diff --git a/lib/core/source/auth_remote_source.dart b/lib/core/source/auth_remote_source.dart index 0045b0d0..47d9740b 100644 --- a/lib/core/source/auth_remote_source.dart +++ b/lib/core/source/auth_remote_source.dart @@ -3,7 +3,7 @@ import 'package:flutter_template/core/model/service/service_response.dart'; import 'package:flutter_template/core/source/common/http_service.dart'; class AuthRemoteSource { - final HttpServiceDio _httpService; + final HttpService _httpService; static const _urlLogin = 'auth/v1/token'; diff --git a/lib/core/source/project_remote_source.dart b/lib/core/source/project_remote_source.dart index 9c6f6b3d..4809f08b 100644 --- a/lib/core/source/project_remote_source.dart +++ b/lib/core/source/project_remote_source.dart @@ -1,11 +1,11 @@ -import 'package:flutter_template/core/model/service/service_response.dart'; import 'package:flutter_template/core/model/project.dart'; +import 'package:flutter_template/core/model/service/service_response.dart'; import 'package:flutter_template/core/source/common/http_service.dart'; class ProjectRemoteSource { static const _urlGetProjects = 'rest/v1/projects?select=*'; - final HttpServiceDio _httpService; + final HttpService _httpService; ProjectRemoteSource(this._httpService); diff --git a/pubspec.lock b/pubspec.lock index 46b2d44c..04c3a182 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -716,6 +716,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: bac151b31e4ed78bd59ab89aa4c0928f297b1180186d5daf03734519e5f596c1 + url: "https://pub.dev" + source: hosted + version: "1.0.1" mutex: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a70544c9..12aadd4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_template description: A new Flutter project. -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.0.0 <4.0.0" flutter: 3.13.9 dependencies: @@ -23,6 +23,7 @@ dependencies: dio: 5.3.2 equatable: 2.0.5 floor: 1.4.2 + mocktail: 1.0.1 flutter_bloc: 8.1.3 flutter_dotenv: 5.1.0 flutter_native_splash: 2.3.2 @@ -79,10 +80,10 @@ flutter_gen: flutter_launcher_icons: android: true ios: true - image_path: 'icons/ic_launcher.png' - image_path_ios: 'icons/ic_launcher_ios.png' # Transparency not supported on IOS - adaptive_icon_foreground: 'icons/ic_launcher_foreground.png' - adaptive_icon_background: '#ee1a64' + image_path: "icons/ic_launcher.png" + image_path_ios: "icons/ic_launcher_ios.png" # Transparency not supported on IOS + adaptive_icon_foreground: "icons/ic_launcher_foreground.png" + adaptive_icon_background: "#ee1a64" remove_alpha_ios: true web: generate: false @@ -90,8 +91,8 @@ flutter_launcher_icons: generate: false flutter_native_splash: - color: '#ffffff' - image: 'icons/splash_logo.png' + color: "#ffffff" + image: "icons/splash_logo.png" android_12: - image: 'icons/splash_logo_android_12.png' - branding: 'icons/splash_branding.png' + image: "icons/splash_logo_android_12.png" + branding: "icons/splash_branding.png" diff --git a/test/repositories/project/project_local_source_test.dart b/test/repositories/project/project_local_source_test.dart new file mode 100644 index 00000000..9ab19550 --- /dev/null +++ b/test/repositories/project/project_local_source_test.dart @@ -0,0 +1,111 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flutter_template/core/model/db/repository_db_entity.dart'; +import 'package:flutter_template/core/source/common/app_database.dart'; +import 'package:flutter_template/core/source/project_local_source.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late AppDatabase database; + late ProjectLocalSource projectDao; + + setUp(() async { + database = await $FloorAppDatabase.inMemoryDatabaseBuilder().build(); + projectDao = database.projectLocalSource; + }); + + tearDown(() async { + await database.close(); + }); + + group('Test projects db', () { + test('Get projects from empty db, should return an empty list', () async { + final projects = await projectDao.getProjects().first; + expect(projects, []); + }); + test('Insert one project to empty db, should return 1 project', () async { + final project = ProjectDbEntity( + id: 1, + name: 'Test projects', + description: 'Test project description', + url: 'test.com', + imageUrl: '', + language: 'ES', + ); + await projectDao.insertProjects([project]); + final projects = await projectDao.getProjects().first; + expect(projects.length, 1); + }); + test('get all projects from db, should return 10', () async { + final projects = Iterable.generate( + 10, + (index) => ProjectDbEntity( + id: index, + name: 'Test $index project', + description: 'Test $index project description', + url: 'test$index.com', + imageUrl: '', + language: 'ES', + ), + ).toList(); + await projectDao.insertProjects(projects); + expect((await projectDao.getProjects().first).length, projects.length); + }); + test('Delete all projects from db, should return empty list', () async { + final projects = Iterable.generate( + 10, + (index) => ProjectDbEntity( + id: index, + name: 'Test $index project', + description: 'Test $index project description', + url: 'test$index.com', + imageUrl: '', + language: 'ES', + ), + ).toList(); + await projectDao.insertProjects(projects); + await projectDao.deleteAllProjects(); + expect(await projectDao.getProjects().first, []); + }); + test('Replace 2 projects from db, should return two new projects', + () async { + final projects = Iterable.generate( + 2, + (index) => ProjectDbEntity( + id: index, + name: 'Test $index project', + description: 'Test $index project description', + url: 'test$index.com', + imageUrl: '', + language: 'ES', + ), + ).toList(); + await projectDao.insertProjects(projects); + final replacement = projects + .map( + (e) => ProjectDbEntity( + id: Random().nextInt(100), + name: 'Test replace project', + description: 'Test project description', + url: 'tes.com', + imageUrl: '', + language: 'ES', + ), + ) + .toList(); + await projectDao.replaceProjects( + replacement, + ); + final list = await projectDao.getProjects().first; + bool result = true; + for (final element in list) { + if (replacement.firstWhereOrNull((e) => e.id == element.id) == null) { + result = false; + break; + } + } + expect(result, true); + }); + }); +} diff --git a/test/repositories/project/project_remote_source_test.dart b/test/repositories/project/project_remote_source_test.dart new file mode 100644 index 00000000..9953fbc5 --- /dev/null +++ b/test/repositories/project/project_remote_source_test.dart @@ -0,0 +1,57 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_template/core/source/common/http_service.dart'; +import 'package:flutter_template/core/source/project_remote_source.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class HttpServiceMock extends Mock implements HttpService {} + +void main() { + late HttpService httpService; + late ProjectRemoteSource projectRemoteSource; + + setUp(() { + httpService = HttpServiceMock(); + projectRemoteSource = ProjectRemoteSource(httpService); + }); + + test('Get projects from API should return one project', () async { + const urlGetProjects = 'rest/v1/projects?select=*'; + final requestOptions = RequestOptions(path: urlGetProjects); + + when(() => httpService.get(urlGetProjects)).thenAnswer( + (_) async => Response( + data: [ + { + 'id': 1, + 'name': 'Test projects', + 'description': 'Test project description', + 'url': 'test.com', + 'image_url': '', + 'language': 'ES', + } + ], + statusCode: 200, + requestOptions: requestOptions, + ), + ); + + final result = await projectRemoteSource.getProjects(); + expect(result.length, 1); + }); + test('Get projects from empty API should return empty', () async { + const urlGetProjects = 'rest/v1/projects?select=*'; + final requestOptions = RequestOptions(path: urlGetProjects); + + when(() => httpService.get(urlGetProjects)).thenAnswer( + (_) async => Response( + data: [], + statusCode: 200, + requestOptions: requestOptions, + ), + ); + + final result = await projectRemoteSource.getProjects(); + expect(result.isEmpty, true); + }); +} diff --git a/test/repositories/project/project_repository_test.dart b/test/repositories/project/project_repository_test.dart new file mode 100644 index 00000000..1daadb75 --- /dev/null +++ b/test/repositories/project/project_repository_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_template/core/model/db/repository_db_entity.dart'; +import 'package:flutter_template/core/model/project.dart'; +import 'package:flutter_template/core/repository/project_repository.dart'; +import 'package:flutter_template/core/source/project_local_source.dart'; +import 'package:flutter_template/core/source/project_remote_source.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class ProjectLocalSourceMock extends Mock implements ProjectLocalSource {} + +class ProjectRemoteSourceMock extends Mock implements ProjectRemoteSource {} + +void main() { + late ProjectLocalSource projectLocalSource; + late ProjectRemoteSource projectRemoteSource; + late ProjectRepository projectRepository; + + setUp(() async { + projectRepository = ProjectRepository( + projectLocalSource = ProjectLocalSourceMock(), + projectRemoteSource = ProjectRemoteSourceMock(), + ); + }); + + test('get projects from empty state', () async { + when(() => projectLocalSource.getProjects()).thenAnswer( + (_) => Stream.value([]), + ); + when(() => projectRemoteSource.getProjects()).thenAnswer( + (_) async => [], + ); + //Default case for creating the repository + when(() => projectLocalSource.replaceProjects(any())) + .thenAnswer((_) async {}); + + final projects = await projectRepository.getProjects().first; + + verify(() => projectLocalSource.getProjects()).called(1); + verify(() => projectRemoteSource.getProjects()).called(1); + verify(() => projectLocalSource.replaceProjects(any())).called(1); + + expect(projects, []); + }); + + test( + 'get projects stream from a loaded state, should return a stream that gives 2 values', + () async { + when(() => projectLocalSource.getProjects()).thenAnswer( + (_) => Stream.value( + [ + ProjectDbEntity( + id: 1, + name: 'Test projects', + description: 'Test project description', + url: 'test.com', + imageUrl: '', + language: 'ES', + ), + ProjectDbEntity( + id: 2, + name: 'Test projects', + description: 'Test project description', + url: 'test.com', + imageUrl: '', + language: 'ES', + ), + ], + ), + ); + when(() => projectLocalSource.replaceProjects(any())) + .thenAnswer((_) async {}); + when(() => projectRemoteSource.getProjects()).thenAnswer( + (_) async => [ + Project( + id: 1, + name: 'Test projects', + description: 'Test project description', + url: 'test.com', + imageUrl: '', + language: 'ES', + ), + Project( + id: 2, + name: 'Test projects', + description: 'Test project description', + url: 'test.com', + imageUrl: '', + language: 'ES', + ), + ], + ); + + final list = await projectRepository.getProjects().first; + verify(() => projectLocalSource.getProjects()).called(1); + verify(() => projectRemoteSource.getProjects()).called(1); + verify(() => projectLocalSource.replaceProjects(any())).called(1); + expect(list?.length, 2); + }, + ); +} diff --git a/test/repositories/session/session_local_source_test.dart b/test/repositories/session/session_local_source_test.dart new file mode 100644 index 00000000..83d02362 --- /dev/null +++ b/test/repositories/session/session_local_source_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter_template/core/model/user.dart'; +import 'package:flutter_template/core/source/auth_local_source.dart'; +import 'package:flutter_template/core/source/common/local_shared_preferences_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class LocalSharedPreferencesStorageMock extends Mock + implements LocalSharedPreferencesStorage {} + +void main() { + late LocalSharedPreferencesStorageMock storage; + late AuthLocalSource authLocalSource; + + setUp(() { + storage = LocalSharedPreferencesStorageMock(); + when(() => storage.init()).thenAnswer((_) => any()); + + authLocalSource = AuthLocalSource(storage); + }); + + group('getUserToken tests', () { + test('getUserToken when local Storage is empty, should return null', + () async { + when(() => storage.read(key: 'AuthLocalSource.token')) + .thenAnswer((_) => Future.value(null)); + expect(await authLocalSource.getUserToken().first, null); + }); + + test('getUserToken when local Storage has data, should return String token', + () async { + when(() => storage.read(key: 'AuthLocalSource.token')) + .thenAnswer((_) => Future.value('123abc')); + expect(await authLocalSource.getUserToken().first, '123abc'); + }); + }); + + test( + 'saveUserToken "123abc" when local Storage is empty, write method should be called once', + () async { + when(() => storage.write(key: 'AuthLocalSource.token', value: '123abc')) + .thenAnswer((_) => Future(() => null)); + await authLocalSource.saveUserToken('123abc'); + verify( + () => storage.write( + key: 'AuthLocalSource.token', + value: '123abc', + ), + ); + }); + + test('getUser when local storage is empty should return empty', () async { + when(() => storage.read(key: 'AuthLocalSource.user')) + .thenAnswer((_) => Future(() => null)); + expect(await authLocalSource.getUser().first, null); + verify(() => storage.read(key: 'AuthLocalSource.user')).called(1); + }); + + test('getUser when local storage has data, should return a user', () async { + when(() => storage.read(key: 'AuthLocalSource.user')).thenAnswer( + (_) => Future( + () => '{"name": "Test user","email": "test@email.com"}', + ), + ); + expect((await authLocalSource.getUser().first)?.email, 'test@email.com'); + verify(() => storage.read(key: 'AuthLocalSource.user')).called(1); + }); + + test( + 'saveUserInfo with null in local storage,' + ' should call write once and save null', () async { + when( + () => storage.write( + key: 'AuthLocalSource.user', + value: null, + ), + ).thenAnswer((_) => Future(() => null)); + + await authLocalSource.saveUserInfo( + null, + ); + verify(() => storage.write(key: 'AuthLocalSource.user', value: null)) + .called(1); + }); + + test( + 'saveUserInfo when local storage is empty,' + ' should call write once', () async { + when( + () => storage.write( + key: 'AuthLocalSource.user', + value: any(named: 'value'), + ), + ).thenAnswer((_) => Future(() => null)); + + await authLocalSource.saveUserInfo( + User( + name: 'Test user', + email: 'test@email.com', + ), + ); + verify( + () => storage.write( + key: 'AuthLocalSource.user', + value: any(named: 'value'), + ), + ).called(1); + }); +} diff --git a/test/repositories/session/session_remote_source_test.dart b/test/repositories/session/session_remote_source_test.dart new file mode 100644 index 00000000..2160d448 --- /dev/null +++ b/test/repositories/session/session_remote_source_test.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_template/core/model/service/auth_models.dart'; +import 'package:flutter_template/core/model/user.dart'; +import 'package:flutter_template/core/source/auth_remote_source.dart'; +import 'package:flutter_template/core/source/common/http_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class HttpServiceMock extends Mock implements HttpService {} + +void main() { + late HttpService httpService; + late AuthRemoteSource authRemoteSource; + + setUp(() { + httpService = HttpServiceMock(); + authRemoteSource = AuthRemoteSource(httpService); + }); + + test('signIn call to API with valid values should return a token', () async { + const uri = 'auth/v1/token'; + final requestOptions = RequestOptions(path: uri); + + when( + () => httpService.post( + uri, + queryParameters: {'grant_type': 'password'}, + data: SignInRequest(email: 'test@email.com', password: '1234').toJson(), + ), + ).thenAnswer( + (_) async => Response( + data: SignInResponse( + accessToken: 'abc123', + user: User(email: 'test@email.com', name: 'test'), + ).toJson(), + statusCode: 200, + requestOptions: requestOptions, + ), + ); + + final result = await authRemoteSource.signIn('test@email.com', '1234'); + expect(result.accessToken, 'abc123'); + }); + + test('signIn call to API with invalid values should return a exception', + () async { + const uri = 'auth/v1/token'; + + when( + () => httpService.post( + uri, + queryParameters: {'grant_type': 'password'}, + data: any(named: 'data'), + ), + ).thenThrow( + const HttpException(''), + ); + + expect( + () => authRemoteSource.signIn('test@email.com', '1224'), + throwsA(isA()), + ); + }); +} diff --git a/test/repositories/session/session_repository_test.dart b/test/repositories/session/session_repository_test.dart new file mode 100644 index 00000000..e0ce7c40 --- /dev/null +++ b/test/repositories/session/session_repository_test.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:flutter_template/core/model/authentication_status.dart'; +import 'package:flutter_template/core/model/service/auth_models.dart'; +import 'package:flutter_template/core/model/user.dart'; +import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/core/source/auth_local_source.dart'; +import 'package:flutter_template/core/source/auth_remote_source.dart'; +import 'package:flutter_template/core/source/common/app_database.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class AuthLocalSourceMock extends Mock implements AuthLocalSource {} + +class AuthRemoteSourceMock extends Mock implements AuthRemoteSource {} + +void main() { + late AuthLocalSource authLocalSource; + late AuthRemoteSource authRemoteSource; + late AppDatabase appDatabase; + late SessionRepository sessionRepository; + + setUp(() async { + authLocalSource = AuthLocalSourceMock(); + authRemoteSource = AuthRemoteSourceMock(); + appDatabase = await $FloorAppDatabase.inMemoryDatabaseBuilder().build(); + sessionRepository = + SessionRepository(appDatabase, authLocalSource, authRemoteSource); + }); + tearDown(() async { + await appDatabase.close(); + }); + + group('status stream tests', () { + test( + 'Get Authenticated status from repository with token stored, should return Authenticated', + () async { + when(() => authLocalSource.getUserToken()).thenAnswer( + (_) => Stream.value('123abc'), + ); + expect( + await sessionRepository.status.first, + AuthenticationStatus.authenticated, + ); + }, + ); + test( + 'Get Unauthenticated status from repository with null token stored, should return Unauthenticated', + () async { + when(() => authLocalSource.getUserToken()).thenAnswer( + (_) => Stream.value(null), + ); + expect( + await sessionRepository.status.first, + AuthenticationStatus.unauthenticated, + ); + }, + ); + }); + + group('getUserInfo tests', () { + test('getUserInfo when localSource is empty, should return null', () async { + when(() => authLocalSource.getUser()) + .thenAnswer((_) => Stream.value(null)); + expect(await sessionRepository.getUserInfo().first, null); + }); + test('getUserInfo when localSource has data, should return a User', + () async { + when(() => authLocalSource.getUser()).thenAnswer( + (_) => Stream.value(User(email: 'test@email', name: 'Test')), + ); + expect( + (await sessionRepository.getUserInfo().first)?.email, + 'test@email', + ); + }); + }); + group('signInUser tests', () { + test( + 'signInUser when the data is valid should consume the remote source and call saveUserToken and saveUserInfo from local source', + () async { + when( + () => authRemoteSource.signIn( + 'test@email.com', + '123456', + ), + ).thenAnswer( + (_) async => SignInResponse( + accessToken: '123abc', + user: User(email: 'test@email.com', name: 'Test'), + ), + ); + + when( + () => authLocalSource.saveUserInfo( + User(email: 'test@email.com', name: 'Test'), + ), + ).thenAnswer((_) async {}); + + when(() => authLocalSource.saveUserToken('123abc')) + .thenAnswer((_) async {}); + + await sessionRepository.signInUser( + email: 'test@email.com', + password: '123456', + ); + verify( + () => authRemoteSource.signIn('test@email.com', '123456'), + ).called(1); + verify( + () => authLocalSource + .saveUserInfo(User(email: 'test@email.com', name: 'Test')), + ).called(1); + verify( + () => authLocalSource.saveUserToken('123abc'), + ).called(1); + }); + + test( + 'signInUser when the data is invalid should throw an exception and not call saveUserToken and saveUserInfo from local source', + () async { + when( + () => authRemoteSource.signIn( + any(), + any(), + ), + ).thenThrow( + const HttpException(''), + ); + + when( + () => authLocalSource.saveUserInfo( + any(), + ), + ).thenAnswer((_) async {}); + + when(() => authLocalSource.saveUserToken(any())).thenAnswer((_) async {}); + + expect( + () => sessionRepository.signInUser( + email: 'test', + password: '123456', + ), + throwsA(isA()), + ); + verifyNever( + () => authLocalSource.saveUserInfo(any()), + ); + verifyNever( + () => authLocalSource.saveUserToken(any()), + ); + }); + }); + + test('logOut when there is data stored locally, should clean all data', + () async { + when(() => authLocalSource.saveUserToken(null)).thenAnswer((_) async {}); + when(() => authLocalSource.saveUserInfo(null)).thenAnswer((_) async {}); + + await sessionRepository.logOut(); + verify(() => authLocalSource.saveUserToken(null)).called(1); + verify(() => authLocalSource.saveUserInfo(null)).called(1); + }); +} From 5c15e02f8c6b442f1876af11b864152e110d60dd Mon Sep 17 00:00:00 2001 From: Federico Correa Date: Thu, 14 Dec 2023 14:28:53 -0300 Subject: [PATCH 02/10] feat: fix comments and add cubit tests --- .github/hooks/pre-push | 3 + analysis_options.yaml | 14 ++- lib/core/di/di_repository_module.dart | 3 +- lib/core/model/db/repository_db_entity.dart | 15 ++- pubspec.lock | 98 +++++++++++++++++- pubspec.yaml | 3 +- test/common/general_helpers.dart | 4 + test/common/mocks.dart | 35 +++++++ test/common/project_helpers.dart | 26 +++++ test/cubits/signin_cubit_test.dart | 85 ++++++++++++++++ test/cubits/welcome_cubit_test.dart | 99 +++++++++++++++++++ .../project/project_local_source_test.dart | 70 +++---------- .../project/project_remote_source_test.dart | 18 +--- .../project/project_repository_test.dart | 53 +++------- .../session/session_local_source_test.dart | 6 +- .../session/session_remote_source_test.dart | 2 +- .../session/session_repository_test.dart | 9 +- 17 files changed, 412 insertions(+), 131 deletions(-) create mode 100644 test/common/general_helpers.dart create mode 100644 test/common/mocks.dart create mode 100644 test/common/project_helpers.dart create mode 100644 test/cubits/signin_cubit_test.dart create mode 100644 test/cubits/welcome_cubit_test.dart diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 9a037673..abc91f17 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -2,3 +2,6 @@ printf "\e[33;1m%s\e[0m\n" 'Running the Pre-push checks' ./scripts/checks.sh + +printf "\e[33;1m%s\e[0m\n" 'Running the Pre-push tests' +fvm flutter test diff --git a/analysis_options.yaml b/analysis_options.yaml index b6e00af1..fc5b6c48 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -112,18 +112,16 @@ dart_code_metrics: allowed-duplicated-chains: 3 - prefer-trailing-comma - - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options analyzer: exclude: - - '**/*.freezed.dart' - - '**/*.g.dart' - - '**/*.gen.dart' - - '**/*.gr.dart' - - 'bricks' - - 'lib/generated_plugin_registrant.dart' + - "**/*.freezed.dart" + - "**/*.g.dart" + - "**/*.gen.dart" + - "**/*.gr.dart" + - "bricks" + - "lib/generated_plugin_registrant.dart" errors: invalid_annotation_target: ignore unused_element: ignore # https://github.com/dart-lang/sdk/issues/49025 diff --git a/lib/core/di/di_repository_module.dart b/lib/core/di/di_repository_module.dart index 45cfb22a..a5ac5fa5 100644 --- a/lib/core/di/di_repository_module.dart +++ b/lib/core/di/di_repository_module.dart @@ -27,7 +27,8 @@ class RepositoryDiModule { extension _GetItDiModuleExtensions on GetIt { void _setupProvidersAndUtils() { registerLazySingleton( - () => HttpServiceDio([AuthInterceptor(get())])); + () => HttpServiceDio([AuthInterceptor(get())]), + ); } void _setupRepositories() { diff --git a/lib/core/model/db/repository_db_entity.dart b/lib/core/model/db/repository_db_entity.dart index c82b94be..9ce52f9a 100644 --- a/lib/core/model/db/repository_db_entity.dart +++ b/lib/core/model/db/repository_db_entity.dart @@ -1,7 +1,8 @@ +import 'package:equatable/equatable.dart'; import 'package:floor/floor.dart'; @Entity(tableName: 'projects') -class ProjectDbEntity { +class ProjectDbEntity extends Equatable { @primaryKey final int id; final String name; @@ -10,7 +11,7 @@ class ProjectDbEntity { final String imageUrl; final String language; - ProjectDbEntity({ + const ProjectDbEntity({ required this.id, required this.name, required this.description, @@ -18,4 +19,14 @@ class ProjectDbEntity { required this.imageUrl, required this.language, }); + + @override + List get props => [ + id, + name, + description, + url, + imageUrl, + language, + ]; } diff --git a/pubspec.lock b/pubspec.lock index 04c3a182..7ede8961 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "02f04270be5abae8df171143e61a0058a7acbce5dcac887612e89bb40cca4c33" + url: "https://pub.dev" + source: hosted + version: "9.1.5" boolean_selector: dependency: transitive description: @@ -232,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + url: "https://pub.dev" + source: hosted + version: "1.6.4" crypto: dependency: transitive description: @@ -288,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -717,7 +741,7 @@ packages: source: hosted version: "1.0.4" mocktail: - dependency: "direct main" + dependency: "direct dev" description: name: mocktail sha256: bac151b31e4ed78bd59ab89aa4c0928f297b1180186d5daf03734519e5f596c1 @@ -740,6 +764,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -956,6 +988,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -985,6 +1033,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -1105,6 +1169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" + source: hosted + version: "1.24.3" test_api: dependency: transitive description: @@ -1113,6 +1185,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + url: "https://pub.dev" + source: hosted + version: "0.5.3" time: dependency: transitive description: @@ -1169,6 +1249,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: @@ -1193,6 +1281,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 12aadd4d..0863fb4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: dio: 5.3.2 equatable: 2.0.5 floor: 1.4.2 - mocktail: 1.0.1 flutter_bloc: 8.1.3 flutter_dotenv: 5.1.0 flutter_native_splash: 2.3.2 @@ -53,6 +52,8 @@ dev_dependencies: auto_route_generator: 7.3.1 build_runner: 2.4.6 + mocktail: 1.0.1 + bloc_test: 9.1.5 dart_code_metrics: 5.7.6 floor_generator: 1.4.2 flutter_flavorizr: 2.2.1 diff --git a/test/common/general_helpers.dart b/test/common/general_helpers.dart new file mode 100644 index 00000000..491fac6d --- /dev/null +++ b/test/common/general_helpers.dart @@ -0,0 +1,4 @@ +import 'package:flutter_template/core/source/common/app_database.dart'; + +Future setupFloorDatabase() async => + $FloorAppDatabase.inMemoryDatabaseBuilder().build(); diff --git a/test/common/mocks.dart b/test/common/mocks.dart new file mode 100644 index 00000000..82e81c81 --- /dev/null +++ b/test/common/mocks.dart @@ -0,0 +1,35 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/core/repository/project_repository.dart'; +import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/core/source/auth_local_source.dart'; +import 'package:flutter_template/core/source/auth_remote_source.dart'; +import 'package:flutter_template/core/source/common/http_service.dart'; +import 'package:flutter_template/core/source/common/local_shared_preferences_storage.dart'; +import 'package:flutter_template/core/source/project_local_source.dart'; +import 'package:flutter_template/core/source/project_remote_source.dart'; +import 'package:flutter_template/ui/section/error_handler/global_event_handler_cubit.dart'; +import 'package:mocktail/mocktail.dart'; + +//* Services +class HttpServiceMock extends Mock implements HttpService {} + +//* Data sources +class ProjectLocalSourceMock extends Mock implements ProjectLocalSource {} + +class ProjectRemoteSourceMock extends Mock implements ProjectRemoteSource {} + +class AuthLocalSourceMock extends Mock implements AuthLocalSource {} + +class AuthRemoteSourceMock extends Mock implements AuthRemoteSource {} + +//* Repositories +class MockSessionRepository extends Mock implements SessionRepository {} + +class MockProjectRepository extends Mock implements ProjectRepository {} + +//* Cubits +class MockGlobalEventHandlerCubit extends MockCubit + implements GlobalEventHandlerCubit {} + +class LocalSharedPreferencesStorageMock extends Mock + implements LocalSharedPreferencesStorage {} diff --git a/test/common/project_helpers.dart b/test/common/project_helpers.dart new file mode 100644 index 00000000..e7536e45 --- /dev/null +++ b/test/common/project_helpers.dart @@ -0,0 +1,26 @@ +import 'package:flutter_template/core/model/db/repository_db_entity.dart'; +import 'package:flutter_template/core/model/project.dart'; + +List generateProjectDbEntities(int count) => Iterable.generate( + count, + (index) => ProjectDbEntity( + id: index, + name: 'Test $index project', + description: 'Test $index project description', + url: 'test$index.com', + imageUrl: '', + language: 'ES', + ), + ).toList(); + +List generateProjects(int count) => Iterable.generate( + count, + (index) => Project( + id: index, + name: 'Test $index project', + description: 'Test $index project description', + url: 'test$index.com', + imageUrl: '', + language: 'ES', + ), + ).toList(); diff --git a/test/cubits/signin_cubit_test.dart b/test/cubits/signin_cubit_test.dart new file mode 100644 index 00000000..3488e00a --- /dev/null +++ b/test/cubits/signin_cubit_test.dart @@ -0,0 +1,85 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/ui/signin/signin_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../common/mocks.dart'; + +void main() { + late GetIt getIt; + late SessionRepository sessionRepository; + late SignInCubit signInCubit; + + setUp(() { + getIt = GetIt.instance + ..registerSingleton( + sessionRepository = MockSessionRepository(), + ); + }); + tearDown(() => getIt.reset()); + + test('Create SignInCubit should return base state', () { + signInCubit = SignInCubit(MockGlobalEventHandlerCubit()); + + expect( + signInCubit.state, + equals( + const SignInBaseState.state( + email: 'hi@xmartlabs.com', + password: 'xmartlabs', + error: '', + ), + ), + ); + }); + + blocTest( + 'SignInCubit change email should return state with new email', + build: () => signInCubit = SignInCubit(MockGlobalEventHandlerCubit()), + act: (bloc) => bloc.changeEmail('hitest@xmartlabs.com'), + expect: () => [ + const SignInBaseState.state( + email: 'hitest@xmartlabs.com', + password: 'xmartlabs', + error: '', + ), + ], + ); + + blocTest( + 'SignInCubit change password should return state with new password', + build: () => signInCubit = SignInCubit(MockGlobalEventHandlerCubit()), + act: (bloc) => bloc.changePassword('xmartlabs123'), + expect: () => [ + const SignInBaseState.state( + email: 'hi@xmartlabs.com', + password: 'xmartlabs123', + error: '', + ), + ], + ); + + blocTest( + 'SignInCubit signIn method should call signInUser in repository', + setUp: () { + when( + () => sessionRepository.signInUser( + email: 'hi@xmartlabs.com', + password: 'xmartlabs', + ), + ).thenAnswer((_) async {}); + }, + build: () => signInCubit = SignInCubit(MockGlobalEventHandlerCubit()), + act: (bloc) => bloc.signIn(), + verify: (_) { + verify( + () => sessionRepository.signInUser( + email: 'hi@xmartlabs.com', + password: 'xmartlabs', + ), + ).called(1); + }, + ); +} diff --git a/test/cubits/welcome_cubit_test.dart b/test/cubits/welcome_cubit_test.dart new file mode 100644 index 00000000..879674f3 --- /dev/null +++ b/test/cubits/welcome_cubit_test.dart @@ -0,0 +1,99 @@ +// ignore_for_file: lines_longer_than_80_chars + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/core/repository/project_repository.dart'; +import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/ui/welcome/welcome_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../common/mocks.dart'; +import '../common/project_helpers.dart'; + +void main() { + // ignore: unused_local_variable + late GetIt getIt; + late SessionRepository sessionRepository; + late ProjectRepository projectRepository; + late WelcomeCubit welcomeCubit; + + setUp(() { + getIt = GetIt.instance + ..registerSingleton( + sessionRepository = MockSessionRepository(), + ) + ..registerSingleton( + projectRepository = MockProjectRepository(), + ); + }); + + tearDown(() => getIt.reset()); + test( + 'Create Welcome cubit, should return a WelcomeCubit with base state', + () { + when(() => projectRepository.getProjects()).thenAnswer( + (_) => Stream.value([]), + ); + welcomeCubit = WelcomeCubit(MockGlobalEventHandlerCubit()); + + expect(welcomeCubit.state, equals(const WelcomeBaseState.state())); + }, + ); + + group('Welcome cubit with loaded projects tests', () { + blocTest( + 'Welcome cubit is created when project repository has data, should have' + ' projects in state', + setUp: () { + when(() => projectRepository.getProjects()).thenAnswer( + (_) => Stream.value(generateProjects(1)), + ); + }, + build: () => welcomeCubit = WelcomeCubit(MockGlobalEventHandlerCubit()), + expect: () => [ + WelcomeBaseState.state( + projects: generateProjects(1), + ), + ], + ); + + blocTest( + 'Welcome cubit is created when project repository is empty, project ' + 'repository stream emits new values then WelcomeCubit should emit a new ' + 'state with projects', + setUp: () { + when(() => projectRepository.getProjects()).thenAnswer( + (_) => Stream.fromIterable([ + generateProjects(1), + generateProjects(2), + ]), + ); + }, + build: () => welcomeCubit = WelcomeCubit(MockGlobalEventHandlerCubit()), + expect: () => [ + WelcomeBaseState.state( + projects: generateProjects(1), + ), + WelcomeBaseState.state( + projects: generateProjects(2), + ), + ], + ); + + blocTest( + 'Welcome cubit calls logOut from sessionRepository when the screen calls logOut method from cubit', + build: () => welcomeCubit = WelcomeCubit(MockGlobalEventHandlerCubit()), + setUp: () { + when(() => sessionRepository.logOut()).thenAnswer( + (_) async {}, + ); + when(() => projectRepository.getProjects()).thenAnswer( + (_) => Stream.value([]), + ); + }, + act: (cubit) => cubit.logOut(), + verify: (_) => verify(() => sessionRepository.logOut()).called(1), + ); + }); +} diff --git a/test/repositories/project/project_local_source_test.dart b/test/repositories/project/project_local_source_test.dart index 9ab19550..0c95b564 100644 --- a/test/repositories/project/project_local_source_test.dart +++ b/test/repositories/project/project_local_source_test.dart @@ -1,17 +1,19 @@ import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:flutter_template/core/model/db/repository_db_entity.dart'; import 'package:flutter_template/core/source/common/app_database.dart'; import 'package:flutter_template/core/source/project_local_source.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../../common/general_helpers.dart'; +import '../../common/project_helpers.dart'; + void main() { late AppDatabase database; late ProjectLocalSource projectDao; setUp(() async { - database = await $FloorAppDatabase.inMemoryDatabaseBuilder().build(); + database = await setupFloorDatabase(); projectDao = database.projectLocalSource; }); @@ -20,67 +22,32 @@ void main() { }); group('Test projects db', () { - test('Get projects from empty db, should return an empty list', () async { + test('Get projects from empty db, should return an empty list', () async { final projects = await projectDao.getProjects().first; expect(projects, []); }); test('Insert one project to empty db, should return 1 project', () async { - final project = ProjectDbEntity( - id: 1, - name: 'Test projects', - description: 'Test project description', - url: 'test.com', - imageUrl: '', - language: 'ES', + final projects = generateProjectDbEntities(1); + await projectDao.insertProjects(projects); + expect( + await projectDao.getProjects().first, + projects, ); - await projectDao.insertProjects([project]); - final projects = await projectDao.getProjects().first; - expect(projects.length, 1); }); test('get all projects from db, should return 10', () async { - final projects = Iterable.generate( - 10, - (index) => ProjectDbEntity( - id: index, - name: 'Test $index project', - description: 'Test $index project description', - url: 'test$index.com', - imageUrl: '', - language: 'ES', - ), - ).toList(); + final projects = generateProjectDbEntities(10); await projectDao.insertProjects(projects); - expect((await projectDao.getProjects().first).length, projects.length); + expect(await projectDao.getProjects().first, projects); }); test('Delete all projects from db, should return empty list', () async { - final projects = Iterable.generate( - 10, - (index) => ProjectDbEntity( - id: index, - name: 'Test $index project', - description: 'Test $index project description', - url: 'test$index.com', - imageUrl: '', - language: 'ES', - ), - ).toList(); + final projects = generateProjectDbEntities(10); await projectDao.insertProjects(projects); await projectDao.deleteAllProjects(); expect(await projectDao.getProjects().first, []); }); test('Replace 2 projects from db, should return two new projects', () async { - final projects = Iterable.generate( - 2, - (index) => ProjectDbEntity( - id: index, - name: 'Test $index project', - description: 'Test $index project description', - url: 'test$index.com', - imageUrl: '', - language: 'ES', - ), - ).toList(); + final projects = generateProjectDbEntities(2); await projectDao.insertProjects(projects); final replacement = projects .map( @@ -98,14 +65,7 @@ void main() { replacement, ); final list = await projectDao.getProjects().first; - bool result = true; - for (final element in list) { - if (replacement.firstWhereOrNull((e) => e.id == element.id) == null) { - result = false; - break; - } - } - expect(result, true); + expect(list, replacement); }); }); } diff --git a/test/repositories/project/project_remote_source_test.dart b/test/repositories/project/project_remote_source_test.dart index 9953fbc5..aeaaec0c 100644 --- a/test/repositories/project/project_remote_source_test.dart +++ b/test/repositories/project/project_remote_source_test.dart @@ -4,7 +4,8 @@ import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class HttpServiceMock extends Mock implements HttpService {} +import '../../common/mocks.dart'; +import '../../common/project_helpers.dart'; void main() { late HttpService httpService; @@ -18,26 +19,17 @@ void main() { test('Get projects from API should return one project', () async { const urlGetProjects = 'rest/v1/projects?select=*'; final requestOptions = RequestOptions(path: urlGetProjects); - + final projects = generateProjects(1); when(() => httpService.get(urlGetProjects)).thenAnswer( (_) async => Response( - data: [ - { - 'id': 1, - 'name': 'Test projects', - 'description': 'Test project description', - 'url': 'test.com', - 'image_url': '', - 'language': 'ES', - } - ], + data: projects.map((e) => e.toJson()).toList(), statusCode: 200, requestOptions: requestOptions, ), ); final result = await projectRemoteSource.getProjects(); - expect(result.length, 1); + expect(result, projects); }); test('Get projects from empty API should return empty', () async { const urlGetProjects = 'rest/v1/projects?select=*'; diff --git a/test/repositories/project/project_repository_test.dart b/test/repositories/project/project_repository_test.dart index 1daadb75..a678653f 100644 --- a/test/repositories/project/project_repository_test.dart +++ b/test/repositories/project/project_repository_test.dart @@ -1,14 +1,13 @@ -import 'package:flutter_template/core/model/db/repository_db_entity.dart'; -import 'package:flutter_template/core/model/project.dart'; +// ignore_for_file: lines_longer_than_80_chars + import 'package:flutter_template/core/repository/project_repository.dart'; import 'package:flutter_template/core/source/project_local_source.dart'; import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class ProjectLocalSourceMock extends Mock implements ProjectLocalSource {} - -class ProjectRemoteSourceMock extends Mock implements ProjectRemoteSource {} +import '../../common/mocks.dart'; +import '../../common/project_helpers.dart'; void main() { late ProjectLocalSource projectLocalSource; @@ -45,56 +44,26 @@ void main() { test( 'get projects stream from a loaded state, should return a stream that gives 2 values', () async { + final projects = generateProjects(2); when(() => projectLocalSource.getProjects()).thenAnswer( (_) => Stream.value( - [ - ProjectDbEntity( - id: 1, - name: 'Test projects', - description: 'Test project description', - url: 'test.com', - imageUrl: '', - language: 'ES', - ), - ProjectDbEntity( - id: 2, - name: 'Test projects', - description: 'Test project description', - url: 'test.com', - imageUrl: '', - language: 'ES', - ), - ], + generateProjectDbEntities(2), ), ); when(() => projectLocalSource.replaceProjects(any())) .thenAnswer((_) async {}); when(() => projectRemoteSource.getProjects()).thenAnswer( - (_) async => [ - Project( - id: 1, - name: 'Test projects', - description: 'Test project description', - url: 'test.com', - imageUrl: '', - language: 'ES', - ), - Project( - id: 2, - name: 'Test projects', - description: 'Test project description', - url: 'test.com', - imageUrl: '', - language: 'ES', - ), - ], + (_) async => generateProjects(2), ); final list = await projectRepository.getProjects().first; verify(() => projectLocalSource.getProjects()).called(1); verify(() => projectRemoteSource.getProjects()).called(1); verify(() => projectLocalSource.replaceProjects(any())).called(1); - expect(list?.length, 2); + expect( + list, + projects, + ); }, ); } diff --git a/test/repositories/session/session_local_source_test.dart b/test/repositories/session/session_local_source_test.dart index 83d02362..62745ae9 100644 --- a/test/repositories/session/session_local_source_test.dart +++ b/test/repositories/session/session_local_source_test.dart @@ -1,11 +1,11 @@ +// ignore_for_file: lines_longer_than_80_chars + import 'package:flutter_template/core/model/user.dart'; import 'package:flutter_template/core/source/auth_local_source.dart'; -import 'package:flutter_template/core/source/common/local_shared_preferences_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class LocalSharedPreferencesStorageMock extends Mock - implements LocalSharedPreferencesStorage {} +import '../../common/mocks.dart'; void main() { late LocalSharedPreferencesStorageMock storage; diff --git a/test/repositories/session/session_remote_source_test.dart b/test/repositories/session/session_remote_source_test.dart index 2160d448..7274cf40 100644 --- a/test/repositories/session/session_remote_source_test.dart +++ b/test/repositories/session/session_remote_source_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_template/core/source/common/http_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class HttpServiceMock extends Mock implements HttpService {} +import '../../common/mocks.dart'; void main() { late HttpService httpService; diff --git a/test/repositories/session/session_repository_test.dart b/test/repositories/session/session_repository_test.dart index e0ce7c40..147cda71 100644 --- a/test/repositories/session/session_repository_test.dart +++ b/test/repositories/session/session_repository_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + import 'dart:io'; import 'package:flutter_template/core/model/authentication_status.dart'; @@ -10,9 +12,8 @@ import 'package:flutter_template/core/source/common/app_database.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class AuthLocalSourceMock extends Mock implements AuthLocalSource {} - -class AuthRemoteSourceMock extends Mock implements AuthRemoteSource {} +import '../../common/general_helpers.dart'; +import '../../common/mocks.dart'; void main() { late AuthLocalSource authLocalSource; @@ -23,7 +24,7 @@ void main() { setUp(() async { authLocalSource = AuthLocalSourceMock(); authRemoteSource = AuthRemoteSourceMock(); - appDatabase = await $FloorAppDatabase.inMemoryDatabaseBuilder().build(); + appDatabase = await setupFloorDatabase(); sessionRepository = SessionRepository(appDatabase, authLocalSource, authRemoteSource); }); From 89eb5725967674694dc918ec25135ed7cd1b2285 Mon Sep 17 00:00:00 2001 From: Federico Correa Date: Tue, 19 Dec 2023 09:37:30 -0300 Subject: [PATCH 03/10] fix: PR comments --- pubspec.lock | 4 ++-- test/common/general_helpers.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7ede8961..479f3f6b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -888,10 +888,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: diff --git a/test/common/general_helpers.dart b/test/common/general_helpers.dart index 491fac6d..31ba20b7 100644 --- a/test/common/general_helpers.dart +++ b/test/common/general_helpers.dart @@ -1,4 +1,4 @@ import 'package:flutter_template/core/source/common/app_database.dart'; -Future setupFloorDatabase() async => +Future setupFloorDatabase() => $FloorAppDatabase.inMemoryDatabaseBuilder().build(); From 1167ea2cc543079066f577195ffc1bcadc15b9b6 Mon Sep 17 00:00:00 2001 From: Federico Correa Date: Tue, 19 Dec 2023 13:10:24 -0300 Subject: [PATCH 04/10] fix: PR Comments --- lib/core/di/di_provider.dart | 14 ++++++++------ pubspec.yaml | 2 +- test/common/cubit_mocks.dart | 5 +++++ test/common/{mocks.dart => data_mocks.dart} | 12 +++--------- test/common/general_helpers.dart | 7 +++++++ test/common/project_helpers.dart | 4 ++-- test/cubits/signin_cubit_test.dart | 3 ++- test/cubits/welcome_cubit_test.dart | 7 ++++--- .../project/project_local_source_test.dart | 6 ++---- .../project/project_remote_source_test.dart | 15 ++++----------- .../project/project_repository_test.dart | 2 +- .../session/session_local_source_test.dart | 2 +- .../session/session_remote_source_test.dart | 2 +- .../session/session_repository_test.dart | 2 +- 14 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 test/common/cubit_mocks.dart rename test/common/{mocks.dart => data_mocks.dart} (82%) diff --git a/lib/core/di/di_provider.dart b/lib/core/di/di_provider.dart index 92c6d4c6..de85d0f5 100644 --- a/lib/core/di/di_provider.dart +++ b/lib/core/di/di_provider.dart @@ -1,17 +1,19 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_template/core/di/app_providers_module.dart'; import 'package:flutter_template/core/di/di_repository_module.dart'; import 'package:flutter_template/core/di/di_utils_module.dart'; import 'package:get_it/get_it.dart'; abstract class DiProvider { - static GetIt get _instance => GetIt.instance; + @visibleForTesting + static GetIt get instance => GetIt.instance; static Future init() async { // Setup app providers have to be done first - await AppProvidersModule().setupModule(_instance); - UtilsDiModule().setupModule(_instance); - RepositoryDiModule().setupModule(_instance); - await _instance.allReady(); + await AppProvidersModule().setupModule(instance); + UtilsDiModule().setupModule(instance); + RepositoryDiModule().setupModule(instance); + await instance.allReady(); } static T get({ @@ -19,5 +21,5 @@ abstract class DiProvider { dynamic param1, dynamic param2, }) => - _instance.get(instanceName: instanceName, param1: param1, param2: param2); + instance.get(instanceName: instanceName, param1: param1, param2: param2); } diff --git a/pubspec.yaml b/pubspec.yaml index 0863fb4f..f1a612f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,6 @@ dev_dependencies: auto_route_generator: 7.3.1 build_runner: 2.4.6 - mocktail: 1.0.1 bloc_test: 9.1.5 dart_code_metrics: 5.7.6 floor_generator: 1.4.2 @@ -63,6 +62,7 @@ dev_dependencies: freezed: 2.4.1 json_serializable: 6.7.1 lints: 3.0.0 + mocktail: 1.0.1 flutter: generate: true diff --git a/test/common/cubit_mocks.dart b/test/common/cubit_mocks.dart new file mode 100644 index 00000000..456e439f --- /dev/null +++ b/test/common/cubit_mocks.dart @@ -0,0 +1,5 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/ui/section/error_handler/global_event_handler_cubit.dart'; + +class MockGlobalEventHandlerCubit extends MockCubit + implements GlobalEventHandlerCubit {} diff --git a/test/common/mocks.dart b/test/common/data_mocks.dart similarity index 82% rename from test/common/mocks.dart rename to test/common/data_mocks.dart index 82e81c81..5c937d00 100644 --- a/test/common/mocks.dart +++ b/test/common/data_mocks.dart @@ -1,4 +1,3 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_template/core/repository/project_repository.dart'; import 'package:flutter_template/core/repository/session_repository.dart'; import 'package:flutter_template/core/source/auth_local_source.dart'; @@ -7,7 +6,6 @@ import 'package:flutter_template/core/source/common/http_service.dart'; import 'package:flutter_template/core/source/common/local_shared_preferences_storage.dart'; import 'package:flutter_template/core/source/project_local_source.dart'; import 'package:flutter_template/core/source/project_remote_source.dart'; -import 'package:flutter_template/ui/section/error_handler/global_event_handler_cubit.dart'; import 'package:mocktail/mocktail.dart'; //* Services @@ -22,14 +20,10 @@ class AuthLocalSourceMock extends Mock implements AuthLocalSource {} class AuthRemoteSourceMock extends Mock implements AuthRemoteSource {} +class LocalSharedPreferencesStorageMock extends Mock + implements LocalSharedPreferencesStorage {} + //* Repositories class MockSessionRepository extends Mock implements SessionRepository {} class MockProjectRepository extends Mock implements ProjectRepository {} - -//* Cubits -class MockGlobalEventHandlerCubit extends MockCubit - implements GlobalEventHandlerCubit {} - -class LocalSharedPreferencesStorageMock extends Mock - implements LocalSharedPreferencesStorage {} diff --git a/test/common/general_helpers.dart b/test/common/general_helpers.dart index 31ba20b7..c3b8b8e4 100644 --- a/test/common/general_helpers.dart +++ b/test/common/general_helpers.dart @@ -1,4 +1,11 @@ +import 'package:dio/dio.dart'; import 'package:flutter_template/core/source/common/app_database.dart'; Future setupFloorDatabase() => $FloorAppDatabase.inMemoryDatabaseBuilder().build(); + +Response successResponse(List elem, RequestOptions requestOptions) => Response( + data: elem.map((e) => e.toJson()).toList(), + statusCode: 200, + requestOptions: requestOptions, + ); diff --git a/test/common/project_helpers.dart b/test/common/project_helpers.dart index e7536e45..c47c7815 100644 --- a/test/common/project_helpers.dart +++ b/test/common/project_helpers.dart @@ -9,7 +9,7 @@ List generateProjectDbEntities(int count) => Iterable.generate( description: 'Test $index project description', url: 'test$index.com', imageUrl: '', - language: 'ES', + language: 'Dart', ), ).toList(); @@ -21,6 +21,6 @@ List generateProjects(int count) => Iterable.generate( description: 'Test $index project description', url: 'test$index.com', imageUrl: '', - language: 'ES', + language: 'Dart', ), ).toList(); diff --git a/test/cubits/signin_cubit_test.dart b/test/cubits/signin_cubit_test.dart index 3488e00a..cc9bea4a 100644 --- a/test/cubits/signin_cubit_test.dart +++ b/test/cubits/signin_cubit_test.dart @@ -5,7 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mocktail/mocktail.dart'; -import '../common/mocks.dart'; +import '../common/cubit_mocks.dart'; +import '../common/data_mocks.dart'; void main() { late GetIt getIt; diff --git a/test/cubits/welcome_cubit_test.dart b/test/cubits/welcome_cubit_test.dart index 879674f3..2d988a07 100644 --- a/test/cubits/welcome_cubit_test.dart +++ b/test/cubits/welcome_cubit_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: lines_longer_than_80_chars import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/core/di/di_provider.dart'; import 'package:flutter_template/core/repository/project_repository.dart'; import 'package:flutter_template/core/repository/session_repository.dart'; import 'package:flutter_template/ui/welcome/welcome_cubit.dart'; @@ -8,18 +9,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mocktail/mocktail.dart'; -import '../common/mocks.dart'; +import '../common/cubit_mocks.dart'; +import '../common/data_mocks.dart'; import '../common/project_helpers.dart'; void main() { - // ignore: unused_local_variable late GetIt getIt; late SessionRepository sessionRepository; late ProjectRepository projectRepository; late WelcomeCubit welcomeCubit; setUp(() { - getIt = GetIt.instance + getIt = DiProvider.instance ..registerSingleton( sessionRepository = MockSessionRepository(), ) diff --git a/test/repositories/project/project_local_source_test.dart b/test/repositories/project/project_local_source_test.dart index 0c95b564..17514a6c 100644 --- a/test/repositories/project/project_local_source_test.dart +++ b/test/repositories/project/project_local_source_test.dart @@ -61,11 +61,9 @@ void main() { ), ) .toList(); - await projectDao.replaceProjects( - replacement, - ); + await projectDao.replaceProjects(replacement); final list = await projectDao.getProjects().first; - expect(list, replacement); + expect(list, unorderedEquals(replacement)); }); }); } diff --git a/test/repositories/project/project_remote_source_test.dart b/test/repositories/project/project_remote_source_test.dart index aeaaec0c..d8f27525 100644 --- a/test/repositories/project/project_remote_source_test.dart +++ b/test/repositories/project/project_remote_source_test.dart @@ -4,7 +4,8 @@ import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import '../../common/mocks.dart'; +import '../../common/data_mocks.dart'; +import '../../common/general_helpers.dart'; import '../../common/project_helpers.dart'; void main() { @@ -21,11 +22,7 @@ void main() { final requestOptions = RequestOptions(path: urlGetProjects); final projects = generateProjects(1); when(() => httpService.get(urlGetProjects)).thenAnswer( - (_) async => Response( - data: projects.map((e) => e.toJson()).toList(), - statusCode: 200, - requestOptions: requestOptions, - ), + (_) async => successResponse(projects, requestOptions), ); final result = await projectRemoteSource.getProjects(); @@ -36,11 +33,7 @@ void main() { final requestOptions = RequestOptions(path: urlGetProjects); when(() => httpService.get(urlGetProjects)).thenAnswer( - (_) async => Response( - data: [], - statusCode: 200, - requestOptions: requestOptions, - ), + (_) async => successResponse([], requestOptions), ); final result = await projectRemoteSource.getProjects(); diff --git a/test/repositories/project/project_repository_test.dart b/test/repositories/project/project_repository_test.dart index a678653f..5ead0759 100644 --- a/test/repositories/project/project_repository_test.dart +++ b/test/repositories/project/project_repository_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import '../../common/mocks.dart'; +import '../../common/data_mocks.dart'; import '../../common/project_helpers.dart'; void main() { diff --git a/test/repositories/session/session_local_source_test.dart b/test/repositories/session/session_local_source_test.dart index 62745ae9..e14866f6 100644 --- a/test/repositories/session/session_local_source_test.dart +++ b/test/repositories/session/session_local_source_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_template/core/source/auth_local_source.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import '../../common/mocks.dart'; +import '../../common/data_mocks.dart'; void main() { late LocalSharedPreferencesStorageMock storage; diff --git a/test/repositories/session/session_remote_source_test.dart b/test/repositories/session/session_remote_source_test.dart index 7274cf40..e2c3a4d9 100644 --- a/test/repositories/session/session_remote_source_test.dart +++ b/test/repositories/session/session_remote_source_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_template/core/source/common/http_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import '../../common/mocks.dart'; +import '../../common/data_mocks.dart'; void main() { late HttpService httpService; diff --git a/test/repositories/session/session_repository_test.dart b/test/repositories/session/session_repository_test.dart index 147cda71..bae4956b 100644 --- a/test/repositories/session/session_repository_test.dart +++ b/test/repositories/session/session_repository_test.dart @@ -12,8 +12,8 @@ import 'package:flutter_template/core/source/common/app_database.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import '../../common/data_mocks.dart'; import '../../common/general_helpers.dart'; -import '../../common/mocks.dart'; void main() { late AuthLocalSource authLocalSource; From e069ecf875c9b28ab92933f38c67f8115b7536f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Wed, 10 Apr 2024 09:57:40 -0300 Subject: [PATCH 05/10] fix: requested changes --- scripts/checks.sh | 3 +++ test/cubits/signin_cubit_test.dart | 3 ++- test/cubits/welcome_cubit_test.dart | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/checks.sh b/scripts/checks.sh index 1e1335d0..3eb9e919 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -29,3 +29,6 @@ echo ':: Run Catalog checks' echo ':: Run linter catalog ::' fvm flutter analyze catalog || error "Linter error - Flutter Analyze error - Catalog gallery" fvm flutter analyze catalog/gallery || error "Linter error - Flutter Analyze error - Catalog gallery" + +echo ':: Run tests ::' +fvm flutter test || error "Tests failed" diff --git a/test/cubits/signin_cubit_test.dart b/test/cubits/signin_cubit_test.dart index cc9bea4a..b9ed1ecb 100644 --- a/test/cubits/signin_cubit_test.dart +++ b/test/cubits/signin_cubit_test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_template/core/di/di_provider.dart'; import 'package:flutter_template/core/repository/session_repository.dart'; import 'package:flutter_template/ui/signin/signin_cubit.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -14,7 +15,7 @@ void main() { late SignInCubit signInCubit; setUp(() { - getIt = GetIt.instance + getIt = DiProvider.instance ..registerSingleton( sessionRepository = MockSessionRepository(), ); diff --git a/test/cubits/welcome_cubit_test.dart b/test/cubits/welcome_cubit_test.dart index 2d988a07..425a5c7f 100644 --- a/test/cubits/welcome_cubit_test.dart +++ b/test/cubits/welcome_cubit_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: lines_longer_than_80_chars - import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_template/core/di/di_provider.dart'; import 'package:flutter_template/core/repository/project_repository.dart'; @@ -83,6 +81,7 @@ void main() { ); blocTest( + // ignore: lines_longer_than_80_chars 'Welcome cubit calls logOut from sessionRepository when the screen calls logOut method from cubit', build: () => welcomeCubit = WelcomeCubit(MockGlobalEventHandlerCubit()), setUp: () { From 8614f84cc3edf0c775528a04fe89e43a5301f835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Wed, 10 Apr 2024 10:57:03 -0300 Subject: [PATCH 06/10] fix: fvm new version --- .fvmrc | 4 ++++ .gitignore | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16d80ab4..dab0ef1a 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ app.*.map.json *.env.default # Fvm -.fvm/flutter_sdk +.fvm/ # fastlane specific **/fastlane/report.xml From 23920f828aa10e50528e4cfe2bcd386b64752fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Wed, 10 Apr 2024 13:28:50 -0300 Subject: [PATCH 07/10] fix: hide settings vscode --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dab0ef1a..2bc6cea4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ mason-lock.json # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/settings.json # Flutter/Dart/Pub related **/doc/api/ From 0374eb1429992d0d4383d18a305b211092b8a48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Wed, 10 Apr 2024 14:29:08 -0300 Subject: [PATCH 08/10] fix: remove fvm config --- .fvm/fvm_config.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .fvm/fvm_config.json diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index d8abe1b9..00000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.13.9", - "flavors": {} -} \ No newline at end of file From 36114d58a9c28ef163bbeec448da939881d472ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Tue, 14 May 2024 14:38:20 -0300 Subject: [PATCH 09/10] fix: remove entity --- lib/core/model/db/repository_db_entity.dart | 32 --------------------- 1 file changed, 32 deletions(-) delete mode 100644 lib/core/model/db/repository_db_entity.dart diff --git a/lib/core/model/db/repository_db_entity.dart b/lib/core/model/db/repository_db_entity.dart deleted file mode 100644 index 9ce52f9a..00000000 --- a/lib/core/model/db/repository_db_entity.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:floor/floor.dart'; - -@Entity(tableName: 'projects') -class ProjectDbEntity extends Equatable { - @primaryKey - final int id; - final String name; - final String description; - final String url; - final String imageUrl; - final String language; - - const ProjectDbEntity({ - required this.id, - required this.name, - required this.description, - required this.url, - required this.imageUrl, - required this.language, - }); - - @override - List get props => [ - id, - name, - description, - url, - imageUrl, - language, - ]; -} From 418d6684b10441e5f45b611eb8836d912e68608b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Wed, 15 May 2024 10:38:22 -0300 Subject: [PATCH 10/10] fix: change on gemfile.lock --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index aa0d3446..693cabb1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -275,6 +275,7 @@ GEM PLATFORMS arm64-darwin-21 + arm64-darwin-22 x86_64-darwin-19 x86_64-darwin-20 x86_64-linux