Home Flutter & Dart Tutorials

Integration Testing in Flutter: Getting Started

Learn how to test UI widgets along with the backend services in your Flutter project using Integration Testing.

Version

  • Dart 2.5, Flutter 2.5, VS Code

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.
Note: This tutorial requires basic knowledge of Flutter. If you’re new to Flutter, take a look at our Getting Started with Flutter and Firestore Tutorial for Flutter before continuing.

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.

This tutorial uses Flutter version 2.5.

After opening the project, run flutter pub get to install the packages this project uses.

Build and run the project. You’ll see the login and sign up page:

Flutter integration Test Starter

First, 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.

Firebase Project Title

Then click Continue again.

Confirm new Firebase project

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.

Firebase android

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. Replace the existing google-services.json in the android/app with the one you downloaded.

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.

Add new Device Firebase

Click the iOS button to add the iOS app.

Add Firebase IOS device

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.

IOS Bundle ID

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.

Firebase Authentication Getting Started

In the Sign-in method tab, select Email/Password.

Firebase sign in screen

Then enable the Email/Password toggle button and click Save.

Enable email/password firebase

In the Firestore Database tab, click the Create Database button.

Firestore Getting Started

Select Start in test mode and click next.

Firestore enable test mode

Click the next.

Bravo! You successfully added the devices. Now you can 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.

Home page

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.

Deleting idea

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.

Flutter Unit Test

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.

Flutter Widget Testing

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.

Flutter Integration Testing

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
Note: When adding a dependency, make sure you use the correct indentation, otherwise you’ll get an indentation error.

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.

Integration Test directory

Note: It’s best practice to create a new folder for each different type of test.

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 than true, it’ll print the value of String 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.

Note: In automated testing, adding time-based/random credentials is useful because you might forget to add a new email when registering a user. Otherwise, you’ll need to either update the test email or delete the email from Firebase authentication every time you run the test. Furthermore, updating the email every time defeats the purpose of automated testing.

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. This WidgetTester 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:

  1. Imports main.dart to get access to MyApp().
  2. Imports material.dart to access Flutter widgets.

Now, below the code you just added, add:

await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();

//TODO: Add here

Here’s an explanation of the code:

  1. pumpWidget() renders the UI of the provided widget. Here you pass MyApp() as the rendering widget. PumpWidget also takes duration 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.

  2. pumpAndSettle() repeatedly calls pump for a given duration until there are no frames to settle, which is usually required when you have some animations. pumpAndSettle is called after pumpWidget() 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:

  1. printToConosle prints statements during the test. Including a few descriptive statements provides a sense comfort that the tests are running.
  2. Waits for all animations to settle down.
  3. In the text field, you can enter text using the tester’s enterText property. There are two parameters: a Finder and a String. 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.

Widget 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.

Next, insert the following code below the previous block:

//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:

  1. The test framework enters Password and confirm details in respective text fields when the SignUp Screen opens.
  2. 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.

Note: This delay may be different for your device type depending on the processor and internet quality. So, you can increase or decrease the duration accordingly.

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

Expect method

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:

  1. 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.
  2. Then you add a fake delay of eight seconds.
  3. You tap ghd Logout button after a successful signUp.
  4. 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 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:

Authentication Integration Test Flutter

Congratulations on writing your first integration test!

Flutter congratulations

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:

  1. Create a new test with the description Modifying Features test.
  2. Then you wait for the test framework to initialize the Firebase app.
  3. Render your MyApp widget to show the login screen and wait for all of the frames to settle.
  4. 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:

  1. 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.
  2. 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:

  1. Add a new idea to the list by clicking the FloatingActionButton on screen. Then the IdeaScreen should open with two text form fields.
  2. Find the text field using ValueKey and insert values into it.
  3. Wait for one second to make sure framework finishes entering values.

Invisible widget Flutter

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.

Test completing happiness

Now, here’s the hard part: deleting an idea. How can you use swipe gestures in automated testing? The next section will explain.

Creature with a monacle, thinking

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:

  1. You import IdeasModel to access the Ideas list.
  2. 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:

  1. state() accesses the state of the current widget tree. It requires a Finder as a parameter. In this statement, you need the state of the current Scaffold widget.
  2. 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(
    title.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:

Flutter Integration Test Complete

Now you’ve earned the title of Integration Test Super hero for successfully learning about integration testing. Bravo!

Integration test super hero

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.

Contributors

Comments

Reviews

More like this