From af065a510e9fb215b8002550a59a558b41ab0a4a Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Sun, 22 Dec 2024 23:09:36 -0300 Subject: [PATCH 1/6] fix(#58): troca nome de arquivo para snake case --- lib/main.dart | 2 +- .../view/{LoginView.dart => login_view.dart} | 15 +++++++-------- ...{LoginViewModel.dart => login_view_model.dart} | 0 .../viewModel/onboarding_view_model.dart | 2 +- lib/ui/register_account/view/RegisterAccount.dart | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) rename lib/ui/login/view/{LoginView.dart => login_view.dart} (94%) rename lib/ui/login/viewModel/{LoginViewModel.dart => login_view_model.dart} (100%) diff --git a/lib/main.dart b/lib/main.dart index dd51bf1..abcd3db 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:aranduapp/config/ThemeApp.dart'; -import 'package:aranduapp/ui/login/view/LoginView.dart'; +import 'package:aranduapp/ui/login/view/login_view.dart'; import 'package:aranduapp/ui/welcome/view/WelcomeView.dart'; import 'package:flutter/material.dart'; diff --git a/lib/ui/login/view/LoginView.dart b/lib/ui/login/view/login_view.dart similarity index 94% rename from lib/ui/login/view/LoginView.dart rename to lib/ui/login/view/login_view.dart index 88ed26e..dc7980d 100644 --- a/lib/ui/login/view/LoginView.dart +++ b/lib/ui/login/view/login_view.dart @@ -1,11 +1,10 @@ import 'package:aranduapp/core/log/Log.dart'; -import 'package:aranduapp/ui/navbar/view/navBarView.dart'; import 'package:aranduapp/ui/shared/TextAndLink.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:aranduapp/ui/login/viewModel/LoginViewModel.dart'; +import 'package:aranduapp/ui/login/viewModel/login_view_model.dart'; import 'package:aranduapp/ui/recover_account/view/recover_account_view.dart'; import 'package:aranduapp/ui/register_account/view/RegisterAccount.dart'; @@ -23,21 +22,21 @@ class Login extends StatelessWidget { Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => LoginViewModel(context), - child: const _Login(), + child: const LoginScreen(), ); } } -class _Login extends StatefulWidget { - const _Login({super.key}); +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); @override State createState() { - return _LoginState(); + return _LoginScreenState(); } } -class _LoginState extends State<_Login> { +class _LoginScreenState extends State { late Future _future; @override @@ -156,7 +155,7 @@ class _LoginState extends State<_Login> { onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => RecoverAccount(), + builder: (context) => const RecoverAccount(), ), ); }, diff --git a/lib/ui/login/viewModel/LoginViewModel.dart b/lib/ui/login/viewModel/login_view_model.dart similarity index 100% rename from lib/ui/login/viewModel/LoginViewModel.dart rename to lib/ui/login/viewModel/login_view_model.dart diff --git a/lib/ui/onboarding/viewModel/onboarding_view_model.dart b/lib/ui/onboarding/viewModel/onboarding_view_model.dart index 0765f95..43fb6a7 100644 --- a/lib/ui/onboarding/viewModel/onboarding_view_model.dart +++ b/lib/ui/onboarding/viewModel/onboarding_view_model.dart @@ -1,4 +1,4 @@ -import 'package:aranduapp/ui/login/view/LoginView.dart'; +import 'package:aranduapp/ui/login/view/login_view.dart'; import 'package:flutter/material.dart'; class OnboardingViewModel extends ChangeNotifier { diff --git a/lib/ui/register_account/view/RegisterAccount.dart b/lib/ui/register_account/view/RegisterAccount.dart index 4e4e269..5ea87db 100644 --- a/lib/ui/register_account/view/RegisterAccount.dart +++ b/lib/ui/register_account/view/RegisterAccount.dart @@ -1,4 +1,4 @@ -import 'package:aranduapp/ui/login/view/LoginView.dart'; +import 'package:aranduapp/ui/login/view/login_view.dart'; import 'package:aranduapp/ui/shared/OrDivider.dart'; import 'package:aranduapp/ui/shared/TextAndLink.dart'; import 'package:aranduapp/ui/shared/TextName.dart'; From 2f00182d7a815794bb18a897a6ad38b147ec39e2 Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Mon, 23 Dec 2024 14:59:37 -0300 Subject: [PATCH 2/6] test(#58): teste da view --- lib/ui/login/view/login_view.dart | 7 +- lib/ui/login/viewModel/login_view_model.dart | 19 +- test/ui/login/view/login_view_test.dart | 64 ++++++ test/ui/login/view/login_view_test.mocks.dart | 210 ++++++++++++++++++ 4 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 test/ui/login/view/login_view_test.dart create mode 100644 test/ui/login/view/login_view_test.mocks.dart diff --git a/lib/ui/login/view/login_view.dart b/lib/ui/login/view/login_view.dart index dc7980d..b9dbc3c 100644 --- a/lib/ui/login/view/login_view.dart +++ b/lib/ui/login/view/login_view.dart @@ -72,6 +72,9 @@ class _LoginScreenState extends State { } Widget _authDevice(LoginViewModel viewModel) { + + Log.d("Mostrando tela de autorização do dispositivo"); + return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -92,7 +95,7 @@ class _LoginScreenState extends State { child: ElevatedButton( onPressed: () async { viewModel.loginWithDeviceAuth().then((ok) { - viewModel.goNextPage(); + viewModel.goToHome(); }); }, child: const Text('Usar senha do celular'), @@ -185,7 +188,7 @@ class _LoginScreenState extends State { onPressed: () { viewModel.loginWithEmailAndPassword().then((_) { - viewModel.goNextPage(); + viewModel.goToHome(); }).catchError((e) => showDialog( context: context, diff --git a/lib/ui/login/viewModel/login_view_model.dart b/lib/ui/login/viewModel/login_view_model.dart index a182eb1..e941a46 100644 --- a/lib/ui/login/viewModel/login_view_model.dart +++ b/lib/ui/login/viewModel/login_view_model.dart @@ -54,11 +54,18 @@ class LoginViewModel extends ChangeNotifier { .authenticate(localizedReason: 'Toque com o dedo no sensor para logar'); } - void goNextPage() { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => const NavbarView(), - ), - ); + void goToHome() { + try { + if (context.mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const NavbarView(), + ), + ); + } + } catch (e) { + Log.e(e); + rethrow; + } } } diff --git a/test/ui/login/view/login_view_test.dart b/test/ui/login/view/login_view_test.dart new file mode 100644 index 0000000..df5d8ff --- /dev/null +++ b/test/ui/login/view/login_view_test.dart @@ -0,0 +1,64 @@ +import 'package:aranduapp/ui/login/view/login_view.dart'; +import 'package:aranduapp/ui/login/viewModel/login_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'login_view_test.mocks.dart'; + +void main() { + late MockLoginViewModel mockViewModel; + + setUp(() { + mockViewModel = MockLoginViewModel(); + when(mockViewModel.formKey).thenReturn(GlobalKey()); + when(mockViewModel.emailController).thenReturn(TextEditingController()); + when(mockViewModel.isLoading).thenReturn(false); + }); + + testWidgets('Displays loading screen while waiting for Future', + (WidgetTester tester) async { + when(mockViewModel.validateToken()).thenAnswer( + (_) async => await Future.delayed(const Duration(seconds: 1))); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: mockViewModel, + child: const MaterialApp( + home: LoginScreen(), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + await tester.pumpAndSettle(); + + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('login with auth device', (WidgetTester tester) async { + when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => true); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: mockViewModel, + child: const MaterialApp( + home: LoginScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + verify(mockViewModel.loginWithDeviceAuth()).called(1); + + await tester.pumpAndSettle(); + verify(mockViewModel.goToHome()).called(1); + }); +} diff --git a/test/ui/login/view/login_view_test.mocks.dart b/test/ui/login/view/login_view_test.mocks.dart new file mode 100644 index 0000000..fa695eb --- /dev/null +++ b/test/ui/login/view/login_view_test.mocks.dart @@ -0,0 +1,210 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in aranduapp/test/ui/login/view/login_view_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:aranduapp/ui/login/viewModel/login_view_model.dart' as _i3; +import 'package:flutter/material.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeBuildContext_0 extends _i1.SmartFake implements _i2.BuildContext { + _FakeBuildContext_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeGlobalKey_1> + extends _i1.SmartFake implements _i2.GlobalKey { + _FakeGlobalKey_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTextEditingController_2 extends _i1.SmartFake + implements _i2.TextEditingController { + _FakeTextEditingController_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LoginViewModel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoginViewModel extends _i1.Mock implements _i3.LoginViewModel { + @override + _i2.BuildContext get context => (super.noSuchMethod( + Invocation.getter(#context), + returnValue: _FakeBuildContext_0( + this, + Invocation.getter(#context), + ), + returnValueForMissingStub: _FakeBuildContext_0( + this, + Invocation.getter(#context), + ), + ) as _i2.BuildContext); + + @override + bool get isLoading => (super.noSuchMethod( + Invocation.getter(#isLoading), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + set isLoading(bool? _isLoading) => super.noSuchMethod( + Invocation.setter( + #isLoading, + _isLoading, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.GlobalKey<_i2.FormState> get formKey => (super.noSuchMethod( + Invocation.getter(#formKey), + returnValue: _FakeGlobalKey_1<_i2.FormState>( + this, + Invocation.getter(#formKey), + ), + returnValueForMissingStub: _FakeGlobalKey_1<_i2.FormState>( + this, + Invocation.getter(#formKey), + ), + ) as _i2.GlobalKey<_i2.FormState>); + + @override + _i2.TextEditingController get emailController => (super.noSuchMethod( + Invocation.getter(#emailController), + returnValue: _FakeTextEditingController_2( + this, + Invocation.getter(#emailController), + ), + returnValueForMissingStub: _FakeTextEditingController_2( + this, + Invocation.getter(#emailController), + ), + ) as _i2.TextEditingController); + + @override + _i2.TextEditingController get passwordController => (super.noSuchMethod( + Invocation.getter(#passwordController), + returnValue: _FakeTextEditingController_2( + this, + Invocation.getter(#passwordController), + ), + returnValueForMissingStub: _FakeTextEditingController_2( + this, + Invocation.getter(#passwordController), + ), + ) as _i2.TextEditingController); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i4.Future loginWithEmailAndPassword() => (super.noSuchMethod( + Invocation.method( + #loginWithEmailAndPassword, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future validateToken() => (super.noSuchMethod( + Invocation.method( + #validateToken, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future loginWithDeviceAuth() => (super.noSuchMethod( + Invocation.method( + #loginWithDeviceAuth, + [], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); + + @override + void goToHome() => super.noSuchMethod( + Invocation.method( + #goToHome, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void addListener(dynamic listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(dynamic listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} From 0e6095a0d36e725cf3017512543ba6edff277d58 Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Mon, 23 Dec 2024 15:21:21 -0300 Subject: [PATCH 3/6] =?UTF-8?q?fix(#58):=20corrige=20erro=20de=20navega?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui/login/view/login_view.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/ui/login/view/login_view.dart b/lib/ui/login/view/login_view.dart index b9dbc3c..ba527ae 100644 --- a/lib/ui/login/view/login_view.dart +++ b/lib/ui/login/view/login_view.dart @@ -42,8 +42,8 @@ class _LoginScreenState extends State { @override void initState() { super.initState(); - _future = Provider.of(context, listen: false) - .validateToken(); + _future = + Provider.of(context, listen: false).validateToken(); } @override @@ -57,7 +57,6 @@ class _LoginScreenState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return _loadingScreen(viewModel); } else if (!snapshot.hasError) { - viewModel.loginWithDeviceAuth(); return _authDevice(viewModel); } else { return _emailAndPassword(viewModel); @@ -72,9 +71,10 @@ class _LoginScreenState extends State { } Widget _authDevice(LoginViewModel viewModel) { - Log.d("Mostrando tela de autorização do dispositivo"); - + + viewModel.loginWithDeviceAuth().then((_) => viewModel.goToHome()); + return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -187,9 +187,7 @@ class _LoginScreenState extends State { child: ElevatedButton( onPressed: () { viewModel.loginWithEmailAndPassword().then((_) { - viewModel.goToHome(); - }).catchError((e) => showDialog( context: context, builder: (BuildContext context) => From 13ac32f414028bb453bc963235022811360a34fd Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Mon, 23 Dec 2024 16:05:06 -0300 Subject: [PATCH 4/6] test(#58): melhora casos de test da view --- test/ui/login/view/login_view_test.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/ui/login/view/login_view_test.dart b/test/ui/login/view/login_view_test.dart index df5d8ff..5ed50d0 100644 --- a/test/ui/login/view/login_view_test.dart +++ b/test/ui/login/view/login_view_test.dart @@ -16,7 +16,6 @@ void main() { mockViewModel = MockLoginViewModel(); when(mockViewModel.formKey).thenReturn(GlobalKey()); when(mockViewModel.emailController).thenReturn(TextEditingController()); - when(mockViewModel.isLoading).thenReturn(false); }); testWidgets('Displays loading screen while waiting for Future', @@ -42,8 +41,9 @@ void main() { expect(find.byType(CircularProgressIndicator), findsNothing); }); + testWidgets('login with auth device', (WidgetTester tester) async { - when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => true); + when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => false); await tester.pumpWidget( ChangeNotifierProvider.value( @@ -56,9 +56,14 @@ void main() { await tester.pumpAndSettle(); - verify(mockViewModel.loginWithDeviceAuth()).called(1); + when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => true); + final button = find.text('Usar senha do celular'); + expect(button, findsOneWidget); + await tester.tap(button); await tester.pumpAndSettle(); + verify(mockViewModel.goToHome()).called(1); + verify(mockViewModel.loginWithDeviceAuth()).called(2); }); } From c99ebbb27b4dbff043cca1d5247baf879028a3f7 Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Mon, 23 Dec 2024 16:06:33 -0300 Subject: [PATCH 5/6] fix(#58): corrige erro encontrado com o teste --- lib/ui/login/view/login_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ui/login/view/login_view.dart b/lib/ui/login/view/login_view.dart index ba527ae..8838b86 100644 --- a/lib/ui/login/view/login_view.dart +++ b/lib/ui/login/view/login_view.dart @@ -73,7 +73,11 @@ class _LoginScreenState extends State { Widget _authDevice(LoginViewModel viewModel) { Log.d("Mostrando tela de autorização do dispositivo"); - viewModel.loginWithDeviceAuth().then((_) => viewModel.goToHome()); + viewModel.loginWithDeviceAuth().then((ok) { + if (ok) { + viewModel.goToHome(); + } + }); return Column( mainAxisAlignment: MainAxisAlignment.center, From a20333547870321f41d111e2c636670e3b7b1bb9 Mon Sep 17 00:00:00 2001 From: Gabriel Costa de Oliveira Date: Tue, 24 Dec 2024 13:45:05 -0300 Subject: [PATCH 6/6] test(#58): molhora casos de teste da LoginView --- test/ui/login/view/login_view_test.dart | 85 +++++++++++++++++++------ 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/test/ui/login/view/login_view_test.dart b/test/ui/login/view/login_view_test.dart index 5ed50d0..ce87e46 100644 --- a/test/ui/login/view/login_view_test.dart +++ b/test/ui/login/view/login_view_test.dart @@ -1,5 +1,7 @@ import 'package:aranduapp/ui/login/view/login_view.dart'; import 'package:aranduapp/ui/login/viewModel/login_view_model.dart'; +import 'package:aranduapp/ui/shared/TextEmail.dart'; +import 'package:aranduapp/ui/shared/TextPassword.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -16,21 +18,24 @@ void main() { mockViewModel = MockLoginViewModel(); when(mockViewModel.formKey).thenReturn(GlobalKey()); when(mockViewModel.emailController).thenReturn(TextEditingController()); + when(mockViewModel.passwordController).thenReturn(TextEditingController()); }); + Widget createLoginScreen(LoginViewModel mockViewModel) { + return ChangeNotifierProvider.value( + value: mockViewModel, + child: const MaterialApp( + home: LoginScreen(), + ), + ); + } + testWidgets('Displays loading screen while waiting for Future', (WidgetTester tester) async { when(mockViewModel.validateToken()).thenAnswer( (_) async => await Future.delayed(const Duration(seconds: 1))); - await tester.pumpWidget( - ChangeNotifierProvider.value( - value: mockViewModel, - child: const MaterialApp( - home: LoginScreen(), - ), - ), - ); + await tester.pumpWidget(createLoginScreen(mockViewModel)); await tester.pump(const Duration(milliseconds: 500)); @@ -41,19 +46,10 @@ void main() { expect(find.byType(CircularProgressIndicator), findsNothing); }); - testWidgets('login with auth device', (WidgetTester tester) async { when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => false); - await tester.pumpWidget( - ChangeNotifierProvider.value( - value: mockViewModel, - child: const MaterialApp( - home: LoginScreen(), - ), - ), - ); - + await tester.pumpWidget(createLoginScreen(mockViewModel)); await tester.pumpAndSettle(); when(mockViewModel.loginWithDeviceAuth()).thenAnswer((_) async => true); @@ -66,4 +62,57 @@ void main() { verify(mockViewModel.goToHome()).called(1); verify(mockViewModel.loginWithDeviceAuth()).called(2); }); + + testWidgets( + 'Login screen displays email and password fields and login button', + (WidgetTester tester) async { + when(mockViewModel.validateToken()) + .thenAnswer((_) async => throw Exception('Token validation failed')); + + await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpAndSettle(); + + expect(find.byType(TextEmail), findsOneWidget); + expect(find.byType(TextPassWord), findsOneWidget); + expect(find.text('Entrar'), findsOneWidget); + }); + + testWidgets('Test User Input for Email and Password', + (WidgetTester tester) async { + when(mockViewModel.validateToken()) + .thenAnswer((_) async => throw Exception('Token validation failed')); + + await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpAndSettle(); + + const email = 'test@example.com'; + const password = 'password123'; + + await tester.enterText(find.byType(TextEmail), email); + await tester.enterText(find.byType(TextPassWord), password); + + expect(mockViewModel.emailController.text, email); + expect(mockViewModel.passwordController.text, password); + }); + + testWidgets('Login is successful', + (WidgetTester tester) async { + when(mockViewModel.validateToken()) + .thenAnswer((_) async => throw Exception('Token validation failed')); + + await tester.pumpWidget(createLoginScreen(mockViewModel)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Entrar')); + + when(mockViewModel.loginWithEmailAndPassword()).thenAnswer( + (_) async => await Future.delayed(const Duration(seconds: 1))); + + await tester.pumpAndSettle(); + verify(mockViewModel.loginWithEmailAndPassword()).called(1); + verify(mockViewModel.goToHome()).called(1); + }); + + testWidgets('Displays error when login fails', (WidgetTester tester) async { + }); }