Summary

In this lesson we'll cover:

  • Integration testing concepts
  • Working with the flutter_driver package
  • Working with widget keys
  • Writing an integration test

The Code for This Lesson

You can check out the step/step09 branch here which will contain the code for this lesson.

Core Concepts

  • Integration tests test how pieces of your application work together as a whole.
  • A typical integration test runs the actual app in an automated fashion and simulates the user interacting with the app.

How Integration Tests Work in Flutter

  • With an integration test, one bit of code will run (or "drive") the actual app and another separate bit of code will simulate user interaction.
  • The flutter_driver package will run or "drive" our app for us.

"Driving" Our Application

  • We'll use the flutter_driver package to start our app.

Adding flutter_driver

  • Add the following to your pubspec.yaml file:
# pubspec.yaml

# ...

dev_dependencies:
  # ...
  flutter_driver:
    sdk: flutter

  # ...

Adding test/test_driver/app.dart

  • All files for integration tests will resides in a new directory, test/test_driver
  • flutter_driver is initiated within a new main() function within a new file called test/test_driver/app.dart
  • flutter_driver then will invoke the main() function of our app located in package:tourismandco/main.dart:
// test/test_driver/app.dart

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

void main() {
  // This line enables the extension
  enableFlutterDriverExtension();

  // Call the `main()` function of your app or call `runApp` with any widget you
  // are interested in testing.
  app.main();
}

NOTE: Original source here.

Writing an Integration Test

  • flutter_driver will provide a handy command to run this test/test_driver/app.dart file: flutter drive --target=test_driver/app.dart
  • It will run any test files located in the same directory.
  • So now let's write an actual test file (ending with _test.dart), create test/test_driver/app_test.dart.
  • But not so fast, read on.

Before We Continue

  • Our tests will need to see if specific widgets exist with specific bits of text, such as a location name.
  • How will be "find" these widgets then?
  • What we will do is add a special property to certain widgets, called a "key".

Understanding What Widget Keys Are

  • Widgets can be uniquely identified in the "widget tree" by a "key".
  • The "widget tree" is a hierarchical representation of a given screen in a Flutter app.
  • A widget tree is akin to the "DOM" in HTML. This tree structure allows Flutter to build a tree of widgets in memory.
  • When Flutter needs to update specific parts of the UI, representing the UI as a "tree" makes it easy to do this.
  • Therefore, a "key" is a unique string associated with the widget.
  • The "key" is effectively say, the equivalent to the "id" property of a
    in HTML.
  • Keys can be used to optimize the performance of an app in certain situations, but are generally optional.
  • For our purposes, we can use a "key" that are tests can use as well.
  • NOTE: many integration testing frameworks for the web work the same way\

Adding Widget Keys for our Test

  • Locate lib/screens/locations/locations.dart
  • Add the following key to the existing code:
// lib/screens/locations/locations.dart

// ...

// NOTE _itemBuilder() is can existing method you'll have implemented in this file,
// so you're just adding the 'key' property here...
Widget _itemBuilder(BuildContext context, Location location) {
    return GestureDetector(
      // ...
      key: Key('location_list_item_${location.id}'),

// ...
  • Here, each widget returned by our _itemBuilder() is now uniquely identifyable in the Flutter widget tree.
  • Next, update the folowing file to add another key to our LocationTile widget:
// lib/widgets/location_tile.dart

// ...

@override
Widget build(BuildContext context) {
// ...

return Container(
    // ...
        children: [
        Text(
            // ..
            key: Key('location_tile_name_${location.id}'),

// ...
  • Here, you see we have another uniquely named key for the specific Text widget in our LocationTile.

Implemeting our Integration Test

  • We can now write our test, which will assume our app is already running via our "driver", implemented earlier in this tutorial.
  • We will fetch all locations and wait until the Locations screen is loaded.
  • The test will do a basic check to see if the location list is loaded, by finding a widget by the key we defined in the previous step.
// test/test_driver/app_test.dart

// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
import 'package:tourismandco/models/location.dart';

void main() {
  group('happy path integration tests', () {
    final locations = Location.fetchAll();

    // First, define the Finders. We can use these to locate Widgets from the
    // test suite. Note: the Strings provided to the `byValueKey` method must
    // be the same as the Strings we used for the Keys in step 1.
    final locationListItemTextFinder =
        find.byValueKey('location_list_item_${locations.first.id}');

    FlutterDriver driver;

    // Connect to the Flutter driver before running any tests
    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    // Close the connection to the driver after the tests have completed
    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('a location name appears in location list', () async {
      // Use the `driver.getText` method to verify the counter starts at 0.
      expect(await driver.getText(locationTileOverlayTextFinder), isNotEmpty);
    });

    // NOTE one more test to come in the next step!
  });
}

Running Our Integration Test

  • Since we are using a custom command to run our integration test, flutter drive <...>, we can't just right click this test file and expect it to run.
  • We can configure a custom command in Visual Studio Code to run these tests but that's outside the scope of this tutorial, so we will run it via the command line.
  • Open a command prompt via the Terminal app in macOS or the Command Prompt in Windows.
  • Ensure you have flutter set in your PATH environment variable. If you do not, check out my previous video on how to set up Flutter on macOS or Windows.
  • Ensure an iOS Simulator or Android Emulator is running.
  • Now run cd <PATH TO YOUR tourismandco app> then flutter drive --target=test_drive/app.dart
  • The integration test should now run.

Adding One Additional Test Case

  • As you can see, we created a "finder" which allowed us to find a widget and make a test assertion.
  • Let's add another test case, this time ensuring that we can tap on a specific location, transition to the Location Detail screen and that specific text there appears properly:
// test/test_driver/app_test.dart

// ...

void main() {
  group('happy path integration tests', () {
    // ...

    final locationTileOverlayTextFinder =
        find.byValueKey('location_tile_name_${locations.first.id}');

    // ...

    test('a location in the list is tappable', () async {
      // First, tap on the button
      await driver.tap(locationTileOverlayTextFinder);

      // Use the `driver.getText` method to verify the counter starts at 0.
      expect(await driver.getText(locationTileOverlayTextFinder), isNotEmpty);
    });

    // ...
  • Here, we are adding an additional finder and test case in the same group() test case we've implemented in the previous step.
  • Re-run the test to ensure everything passes correctly.

Summary

  • In this tutorial, we learned a very important part to app testing which is integration testing.
  • Integration tests ensure that each major piece our app works together without any crashes, failure to load data, order incorrect rendering issues.

Sources

  • For the original documentation on this, click here.