The Complete Guide to Integration Testing in Flutter

In the first episode of this testing series, we looked at Unit Testing in Flutter. Afterward, we took a look at Widget Testing in Flutter.
This article requires knowledge about Unit & Widget Testing. If you do not know it yet, take a look at my article about Unit Testing and my article about Widget Testing and come back here afterward!
With that being said, let’s get started with Integration Testing!
Integration Testing
So, what is Integration Testing?
Integration test is the process of testing multiple components of an app together, to ensure that they function correctly as a whole.
This means checking that the different parts of an app work well together, like making sure all pieces of a puzzle fit to form the right picture.
We also call Integration Testing “end-to-end testing”.
Starting Point for our Integration Testing
To get to know integration testing, we are going to test a simple login progress.
We have a login screen that has a text field for email and password, as well as a button “Login”. When the username is “tomic” and the password is “12345”, then the app pushes to the Home Screen (a simple screen with a text). If not, we are showing an error dialog stating that something went wrong.
We are going to use the following code as the starting point:
main.dart
import 'package:flutter/material.dart';
import 'package:intro_to_testing/login_screen.dart';
void main() {
runApp(
const MaterialApp(
home: const LoginPage(),
),
);
}2. login_screen.dart
import 'package:flutter/material.dart';
import 'package:intro_to_testing/home_screen.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
void _login() {
// Validate login credentials and navigate to home screen if valid
if (_usernameController.text == 'tomic' &&
_passwordController.text == '12345') {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const HomeScreen(),
),
);
} else {
// Show error message if credentials are invalid
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: const Text('Invalid username or password'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
},
);
}
}
@override
void dispose() {
super.dispose();
_usernameController.dispose();
_passwordController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
),
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _login,
child: const Text('Login'),
),
],
),
),
);
}
}3. home_screen.dart
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: const Center(
child: Text('Home Screen'),
),
);
}
}With these big code chunks out of the way, let’s finally start testing!
Writing our Integration Tests
To get started, first, add the integration_test package to your dev dependencies. It is from the Flutter SDK, so we do not rely on any third-party libraries:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutterGreat. Now, instead of writing our tests in the test folder, we are going to create a separate folder named integration_test at the highest level of our project. Create a new file called app_test.dart inside of this folder. Of course, if you have a bigger application and test separate parts, you can use more descriptive names, but because we are literally going to test the whole app, we are going to leave it like this for now.
Great. Now let’s think about what we want to do:
- We obviously want to define a new test. We already learned that in Unit & Widget Testing.
- We want to build our whole app. Unlike Widget Testing, we want to run the whole app, including all the real functionalities, etc. This means we need to find a way to call the
mainfunction from ourmain.dartin the lib folder without confusing it with themainfunction of our integration test - We want to type in the username and password
- We want to click on the “Login” button
- We want to check if the app actually navigates to the home screen
I am going to share our whole test here now and explain each part inside of the code. I think this makes it way easier to understand:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intro_to_testing/home_screen.dart';
// We are going to prefix this with app, as we do not want to call the
// main function of our integration test over and over again
import 'package:intro_to_testing/main.dart' as app;
void main() {
// This does the work of WidgetsFlutterBinding.ensureInitialized();
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets(
'Login screen with valid username and password',
(widgetTester) async {
// Call the main function from our lib folder.
// If we wouldn't have prefixed it with app., the program would not know
// which main function we want to call
app.main();
// We wait till everything is loaded, just like we did in widget tests
await widgetTester.pumpAndSettle();
// We want to enter the text in our text fields. To find our widgets
// we use find.byType. The problem: We have two TextFormFields
// There are multiple approaches on how to solve this. One is to use
// .at(n), just like we did below. A more complex, but also the recommended
// approach is to give the TextFormFields some Key and then use
// find.byKey().
await widgetTester.enterText(
find.byType(TextFormField).at(0), 'tomic');
await widgetTester.enterText(
find.byType(TextFormField).at(1), '12345');
// Press the login button
await widgetTester.tap(find.byType(ElevatedButton));
// We wait till everything is loaded / the app pushed to the new screen
await widgetTester.pumpAndSettle();
// Now we want to check if the app actually displays our home screen
expect(find.byType(HomeScreen), findsOneWidget);
},
);
}And we have just written our first Integration Test! Now you can start your simulator and run this test.
Before reading on, do you want to boost your Flutter knowledge in no-time?
Join 100+ Flutter developers getting high-quality articles every week.
100% free!
But… normally you are supposed to see something happen in the Simulator. Why don’t we see anything?
The answer is simpler than you might think: The program executes everything unbelievably fast. If you want to see what happens step by step, just add a Future.delayed() with a duration of 1 or 2 seconds between each step.
Let’s implement our test for the case when the input is not valid. Try it by yourself, you only need to change 4 lines of code.
Okay, let’s take a look at it:
testWidgets(
'Login screen with invalid username and password',
(widgetTester) async {
app.main();
await widgetTester.pumpAndSettle();
// Enter a wrong username and/or password
await widgetTester.enterText(
find.byType(TextFormField).at(0), 'ups');
await widgetTester.enterText(
find.byType(TextFormField).at(1), 'badpassword');
await widgetTester.tap(find.byType(ElevatedButton));
await widgetTester.pumpAndSettle();
// Check if the alert dialog is present
expect(find.byType(AlertDialog), findsOneWidget);
},
);And voilà, we just added integration testing for our whole application. It’s literally that easy!
Final Words
We have now learned how to apply integration testing in your app. There may be some rare cases I haven’t covered yet. If you find one of those, please let me know in the comments. I will either edit this article to include it, or I will create a separate article if I haven’t covered a bigger case. But there will be an update, I promise!
If you liked this article, you will likely like my other articles too. You can subscribe to my newsletter to get high-quality articles into your inbox once a week, or read some more articles of mine (Or simply do both :)
Stackademic 🎓
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord
- Visit our other platforms: In Plain English | CoFeed | Venture | Cubed
- More content at Stackademic.com






