From deb4d79b0144d03fef2c2b9dbbd0cc7b9d705c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicola=CC=81s=20Lantean?= Date: Tue, 23 Apr 2024 12:33:42 -0300 Subject: [PATCH] fix: requested changes --- README.md | 32 +++++-- integration_test/common/general_helpers.dart | 30 ++++-- integration_test/common/repository_mocks.dart | 7 +- integration_test/test/signin_test.dart | 95 +++++++++---------- scripts/integration_test.sh | 3 + test_driver/integration_test.dart | 3 + 6 files changed, 99 insertions(+), 71 deletions(-) create mode 100755 scripts/integration_test.sh create mode 100644 test_driver/integration_test.dart diff --git a/README.md b/README.md index d8ad2083..26759e08 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Xmartlabs' Flutter template -## Arch Overview +## Arch Overview The project is divided into two main folders: + - The UI contains all app screens. - The Core contains the models and the data layer. @@ -27,6 +28,7 @@ These components are injected in the Cubits using [get_it][get_it]. ## Project Overview ### Assets + The [`/assets/`](./assets) folder contains the assets used by the application, such as images, fonts, and other files. ### Environments @@ -34,6 +36,16 @@ The [`/assets/`](./assets) folder contains the assets used by the application, s The environment variables are defined in the `default.env` file located in [`/environments/`](./environments) folder. You can read more information about the environment variables in the [README.md](./environments/README.md) file. +### Testing + +#### Mocks + +For moking the projects uses [mocktail][mocktail], a library inspired on [mockito][mockito] which deletes the code generation part. + +#### Integration test + +The integration tests for the project are defined in [integration_test][integration_test]. Dart package integration_test is used for the implementation. Are included two tests for the Sign In flow, covering both successful and failed sign-in attempts. + ## Project Setup The project setup is based on some plugins which generate the required native code. @@ -41,20 +53,22 @@ The project setup is based on some plugins which generate the required native co You can use [project_setup.sh](scripts/project_setup.sh) to reload all project setups. ### Flavor setup: Project name, properties BundleId & Application id -This information is set using [flavorizr], a flutter utility to easily create flavors in your flutter application. + +This information is set using [flavorizr], a flutter utility to easily create flavors in your flutter application. To change it go to `flavorizr` section in the [pubspec] file. For example, to add a new flavour, you can do something like: + ```yaml flavorizr: flavors: qa: app: - name: 'My Project - QA' + name: "My Project - QA" android: - applicationId: 'com.xmartlabs.myproject.qa' + applicationId: "com.xmartlabs.myproject.qa" ios: - bundleId: 'com.xmartlabs.myproject.qa' + bundleId: "com.xmartlabs.myproject.qa" ``` After a change is made, you need to regenerate your native files. @@ -70,7 +84,6 @@ To change it go to `flutter_icons` section in the [pubspec] file. After a change is made, you need to regenerate your native files. You can do that by executing `flutter pub run flutter_launcher_icons:main`. - ### Splash screen Splash screen is generated using [flutter_native_splash]. @@ -80,13 +93,14 @@ After a change is made, you need to regenerate your native files. You can do that by executing `flutter pub run flutter_native_splash:create`. Although you can setup a bunch of features in this library, it doesn't provide a way to display animations. -If you need a more personalized splash screen, you can edit the native code or just remove this library. +If you need a more personalized splash screen, you can edit the native code or just remove this library. ### Code generation Code generation is created using `build_runner` package.\ To configure this package edit the `build.yaml`\ To add new files to watch for code generation add the following lines: + ``` targets: $default: @@ -98,6 +112,7 @@ targets: # Example glob for only the Dart files under `lib/models` - lib/models/*.dart ``` + To create generated code run `clean_up.sh` under [scripts] folder or the following command: `flutter pub run build_runner build --delete-conflicting-outputs` ### Pre Push config @@ -117,3 +132,6 @@ In order to setup pre-push hook you need to go to the root of the project and ru [data_source_folder]: https://github.com/xmartlabs/flutter-template/tree/main/lib/core/source [get_it]: https://pub.dev/packages/get_it [scripts]: https://github.com/xmartlabs/flutter-template/tree/main/scripts +[integration_test]: https://github.com/xmartlabs/flutter-template/tree/main/intgration_test +[mocktail]: https://pub.dev/packages/mocktail +[mockito]: https://pub.dev/packages/mockito diff --git a/integration_test/common/general_helpers.dart b/integration_test/common/general_helpers.dart index 29a8f525..4da09982 100644 --- a/integration_test/common/general_helpers.dart +++ b/integration_test/common/general_helpers.dart @@ -1,20 +1,30 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_template/core/di/di_provider.dart'; -import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/core/source/auth_remote_source.dart'; +import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:flutter_template/main.dart' as app; -import 'package:flutter_template/ui/router/app_router.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'repository_mocks.dart'; -Future commonSetup( - MockSessionRepository mockSessionRepository, - AppRouter appRouter, -) async { +Future commonSetup({ + required MockAuthRemoteSource mockAuthRemoteSource, + required MockProjectRemoteSource mockProjectRemoteSource, +}) async { await app.initSdks(); - DiProvider.instance.unregister(); - DiProvider.instance.unregister(); + DiProvider.instance.unregister(); + DiProvider.instance.unregister(); + DiProvider.instance.registerSingleton(mockAuthRemoteSource); DiProvider.instance - .registerSingleton(mockSessionRepository); - DiProvider.instance.registerSingleton(appRouter); + .registerSingleton(mockProjectRemoteSource); +} + +extension WidgetTesterExtension on WidgetTester { + BuildContext contextOfType() { + final finder = find.byType(T); + expect(finder, findsOneWidget); + return element(finder); + } } diff --git a/integration_test/common/repository_mocks.dart b/integration_test/common/repository_mocks.dart index c8c37bbe..77f6216c 100644 --- a/integration_test/common/repository_mocks.dart +++ b/integration_test/common/repository_mocks.dart @@ -1,4 +1,7 @@ -import 'package:flutter_template/core/repository/session_repository.dart'; +import 'package:flutter_template/core/source/auth_remote_source.dart'; +import 'package:flutter_template/core/source/project_remote_source.dart'; import 'package:mocktail/mocktail.dart'; -class MockSessionRepository extends Mock implements SessionRepository {} +class MockProjectRemoteSource extends Mock implements ProjectRemoteSource {} + +class MockAuthRemoteSource extends Mock implements AuthRemoteSource {} diff --git a/integration_test/test/signin_test.dart b/integration_test/test/signin_test.dart index 361a20c5..69037982 100644 --- a/integration_test/test/signin_test.dart +++ b/integration_test/test/signin_test.dart @@ -1,102 +1,93 @@ -import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_template/core/common/network_exceptions.dart'; import 'package:flutter_template/core/di/di_provider.dart'; -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/main.dart' as app; import 'package:flutter_template/ui/extensions/context_extensions.dart'; -import 'package:flutter_template/ui/router/app_router.dart'; -import 'package:flutter_template/ui/section/error_handler/global_event_handler_cubit.dart'; import 'package:flutter_template/ui/signin/signin_screen.dart'; import 'package:flutter_template/ui/welcome/welcome_screen.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:mocktail/mocktail.dart'; import '../common/general_helpers.dart'; import '../common/repository_mocks.dart'; + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late MockSessionRepository mockSessionRepository; - late AppRouter appRouter; - late BuildContext context; - late StreamController statusController; + late MockAuthRemoteSource mockAuthRemoteSource; + late MockProjectRemoteSource mockProjectRemoteSource; setUp(() async { - mockSessionRepository = MockSessionRepository(); - statusController = BehaviorSubject() - ..add(AuthenticationStatus.unauthenticated); - when(() => mockSessionRepository.status) - .thenAnswer((_) => statusController.stream); - appRouter = AppRouter(mockSessionRepository); - await commonSetup(mockSessionRepository, appRouter); + mockAuthRemoteSource = MockAuthRemoteSource(); + mockProjectRemoteSource = MockProjectRemoteSource(); + await commonSetup( + mockAuthRemoteSource: mockAuthRemoteSource, + mockProjectRemoteSource: mockProjectRemoteSource, + ); }); tearDown(() { DiProvider.instance.reset(); - statusController.close(); }); group('SignIn screen tests', () { + const email = "test@test.com"; + const password = "test"; + testWidgets('SignIn with wrong credentials', (WidgetTester tester) async { - mockSignInUser(false, mockSessionRepository, statusController); - await Future.delayed(const Duration(seconds: 2)); + when(() => mockAuthRemoteSource.signIn(email, password)).thenAnswer( + (_) => Future.error( + const NetworkException.unauthorizedRequest("Unauthorized"), + ), + ); + await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(); - context = tester.element(find.byType(SignInScreen)); + + final context = tester.contextOfType(); await tester.enterText( find.widgetWithText(TextField, context.localizations.mail), - 'hi@xmartlabs.com', + email, ); await tester.enterText( find.widgetWithText(TextField, context.localizations.password), - '1234', + password, + ); + await tester.tap( + find.widgetWithText(TextButton, context.localizations.sign_in), ); - await tester - .tap(find.widgetWithText(TextButton, context.localizations.sign_in)); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); }); testWidgets('SignIn with correct credentials', (WidgetTester tester) async { - mockSignInUser(true, mockSessionRepository, statusController); - await Future.delayed(const Duration(seconds: 2)); + when(() => mockAuthRemoteSource.signIn(email, password)).thenAnswer( + (_) => Future.value( + SignInResponse( + accessToken: "testing-token", + user: User(email: "user@test.com"), + ), + ), + ); + when(() => mockProjectRemoteSource.getProjects()) + .thenAnswer((_) => Future.value([])); + await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(); - context = tester.element(find.byType(SignInScreen)); + final context = tester.contextOfType(); await tester.enterText( find.widgetWithText(TextField, context.localizations.mail), - 'hi@xmartlabs.com', + email, ); await tester.enterText( find.widgetWithText(TextField, context.localizations.password), - 'xmartlabs', + password, ); await tester .tap(find.widgetWithText(TextButton, context.localizations.sign_in)); await tester.pumpAndSettle(); - await Future.delayed(const Duration(seconds: 2)); expect(find.byType(WelcomeScreen), findsOneWidget); }); }); } - -void mockSignInUser( - bool correctCredentials, - MockSessionRepository mockSessionRepository, - StreamController statusController, -) => - when( - () => mockSessionRepository.signInUser( - email: 'hi@xmartlabs.com', - password: correctCredentials ? 'xmartlabs' : '1234', - ), - ).thenAnswer( - correctCredentials - ? (_) async { - statusController.add(AuthenticationStatus.authenticated); - } - : (_) => Future.error( - const UnknownError('Ops! Something went wrong'), - ), - ); diff --git a/scripts/integration_test.sh b/scripts/integration_test.sh new file mode 100755 index 00000000..960e1566 --- /dev/null +++ b/scripts/integration_test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test/signin_test.dart --flavor dev \ No newline at end of file diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 00000000..b38629cc --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver();