Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

In this codelab, you'll build and test a simple Flutter app. The app will use the Provider package for managing state.

What you'll learn

What you'll build

In this codelab, you'll start by building a simple application with a list of items. We provide the source code for you so you can get right to the testing. The app supports the following operations:

Once the app is complete, you will write the following tests:

  • Unit tests to validate the add and remove operations
  • Widgets tests for the home and favorites pages
  • UI and performance tests for the entire app using Flutter Driver

GIF of the app running on Android

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for an example code to use in my project. I'm looking for an explanation of something specific.

Create a new Flutter app & update dependencies

This codelab focuses on testing a Flutter mobile app. You will quickly create the app to be tested using source files that you copy and paste. The rest of the codelab then focuses on learning different kinds of testing.

Create a simple templated Flutter app, using the instructions in Getting Started with your first Flutter app. Name the project testing_app (instead of myapp). You'll be modifying this starter app to create the finished app.

In your IDE or editor, open the pubspec.yaml file. Add the following dependencies marked as new, then save the file. (You can delete the comments to make the file more readable.)

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.3
  provider: ^4.1.3   # new

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_driver:    # new
    sdk: flutter     # new
  test: ^1.14.4      # new
  1. Click the Pub get button in your IDE or, at the command line, run flutter pub get from the top of the project.

If this results in an error, make sure that the indentation in your dependencies block is exactly the same as shown above, using spaces (not tabs). YAML files are sensitive to white space.

Next, you'll build out the app so that you can test it. The app contains the following files:

Replace the contents of lib/main.dart

Replace the contents of lib/main.dart with the following code:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';
import 'package:testing_app/screens/home.dart';

void main() {
  runApp(TestingApp());
}

class TestingApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp(
        title: 'Testing Sample',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        routes: {
          HomePage.routeName: (context) => HomePage(),
          FavoritesPage.routeName: (context) => FavoritesPage(),
        },
        initialRoute: HomePage.routeName,
      ),
    );
  }
}

Add the Home page in lib/screens/home.dart

Create a new directory, screens, in the lib directory and, in that newly created directory, create a new file named home.dart. In lib/screens/home.dart add the following code:

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Testing Sample'),
        actions: <Widget>[
          FlatButton.icon(
            textColor: Colors.white,
            onPressed: () {
              Navigator.pushNamed(context, FavoritesPage.routeName);
            },
            icon: Icon(Icons.favorite_border),
            label: Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(
    this.itemNo,
  );

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? Icon(Icons.favorite)
              : Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            Scaffold.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Add the Favorites page in lib/screens/favorites.dart

In the lib/screens directory create another new file named favorites.dart. In that file add the following code:

lib/screens/favorites.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  static String routeName = '/favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  final int itemNo;

  const FavoriteItemTile(
    this.itemNo,
  );

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            Scaffold.of(context).showSnackBar(
              SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Lastly, create the Favorites model in lib/models/favorites.dart

Create a new directory, models and, in that directory, create a new file named favorites.dart. In that file add the following code:

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

The app is now complete, but untested.

Run the app by clicking the Run icon in the editor . The first time you run an app, it can take a while. The app is faster in later steps. It should look like the following screenshot:

The app shows a list of items. Tap the heart-shaped icon on any row to fill in the heart and add the item to the favorites list. The Favorites button on the AppBar takes you to a second screen containing the favorites list.

The app is now ready for testing. You'll start testing it from the next step.

You'll start by unit testing the favorites model. What is a unit test? A unit test verifies that every individual unit of software (often a function) performs its intended task correctly.

All the test files in a Flutter app (except for those using Flutter Driver) are placed in the test directory.

Remove test/widget_test.dart

Before you begin testing, delete the widget_test.dart file. You'll be adding your own test files.

Create a new test file

First, you'll test the add() method in the Favorites model to verify that a new item gets added to the list, and that the list reflects the change. By convention, the directory structure in the test directory mimics that in the lib directory and the Dart files have the same name, but appended with _test.

Create a models directory in the test directory. In this new directory, create a favourites_test.dart file with the following content:

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('App Provider Tests', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });    
  });
}

The Flutter testing framework allows you to bind similar tests related to each other in a group. There can be multiple groups in a single test file intended to test different parts of the corresponding file in the /lib directory.

The test() method takes two positional parameters: the description of the test and the callback where you actually write the test.

Test removing an item from the list. Copy and paste the following test in the same test group. Add the following code to the test file:

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

Run the test

If your app is running in your emulator or device, close it before continuing.

At the command line, navigate to the project's root directory and enter the following command:

$ flutter test test/models/favorites_test.dart 

If everything works, you should see a message similar to the following:

00:06 +2: All tests passed!                                                    

The complete test file: test/models/favorites_test.dart.

For more information on unit testing, visit An introduction to unit testing.

In this step you'll be performing widget tests. Widget testing is unique to Flutter, where you can test each and every individual widget of your choice. This step tests the screens (HomePage and FavoritesPage) individually.

Widget testing uses the testWidget() function instead of the test() function. It also takes two parameters: the description, and the callback. But here, the callback takes a WidgetTester as an argument.

Widget tests use TestFlutterWidgetsBinding, a class that provides the same resources to your widgets that they would have in a running app (information about screen size, the ability to schedule animations, and so on), but without the actual app. Instead, a virtual environment is used to run the widget, measure it, and so on, then tests the results. Here, pumpWidget kicks off the process by telling the framework to mount and measure a particular widget just as it would in a complete application.

The widget testing framework provides finders to find widgets (for example, text(), byType(), byIcon()) and also matchers to verify the results.

Start by testing the HomePage widget.

Create a new test file

The first test verifies whether scrolling the HomePage works properly.

Create a new file in the test directory and name it home_test.dart. In the newly created file, add the following code:

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(find.byType(ListView), Offset(0, -200), 3000);
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

The createHomeScreen() function is used to create an app that loads the widget to be tested in a MaterialApp, wrapped into a ChangeNotifierProvider. The HomePage widget needs both of these widgets to be present above it in the widget tree so it can inherit from them and get access to the data they offer. This function is passed as a parameter to the pumpWidget() function.

Next, test whether the framework can find a ListView rendered onto the screen.

Add the following code snippet to home_test.dart:

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {  
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });                                                
  // END OF NEW CONTENT

  testWidgets('Testing Scrolling', (tester) async {       
    await tester.pumpWidget(createHomeScreen());
    expect(find.text('Item 0'), findsOneWidget);
    await tester.fling(find.byType(ListView), Offset(0, -200), 3000);
    await tester.pumpAndSettle();
    expect(find.text('Item 0'), findsNothing);
  });
});

Run the test

You can run widget tests in the same way as unit tests, but using a device or an emulator allows you to watch the test running. It also gives you the ability to use hot restart.

Plug-in your device or start your emulator.

From the command line, navigate to the project's root directory and enter the following command:

$ flutter run test/home_test.dart 

If everything works you should see an output similar to the following:

Launching test/home_test.dart on Mi A3 in debug mode...
Running Gradle task 'assembleDebug'...                                  
Running Gradle task 'assembleDebug'... Done                        62.7s
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk...                 5.8s
Waiting for Mi A3 to report its views...                            16ms
I/flutter ( 1616): 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Mi A3...                                        
I/flutter ( 1616): 00:02 +1: Home Page Widget Tests Testing Scrolling
Syncing files to device Mi A3...                                                 4,008ms (!)                                       

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on Mi A3 is available at:
http://127.0.0.1:40433/KOsGesHSxR8=/
I/flutter ( 1616): 00:00 +0: Home Page Widget Tests Testing if ListView shows up
I/flutter ( 1616): 00:02 +1: Home Page Widget Tests Testing Scrolling
I/flutter ( 1616): 00:09 +3: All tests passed!

Next, you'll make changes to the test file and enter Shift + R to hot restart the app and re-run all the tests.

Add more tests to the group that tests the HomePage widgets. Copy the following test to your file:

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

This test verifies that tapping the IconButton changes from Icons.favorite_border (an open heart) to Icons.favorite (a filled-in heart) and then back to Icons.favorite_border when tapped again.

Enter Shift + R. This hot restarts the app and re-runs all the tests.

The complete test file: test/home_test.dart.

Use the same process to test the FavoritesPage with the following code. Follow the same steps and run it.

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

This test verifies whether an item disappears when the close (remove) button is pressed.

For more information on widget testing, visit:

The Flutter Driver is used to test how individual pieces work together as a whole. This is Flutter's version of Selenium WebDriver (generic web), Protractor (Angular), Espresso (Android), or Earl Gray (iOS).

Flutter Driver tests require the flutter_driver dev_dependency, which you added to pubspec.yaml when setting up the project in the Getting started step.

Instrument the app

In order to write a Flutter Driver test, you must first instrument the app. Instrumenting the app means configuring the app so that the driver can access its GUI and functions for the purpose of creating and running an automated test. Flutter driver tests are placed in a directory called test_driver. In this step, you'll add the following files for integration testing:

