Flutter Integration testing failed for multiple test cases in a single file - flutter

I have a simple login page with email and password text field. A button to login and another to sign up. I tried to write integration testing for the Sign in page.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('''could type email and password in text filed''',
(WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
final textFieldEmail = find.byType(InputTextWidget).first;
final textFieldPassword = find.byType(InputTextWidget).last;
await tester.enterText(textFieldEmail, "asis.adh#gmail.com");
await tester.pumpAndSettle();
expect(find.text("asis.adh#gmail.com"), findsOneWidget);
});
testWidgets(
'should redirect to Sign Up Page when create an account is tapped',
(WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
final createAnAccount = find.text("Create an account");
await tester.tap(createAnAccount);
await tester.pumpAndSettle();
expect(find.byType(SignupPage), findsOneWidget);
expect(find.byType(LoginPage), findsNothing);
});
}
When I execute the test case, it fails with the following error:
The following ArgumentError was thrown running a test: Invalid
argument(s): Object/factory with type AmenitiesProvider is already
registered inside GetIt.
Here is my main.dart file
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
configureInjection(Environment.prod);
/// for registering the factory.
await Future.delayed(const Duration(seconds: 2));
runApp(RoopaApp());
}
I tried with a main_test.dart and adding configureInjection(Environment.test) but nothing changes. I am not sure how to fix the error. Is there a way to clean the app or destroy it before going to new test case. If I combine the both testcase into one then it works without any problem.
Here is configureInjection
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
}
I am using get_it and injection package for dependency injection.
Here is the auto generated initGetIt
GetIt $initGetIt(
GetIt get, {
String environment,
EnvironmentFilter environmentFilter,
}) {
final gh = GetItHelper(get, environment, environmentFilter);
final httpClientInjectableModule = _$HttpClientInjectableModule();
final flutterStorageModule = _$FlutterStorageModule();
gh.lazySingleton<AmenitiesProvider>(() => AmenitiesProvider());
gh.factory<Client>(() => httpClientInjectableModule.client);
gh.lazySingleton<FileProvider>(() => FileProvider());
gh.lazySingleton<FlutterSecureStorage>(
() => flutterStorageModule.secureStorate);
gh.factory<ProfilePageBloc>(() => ProfilePageBloc());
gh.factory<SplashScreenBloc>(() => SplashScreenBloc());
gh.lazySingleton<AuthLocalDataSourceProtocol>(
() => AuthLocalDataSource(secureStorage: get<FlutterSecureStorage>()));
gh.lazySingleton<AuthRemoteDataSourceProtocol>(
() => AuthRemoteDataSource(client: get<Client>()));
return get;
}

In my config page.
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
}
I just created a test environment and added following like, now its working as expected.
#injectableInit
void configureInjection(String environment) {
$initGetIt(getIt, environment: environment);
if (environment == Environment.test) {
getIt.allowReassignment = true;
}
}

tearDown(() async {
final getIt = GetIt.instance;
await getIt.reset();
});

Related

Object/factory with type GroupProvider is already registered inside GetIt

