Integration Testing

    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.