Integration Testing in Flutter: Getting Started
Learn how to test UI widgets along with the backend services in your Flutter project using Integration Testing. By Monikinderjit Singh.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Integration Testing in Flutter: Getting Started
30 mins
- Getting Started
- Setting Up a Firebase Project
- Setting Up The Android Project
- Setting Up The iOS Project
- Setting Up Authentication and Firestore
- Exploring the Starter Project
- Testing in Flutter
- Comparing Types of Testing
- Examining Unit Testing
- Examining Widget Testing
- Examining Integration Testing
- Setting Up the Project for Testing
- Adding Dependencies
- Creating a Test Directory
- Writing Your First Test
- Diving into LiveTestWidgetsFlutterBinding
- Grouping Tests
- Testing Feature One: Authentication
- Understanding pumpWidget and pumpAndSettle
- Diving Into Widget Keys
- Adding Fake Delays
- Understanding expect()
- Running the Test
- Testing Feature Two: Modifying Ideas
- Inserting New Ideas in a List
- Deleting an Idea
- Where to Go From Here?
Testing is a must-have skill and a vital part of the software development process. It ensures that your software products are bug-free and meet their specifications.
Michael Bolton’s quote illustrates the importance of testing:
The problem is not that testing is the bottleneck. The problem is that you don’t know what’s in the bottle. That’s a problem that testing addresses.
Companies of all sizes need expert testers. They help streamline your development process while saving you time and money. Learning to test is a win-win scenario.
In this tutorial, you’ll learn about integration testing for your Flutter app. More specifically, you’ll:
- Learn about different types of testing in Flutter.
- Write integration tests for Flutter.
- Access the state of the app.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
The starter project is a complete app. Your primary goal throughout this tutorial is to test it before production.
Open the starter project in VS Code or Android Studio. This tutorial uses VS Code, so you may need to change some of the instructions if you decide to go with Android Studio.
After opening the project, run flutter pub get
to install the packages this project uses. Now you’ll set up Firebase and its services for the project.
Setting Up a Firebase Project
Before moving ahead with integration testing, you need to install the Google services configuration file.
Sign in to Firebase. Click Add Project.
Enter the project’s name and then click Continue.
Then click Continue again.
You successfully created the Firebase project. Next, you’ll set up Android and iOS projects. Start with Android.
Setting Up The Android Project
The starter project depends on Firebase, so you need to configure your project to access Firebase. You’ll start by configuring the Android project. From your project page in Firebase, click the Android button.
Enter the package name. You can get your package name from android/app/build.gradle.
Click Register App and then download the google-services.json file. Put the google-services.json file in the android/app directory.
That’s it. The remaining necessary code is already in the starter project. Next, you’ll configure your iOS project to access Firebase.
Setting Up The iOS Project
To configure the iOS project to access your Firebase project, follow these steps:
Click Add app to add a new app.
Click the iOS button to add the iOS app.
Enter the bundle ID, the same one you used when setting up the android app. Additionally, you can open ios/Runner.xcodeproj/project.pbxproj and search for PRODUCT_BUNDLE_IDENTIFIER
.
Click Register App and then download GoogleService-Info.plist. Move the file to the ios/Runner folder by replacing the existing GoogleService-Info.plist.
Now, open the iOS project in Xcode. In Xcode, right-click the Runner folder and choose Add files to Runner….
Finally, add GoogleService-Info.plist.
Good job. :]
The rest of the necessary code is already in the starter project. With the devices set up, you only need to set up the services the project uses.
Setting Up Authentication and Firestore
This section will guide you through the steps required to set up Authentication and Firestore services.
In the authentication tab, click Get started.
In the Sign-in method tab, select Email/Password.
Then enable the Email/Password toggle button and click Save.
In the Firestore Database tab, click the Create Database button.
Select Start in test mode and click next.
Click the next.
Bravo! You successfully added the devices. Finally, you need to configure your Firebase project. To do this, head over to the Firebase documentation and follow step one and step two.
Build and run the project. You’ll see the login and sign up page:
You’re ready to explore the starter project.
Exploring the Starter Project
Explore the starter project. You’ll see that it uses Provider
for state management and Firebase for authentication and storage.
The home screen displays the user’s ideas. An idea is stored in a collection named ideas
in Firestore.
To insert a new idea, tap the floating action button. Whenever you insert a new idea, the list updates using IdeasModel
which extends ChangeNotifierProvider
.
To delete an idea, swipe the idea tile horizontally.
With the steps above, you began to create your tests. In this next section, you’ll learn about testing in Flutter.
Testing in Flutter
While understanding what types of testing are available in Flutter is important, knowing when to use which type is critical.
Currently, there are three categories of testing in Flutter:
- Unit Testing
- Widget Testing
- Integration Testing
Now you’ll learn more about these types by comparing them.
Comparing Types of Testing
You’ll compare the different types of tests available in Flutter to help you decide which type of test is right for you.
You can compare the tests based on multiple parameters:
- Goal: The test’s ultimate goal.
- Point to Note: Important point to remember.
- When to Use: When you should use this type of test.
- Confidence: Confidence you have in the test’s ability to show that the product does what you expected it to.
- Speed: The test’s execution speed.
First, you’ll look at unit testing.
Examining Unit Testing
With Unit Testing, your goal is to test the smallest testable piece of code, including, but not limited to classes, and functions. Normally, unit tests run in an isolated environment, where services are mocked with faked data in order to test the output of the testable unit.
Parameter | Explanation |
---|---|
Goal | Test the logic of a single unit of functions/methods. |
Point to Note | Requires an isolated system and thus no connection to real-world APIs. Must use mock credentials/service to mimic the real world API service. |
When to Use | 1. To test the new feature/logic. 2. Validate code. 3. Making a Proof of Concept. |
Confidence | Low |
Speed | Highest |
Examining Widget Testing
With Widget Testing, the goal is to assert that a single widget behaves deterministically, based on possibly mocked inputs. Similar to unit testing, Widget tests are normally run in an isolated environment, where input data can be mocked.
Parameter | Explanation |
---|---|
Goal | Verifying appearance and interaction of a single widget in the widget tree. |
Point to Note | Requires isolated test environment with appropriate widget lifecycle context. |
When to Use. | When testing a single widget rather than testing a full-blown app. |
Confidence | Moderate |
Speed | Medium |
Examining Integration Testing
Integration tests involve testing multiple testable pieces. Integration tests are often not-run in an isolated environment, and often mimic real-world conditions.
Parameter | Explanation |
---|---|
Goal | Verifying that all widgets, along with their backend services, are working as expected. |
Point to Note | Used to test larger parts of the app. The testing process works just like the real-world app. |
When to Use | 1. Before deploying the app. 2. After connecting different unit tests. 3. Testing user-based scenarios. |
Confidence | High |
Speed | Slowest |
In this tutorial, you’ll focus on integration testing in Flutter.
Setting Up the Project for Testing
To create integration tests for Flutter, you’ll use the integration_test
package. In the past, you would have used flutter_driver
, but Flutter discontinued it for the following reasons:
- Difficulty in catching exceptions.
- Hard interaction with the app components like
showBottomSheet
. - Poor readability of the API.
- Difficulty in verifying the state of the app.
The integration_test
package solves these issues.
Now it’s time to learn how to configure your project to write integration tests. First, you’ll add all the required dependencies.
Adding Dependencies
The Flutter SDK includes integration_test
package, so you don’t need to copy it from the Pub.dev website. Instead, you just need to add integration_test
to your pubspec.yaml.
Open pubspec.yaml and replace # TODO: add integration test dependency here
with:
integration_test:
sdk: flutter
Don’t forget to run flutter pub get
in the terminal.
Now, you’ll create the test directory.
Creating a Test Directory
In the project’s root, create a folder named integration_test. This folder will act as a directory for all of the project’s integration tests.
Inside the integration_test folder, create a dart file named app_test.dart. This file will include your integration tests.
Now, it’s time to start writing integration tests.
Writing Your First Test
In app_test.dart, insert:
void main() {
}
This function is the first called when running the tests. You’ll write all tests inside this function.
At the top of app_test.dart, insert:
import 'package:integration_test/integration_test.dart';
Here, you import the integration_test
package making it ready to use in the file.
Now, inside main()
, add:
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Here, ensureInitialized()
verifies the integration test driver’s initialization. It also reinitializes the driver if it isn’t initialized.
Next, you’ll learn how to use the LiveTestWidgetsFlutterBinding
method.
Diving into LiveTestWidgetsFlutterBinding
Insert the following code at the top of app_test.dart:
import 'package:flutter_test/flutter_test.dart';
This code includes the flutter_test
package required for configuring the test.
Then, add this code block below the binding
variable you defined before:
if (binding is LiveTestWidgetsFlutterBinding) {
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
}
LiveTestWidgetsFlutterBindingFramePolicy
defines how LiveTestWidgetsFlutterBinding
should paint frames. fullyLive
is a policy used to show every frame requested by the framework and is best suited for heavy animations.
Checkout the official docs for other policies that might make more sense for your app.
Next, you’ll work on grouping tests.
Grouping Tests
At the last line inside main()
, insert:
group('end-to-end test', () {
//TODO: add random email var here
//TODO: add test 1 here
//TODO: add test 2 here
});
The group()
method groups and runs many tests. You include it here since you’ll run multiple tests in the app. It has the following arguments:
- description: A description of the test group.
- void Function() body: A function defining what tests to run.
-
skip: An optional argument used to skip the test group. Since it’s dynamic, if the value is
String
rather thantrue
, it’ll print the value ofString
when skipping the test.
Now you have all the skills you need to create your first test!
Testing Feature One: Authentication
Most apps start with an authentication screen, and this project is no different. Therefore, you’ll start by writing an authentication test first.
Write the following code in place of //TODO: add random email var here
:
final timeBasedEmail = DateTime.now().microsecondsSinceEpoch.toString() + '@test.com';
This code creates a time-based email address.
Now, replace //TODO: add test 1 here
with:
testWidgets('Authentication Testing', (WidgetTester tester) async {
//TODO: add Firebase Initialization Here
});
testWidgets()
lets you define tests for widgets and takes two required parameters:
- description: Defines what the test is about.
-
callback function: A function that executes during the test. It takes a
WidgetTester
object as a parameter. ThisWidgetTester
object interacts with the widgets and the test environment.
The callback function is asynchronous because your test will interact with real-world APIs.
testWidgets()
also has some optional parameters like skip
and timeout
.
timeout
is the maximum time required to run the test. After that time, the test will fail automatically. It’s ten minutes by default.
Insert the following lines at the top of app_test.dart:
import 'package:firebase_core/firebase_core.dart';
This code lets you initialize your Firebase app during the test.
Replace //TODO: add Firebase Initialization Here
with:
await Firebase.initializeApp();
This code ensures your app is ready to use Firebase services.
Now, you’ll work with pumpWidget
and pumpAndSettle
.
Understanding pumpWidget and pumpAndSettle
At the top of app_test.dart, add:
//1
import 'package:ideast/main.dart';
//2
import 'package:flutter/material.dart';
Here’s a code breakdown:
- Imports main.dart to get access to
MyApp()
. - Imports material.dart to access Flutter widgets.
Now, below the code you just added, add:
await Firebase.initializeApp(); // previous code
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
//TODO: Add here
Here’s an explanation of the code:
-
pumpWidget()
renders the UI of the provided widget. Here you passMyApp()
as the rendering widget.PumpWidget
also takesduration
as a parameter, which will shift the fake clock by the specified duration to help you avoid excessive frame rates.duration
is the most suitable option when you know how many frames will render, such as navigation without animations. -
pumpAndSettle()
repeatedly callspump
for a given duration until there are no frames to settle, which is usually required when you have some animations.pumpAndSettle
is called afterpumpWidget()
because you want to wait for the navigation animations to complete.
Replace //TODO: Add code here
with:
await tester.tap(find.byType(TextButton));
//TODO: Add code here
tap()
is a method in WidgetTester that lets you tap the centre of the widget. This requires a Finder
to tell the framework to tap it.
The byType
property defines the type of widget. TextButton
and ElevatedButton
are acceptable but not abstract classes such as StatefulWidget
.
Replace //TODO: Add code here
with:
//1
tester.printToConsole('SignUp screen opens');
//2
await tester.pumpAndSettle();
//3
await tester.enterText(find.byKey(const ValueKey('emailSignUpField')), timeBasedEmail);
Here’s a code breakdown:
-
printToConosle
prints statements during the test. Including a few descriptive statements provides a sense comfort that the tests are running. - Waits for all animations to settle down.
- In the text field, you can enter text using the tester’s
enterText
property. There are two parameters: aFinder
and aString
.String
enters this text into the Text field.byKey
finds widgets using their widget keys.
In the next section, you’ll learn how to interact with the widgets using keys.
Diving Into Widget Keys
Keys help you store and interact directly with widgets by acting as identifiers for them. Use unique keys because non-distinct keys can make widgets work unexpectedly.
These are the different types of widget keys:
- ValueKey: Uses a string as its value.
- ObjectKey: Uses complex object data as its value.
- UniqueKey: A key with a unique value.
- GlobalKey: A key that is globally available in WidgetTree, for example, Form keys.
It’s important to add keys in their correct place. So, open signup_screen.dart and replace //TODO: add value key for signup email textFormField
with:
key: const ValueKey('emailSignUpField'),
This code assigns a constant ValueKey
to the email TextFormField
.
In signup_screen.dart, replace //TODO: add value key for signup password textFormField
with:
key: const ValueKey('passwordSignUpField'),
Here, you assign a constant ValueKey
to the password TextFormField
.
In signup_screen.dart, replace //TODO: add value key for 'Confirm Password' textFormField
with:
key: const ValueKey('confirmPasswordSignUpField'),
Here, you assign a constant ValueKey
to confirm password TextFormField
.
Back in app_test.dart, insert the following code below the previous block:
// previous code
await tester.enterText(
find.byKey(const ValueKey('emailSignUpField')), timeBasedEmail);
//1
await tester.enterText(
find.byKey(const ValueKey('passwordSignUpField')), 'test123');
await tester.enterText(
find.byKey(const ValueKey('confirmPasswordSignUpField')), 'test123');
//2
await tester.tap(find.byType(ElevatedButton));
//TODO: add addDelay() statement here
Here’s what you did:
- The test framework enters Password and confirm details in respective text fields when the SignUp Screen opens.
- Then it taps the ElevatedButton to register the user and triggers a register user event.
Next, you’ll add fake delays to your code.
Adding Fake Delays
You need to add fake delays because integration tests interact with APIs in the real world. As a result, API results fetch late, and the test framework should wait for them. Otherwise, the subsequent statements will require the results from the previous ones, causing the test to fail.
To add a fake delay, insert the following code before main()
:
Future<void> addDelay(int ms) async {
await Future<void>.delayed(Duration(milliseconds: ms));
}
addDelay()
adds fake delays during the testing.
Next, replace //TODO: add addDelay() statement here
with:
await addDelay(24000);
This code adds a delay of 24 seconds.
Insert the following line immediate after the addDelay()
:
await tester.pumpAndSettle();
This code waits for all the animations to complete.
Understanding expect()
You’ve done the work to inject data into your tests. Next it’s important to verify that your tests succeed with the given data. You verify expectations with the expect()
method.
expect()
is an assert method that verifies that the Matcher and expected value match.
Write the following code immediately after the call to pumpAndSettle()
:
expect(find.text('Ideas'), findsOneWidget);
//TODO: call logout function here
Here, you find the text “Ideas” in the UI. The expectation is that there must be only one widget, as you can see in the image above.
findsOneWidget
is a Matcher constant, which means only one widget should be present.
Insert the following code outside main()
:
//1
Future<void> logout(WidgetTester tester) async {
//2
await addDelay(8000);
//3
await tester.tap(find.byKey(
const ValueKey('LogoutKey'),
));
//4
await addDelay(8000);
tester.printToConsole('Login screen opens');
await tester.pumpAndSettle();
}
Here’s a code breakdown:
- You create an asynchronous function for the logout event which helps to make code modular. There’s one argument to this function: a
WidgetTester
object that describes how the current test framework works. - Then you add a fake delay of eight seconds.
- You tap ghd Logout button after a successful signUp.
- Finally, you add a fake delay of eight seconds and print that the login screen opens.
Next, replace //TODO: call the logout function here
with:
await tester.pumpAndSettle();
expect(find.text('Ideas'), findsOneWidget); // previous code
await logout(tester);
This code calls logout()
which makes the user sign out.
Amazing! You’re ready to run your first test.
Running the Test
Build and run the project by typing the following command into your terminal:
flutter test integration_test/app_test.dart
You’ll see:
Congratulations on writing your first integration test!
Now you’re ready to create another test to handle the app’s more complex features and states.
Testing Feature Two: Modifying Ideas
In this section, you’ll learn about complex testing and methods for accessing context.
You’ll follow steps similar to those you did in Testing Feature One: Authentication.
Replace //TODO: add test 2 here
with:
//1
testWidgets('Modifying Features test', (WidgetTester tester) async {
//2
await Firebase.initializeApp();
//3
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
//4
await addDelay(10000);
// TODO: add code here
});
Here, you:
- Create a new test with the description
Modifying Features test
. - Then you wait for the test framework to initialize the Firebase app.
- Render your
MyApp
widget to show the login screen and wait for all of the frames to settle. - Add a fake delay of ten seconds, so that the database synchronization can complete.
Next, replace // TODO: add code here
with:
//1
await tester.enterText(find.byKey(const ValueKey('emailLoginField')), timeBasedEmail);
await tester.enterText(find.byKey(const ValueKey('passwordLoginField')), 'test123');
await tester.tap(find.byType(ElevatedButton));
//2
await addDelay(18000);
await tester.pumpAndSettle();
Here’s the explanation for the numbered comments:
- After the login screen opens, you insert the values of email and password in their respective text fields and then tap
ElevatedButton
to trigger the login event. - Add a fake delay of 18 seconds and waiting for all animations to complete.
In the next section, you’ll write code to test adding new ideas to Firestore.
Inserting New Ideas in a List
First, insert this code below the call to pumpAndSettle()
:
//1
await tester.tap(find.byType(FloatingActionButton));
await addDelay(2000);
tester.printToConsole('New Idea screen opens');
await tester.pumpAndSettle();
//2
await tester.enterText(find.byKey(const ValueKey('newIdeaField')), 'New Book');
await tester.enterText(find.byKey(const ValueKey('inspirationField')), 'Elon');
//3
await addDelay(1000);
Here, you:
- Add a new idea to the list by clicking the
FloatingActionButton
on screen. Then the IdeaScreen should open with two text form fields. - Find the text field using
ValueKey
and insert values into it. - Wait for one second to make sure framework finishes entering values.
In the image above, the test fails because the ElevatedButton
is below the keyboard so, the framework won’t find the widget. The next code block solves this issue.
Now, insert this code below the previous block:
await tester.ensureVisible(find.byType(ElevatedButton)); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); //TODO: add code here
ensureVisible()
ensures the widget is visible by scrolling up if necessary.
Replace //TODO: add code here
with:
await addDelay(4000);
tester.printToConsole('New Idea added!');
await tester.pumpAndSettle();
await addDelay(1000);
After submitting the idea, the code waits while the Firestore database updates.
You successfully added the testing code for the new idea feature.
Now, here’s the hard part: deleting an idea. How can you use swipe gestures in automated testing? The next section will explain.
Deleting an Idea
Now you’ll gain some insight into accessing the app’s state. Additionally, you’ll learn how to use drag gestures within the test environment.
At the top of app_test.dart, insert:
//1
import 'package:ideast/model/idea_model.dart';
//2
import 'package:provider/provider.dart';
Here a code breakdown:
- You import
IdeasModel
to access the Ideas list. - You’ll access the Ideas list using the
Provider
package. So, you import this package into the file.
Next, below the previous code, insert:
//1
final state = tester.state(find.byType(Scaffold));
//2
final title = Provider.of<IdeasModel>(state.context, listen: false).getAllIdeas[0].title;
final tempTitle = title!;
await addDelay(1000);
//TODO: add deletion code here
Here’s what you added:
-
state()
accesses the state of the current widget tree. It requires aFinder
as a parameter. In this statement, you need the state of the current Scaffold widget. - Using context, you’ll access the list of ideas present in the
IdeasModel
provider, storing title in a temporary variable tempTitle for later use.
Then, replace //TODO: add deletion code here
with:
await tester.drag(
find.byKey(ValueKey(
tempTitle.toString(),
)),
const Offset(-600, 0),
);
await addDelay(5000);
Here, drag()
scrolls or drags the widget horizontally or vertically by the given offset.
You can give a relative offset as:
- Offset(x,0): Drag towards right
- Offset(-x,0): Drag towards left
- Offset(0,y): Drag downwards
- Offset(0,-y): Drag upwards
Now, insert this code below the call to addDelay()
:
expect(find.text(tempTitle), findsNothing);
await logout(tester);
Here’s what this code does:
- The first line uses
expect()
to verify that the idea with the title tempTitle is not present. - The second line calls the
logout()
function to make the user logout.
Build and run the app. You’ll see:
Now you’ve earned the title of Integration Test Super hero for successfully learning about integration testing. Bravo!
Where to Go From Here?
You can download the complete project using the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you learned when to use each type of testing, and how to:
- Test in Flutter.
- Write integration tests in Flutter UI.
- Access State.
- Create gestures in the test environment.
You can use services like Firebase Test Lab to test your app on multiple devices.
To learn more about testing, check out our tutorials on Unit Testing and Widget Testing.
If you have any suggestions, questions or if you want to show off what you did to improve this project, join the discussion below.