Writing an integration test for an application. An error occurred while running the test:
As I understood from the debugger, my SetUp initializes my variables twice. That is, the variables are initialized, the first test is executed, and then SetUp is initialized again before executing the 2nd test. How can this problem be solved?
My Test:
class MockGroupProvider extends Mock implements GroupProvider {}
class MockStudentProvider extends Mock implements StudentProvider {}
class MockGroupRepository extends Mock implements GroupRepository {}
class MockDatabase extends Mock implements ObjectBox {}
void main() {
final injector = GetIt.instance;
final provider = MockGroupProvider();
final studentProvider = MockStudentProvider();
final repository = MockGroupRepository();
final db = MockDatabase();
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
injector.registerSingleton<GroupProvider>(provider);
injector.registerSingleton<StudentProvider>(studentProvider);
injector.registerSingleton<GroupRepository>(repository);
injector.registerSingleton<ObjectBox>(db);
});
testWidgets(
"Not inputting a text and wanting to save group display an error: "
"Group name cannot be empty",
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: AddGroupPage()));
const IconData iconBtn = Icons.save;
final saveGroupBtn = find.byIcon(iconBtn);
await tester.tap(saveGroupBtn);
await tester.pumpAndSettle();
expect(find.byType(AddGroupPage), findsOneWidget);
expect(find.byType(GroupsPage), findsNothing);
expect(find.text('Group name cannot be empty'), findsOneWidget);
},
);
testWidgets(
"After inputting a text, go to the display page which contains group that same text ",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
const inputText = 'Group 1';
await tester.enterText(
find.byKey(const Key('add_group_field')), inputText);
const IconData iconBtn = Icons.save;
final saveGroupBtn = find.byIcon(iconBtn);
await tester.tap(saveGroupBtn);
await tester.pumpAndSettle();
expect(find.byType(AddGroupPage), findsNothing);
expect(find.byType(GroupsPage), findsOneWidget);
expect(find.text(inputText), findsOneWidget);
},
);
}
The setUp callback runs before each test, therefore it's registering your instances in GetIt multiple times, which will cause an exception to be thrown.
In your case, GetIt should not be necessary here, since no dependency injection appears necessary for your mocks. Instead, you can simply create a new instance of each of your dependencies before each test:
void main() {
late MockGroupProvider provider;
late MockStudentProvider studentProvider;
late MockGroupRepository repository;
late MockDatabase db;
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Before each test, create new instances for each dependency
setUp(() {
provider = MockGroupProvider();
studentProvider = MockStudentProvider();
repository = MockGroupRepository();
db = MockDatabase();
});
// Tests...
}

Run mutliple integration tests in Flutter

I have two integrations test files: login_test.dart and homepage_test.dart (each including single test case with 1 testWidgets method)
I'm using command line to run both tests:
flutter test integration_test/
But the problem is when the test has run and passed login_test . it completely reinstalled my app so second test will fail because have no login credentials (stuck at login screen -> failed)
Any idea how I can solve this ?
Appreciate your helps.
login_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end to end tests', () {
testWidgets('test login', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
final Finder textFieldFinder = find.byType(TextFormField);
expect(textFieldFinder, findsNWidgets(2));
final userName = textFieldFinder.first;
final password = textFieldFinder.last;
await tester.enterText(userName, 'myusername');
await tester.enterText(password, 'mypassword');
expect(find.text('Login'), findsOneWidget);
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
expect(find.byKey(Key('createTicketBtn')), findsOneWidget);
});
});
}
homepage_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end to end tests', () {
testWidgets('test homepage', (tester) async {
app.main();
await tester.pumpAndSettle();
expect(find.byKey(Key('createTicketBtn')), findsOneWidget);
});
});
}
You can use this package to scan a folder inside the integration_test directory for files named like *_test.dart. This will produce a file with a new main importing all test files, so you can just run this new generated file.
You can also create manually a file like this and launch it:
import homepage_test.dart as homepage;
import login_test.dart as login;
void main() {
homepage.main();
login.main();
}
In any case, each test will start from the beginning so you need to login each time. The app will not lose its data though, unless you reset its state in the tearDown.

How to wait until the Finder is visible for next code execution in Flutter integration test?