Create a directory called test_driver in the project's root directory. In that newly created directory, create an app.dart file and add the following code:

test_driver/app.dart

import 'package:flutter_driver/driver_extension.dart';
import 'package:testing_app/main.dart' as app;

void main() {
  enableFlutterDriverExtension();
  app.main();
}

This code enables the Flutter Driver extension and then runs the app's main() function. You can also call runApp() with any widget you are interested in testing.

Write the test

Create a new file and name it app_test.dart. The name of the test file must correspond to the name of the file that contains the instrumented app, with _test added at the end. Therefore, in this example, name it test_driver/app_test.dart.

test_driver/app_test.dart

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Testing App Performance Tests', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        await driver.close();
      }
    });  
  });
}

The setUpAll() function connects the app to the driver extension that you enabled in app.dart. The tearDownAll() function closes the connection after the tests have completed.

Next, test the scrolling performance of the app and record it using the traceAction() function.

Paste the following code into the test group you just created:

test_driver/app_test.dart

test('Scrolling test', () async {
  final listFinder = find.byType('ListView');

  final scrollingTimeline = await driver.traceAction(() async {
    await driver.scroll(listFinder, 0, -7000, Duration(seconds: 1));
    await driver.scroll(listFinder, 0, 7000, Duration(seconds: 1));
  });

  final scrollingSummary = TimelineSummary.summarize(scrollingTimeline);
  await scrollingSummary.writeSummaryToFile('scrolling', pretty: true);
  await scrollingSummary.writeTimelineToFile('scrolling', pretty: true);
});

This test scrolls through the list of items really fast and then scrolls all the way up. The traceAction() function records the actions and generates a Timeline object out of it. Then, the Timeline is converted into a TimelineSummary and stored to disk in the form of JSON summaries.

Next, test the add and remove operations and record their performance in a similar way.

Paste the following test into the same group:

test_driver/app_test.dart

test('Favorites operations test', () async {
  final operationsTimeline = await driver.traceAction(() async {
    final iconKeys = [
      'icon_0',
      'icon_1',
      'icon_2',
    ];

    for (var icon in iconKeys) {
      await driver.tap(find.byValueKey(icon));
      await driver.waitFor(find.text('Added to favorites.'));
    }

    await driver.tap(find.text('Favorites'));

    final removeIconKeys = [
      'remove_icon_0',
      'remove_icon_1',
      'remove_icon_2',
    ];

    for (final iconKey in removeIconKeys) {
      await driver.tap(find.byValueKey(iconKey));
      await driver.waitFor(find.text('Removed from favorites.'));
    }
  });

  final operationsSummary = TimelineSummary.summarize(operationsTimeline);
  await operationsSummary.writeSummaryToFile('favorites_operations', pretty: true);
  await operationsSummary.writeTimelineToFile('favorites_operations', pretty: true);
});

Run the test

Plug-in your device or start your emulator.

At the command line, navigate to the project's root directory and enter the following command:

$ flutter drive --target=test_driver/app.dart --profile

If everything works, you should see an output similar to the following:

Starting application: test_driver/app.dart
Installing build/app/outputs/apk/app.apk...                        10.7s
Running Gradle task 'assembleProfile'...                                
Running Gradle task 'assembleProfile'... Done                      18.5s
✓ Built build/app/outputs/apk/profile/app-profile.apk (9.5MB).
I/flutter ( 5351): Observatory listening on http://127.0.0.1:41621/IDIS06Lv8ro=/
00:00 +0: Testing App Driver Tests (setUpAll)

VMServiceFlutterDriver: Connecting to Flutter application at http://127.0.0.1:38871/IDIS06Lv8ro=/
VMServiceFlutterDriver: Isolate found with number: 465902271016059
VMServiceFlutterDriver: Isolate is paused at start.
VMServiceFlutterDriver: Attempting to resume isolate
VMServiceFlutterDriver: Waiting for service extension
VMServiceFlutterDriver: Connected to Flutter application.
00:01 +0: Testing App Driver Tests Scrolling test

00:09 +1: Testing App Driver Tests Favorites operations test

00:12 +2: Testing App Driver Tests (tearDownAll)

00:12 +2: All tests passed!

Stopping application instance.

After the test completes successfully, the build directory at the root of the project should contain two timeline summary files and two timeline files:

The complete test file: test_driver/app_test.dart.

For more details on Flutter Driver (Integration) testing, visit:

You've completed the codelab and have learned different ways to test a Flutter app.

What you've learned

To learn more about testing in Flutter, visit