Information:
I have created a sample Flutter unit test to test the login screen where I have email & password as input field and a login button.
Requirement:
Need to test false cases and for that, I have written code as per the below steps.
Open main.dart
Filled the email & password field
onTap event is done on the login button. Over here API will be called and loader is displayed on the screen until API gets a success or failure response.
Need to check if failure dialog is displayed with a message.
Issue/Query:
Now when the API is calling I want to wait when the loader is visible until the loader is gone. So, as of now I just put a manual delay to execute the next code but I want to make it dynamic. So, let me know how we can put dynamic delay based on the loader visible?
Code:
void main() {
group('App Test', () {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Login Fail Test', (WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 2));
final emailField = find.byType(TextFormField).first;
final passwordField = find.byType(TextFormField).last;
final loginButton = find.byType(RaisedButton).first;
await tester.enterText(emailField, 'Test');
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
await tester.enterText(passwordField, 'Test123');
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
await tester.tap(loginButton);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 3));
final dialog = find.byType(AlertDialog).first;
await tester.element(dialog);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 1));
final dialogButton = find.byType(FlatButton).first;
await tester.tap(dialogButton);
await tester.pumpAndSettle();
await tester.pump(new Duration(seconds: 2));
});
}
I have a file called utils.dart for functionality like this. In this case I use the following function which will basically poll until the finder is valid
// utils.dart
Future<void> pumpUntilFound(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 10),
}) async {
bool timerDone = false;
final timer = Timer(timeout, () => timerDone = true);
while (timerDone != true) {
await tester.pump();
final found = tester.any(finder);
if (found) {
timerDone = true;
}
}
timer.cancel();
}
You can also make it throw an exception if it times out, but the error messages aren't helpful, so I usually follow it up with an expect
It would look like
// my_test.dart
final fab = find.byKey(const ValueKey('fab'));
await pumpUntilFound(widgetTester, fab);
expect(fab, findsOneWidget);
Try wrapping like this:
testWidgets('test',
(WidgetTester tester) async {
await tester.runAsync(() async {
// test code here
});
});
If you use:
await tester.pumpAndSettle();
And then:
final widget = find.byKey(Key('whatever'));
It will find dinamically

Flutter integration_test - initialising and disposing dependencies

I’m initializing the app like
void main() async {
await Initializer.init();
runApp(MyApp());
}
Initializer.init() initializes all dependencies like Firebase Analytics and register things like Hive TypeAdapters.
My sample tests look like:
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets(
'Sample',
(WidgetTester tester) async {
await Initializer.init();
await tester.pumpWidget(MyApp());
await tester.tap(find.text('Something'));
expect(find.text('180 Results'), findsOneWidget);
},
);
testWidgets(
'Sample 2',
(WidgetTester tester) async {
await Initializer.init();
await tester.pumpWidget(MyApp());
await tester.tap(find.text('Something 2'));
expect(find.text('180 Results'), findsOneWidget);
},
);
}
My problem is that after executing the first test, things that were initialized by Initializer.init() aren’t disposed correctly.
Is there a way to tell integration_test to reset the whole environment every time test is reloaded?
Shall I initialize things differently than static Initializer.init();
Shall I explicitly dispose/unregister all dependencies (sth like Initializer.dispose() at the end of every test)? That solution seems to be a nightmare since I'm using many 3rd parties that are not allowing explicit disposing.

Flutter widget test - wait Future completion

I have a widget test in flutter that pumps the widget under test,
and when it opens, performs a background operation (staticWrapper.requestPermission()) which returns a Future and, based on its result, set a state.
The problem is that the test is not waiting this future to complete, test code:
/// When micro permission denied, should show error message.
testWidgets('When micro permission denied, should show error message.',
(WidgetTester tester) async {
when(staticWrapper.requestPermission(Permission.RecordAudio))
.thenAnswer((_) => Future.value(PermissionStatus.denied));
await tester.pumpWidget(widget);
final loginText = find.text(callScreen_microPermissionDenied);
expect(loginText, findsOneWidget);
});
Affected widget code:
void _requestMicroPermission() async {
final result =
await staticWrapper.requestPermission(Permission.RecordAudio);
debugPrint("Microphone permission status: $result");
if (result == PermissionStatus.authorized) {
native.init();
} else {
setState(() {
_loginText = tnsManager.getText(TranslationsManager.callScreen_microPermissionDenied);
});
}
}
The expect() method is called before the setState() call.
Any help?
Finally I solved the issue using WidgetTester#pumpAndSettle():
testWidgets('When micro permission denied, should show error message.',
(WidgetTester tester) async {
when(staticWrapper.requestPermission(Permission.RecordAudio))
.thenAnswer((_) => Future.value(PermissionStatus.denied));
await tester.pumpWidget(widget);
await tester.pump();
final loginText = find.text(callScreen_microPermissionDenied);
expect(loginText, findsOneWidget);
});