Definitive Guide to Unit, Widget and Integration Testing Flutter Apps!
Published: 2022-04-14 01:01:25


Ever wondered why there are so many Flutter development tutorials and guides and yet only a few sources on Flutter testing? Me too! In this blog post I'll show you how to conduct unit, widget and integration testing for Flutter applications.

## Introduction & Apps We'll Be Testing

This blog post will be split into three main parts: Unit testing, Widget testing and Integration testing.

We will be testing a Calorie Tracker Application built in an earlier blog post([click here](https://codinginformer.com/blog/flutter-calorie-tracker-app) to learn how to build a Calorie Tracker app in Flutter). Here is its [GitHub repo link](https://github.com/ShehanAT/flutter_calorie_tracker_app)

Also, for the later part of the Widget Testing section we will be using a sample Flutter application, called `form_app`, contained in [this GitHub repo](https://github.com/ShehanAT/FlutterMiniProjects/tree/master/form_app)

Free feel to clone them and follow along.

Here are some screenshots of the apps we'll be testing:

![Calorie Tracker App Homepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mdwx96pzpbiqvxkfkjqn.png)

_Figure 1: Calorie Tracker App Homepage_


![Calorie Tracker App Day View screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tgmpka5uhok48jmnfz7.png)

_Figure 2: Calorie Tracker App Day View screen_


![Calorie Tracker App History screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aehf143gdvuhy2kdr603.png)

_Figure 3: Calorie Tracker App History screen_


![Calorie Tracker App Settings screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7whj9ls9nakrcy52q54w.png)

_Figure 4: Calorie Tracker App Settings screen_


![Form App Homepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkpwd8708x1j5z7mjmf9.png)

_Figure 5: Form App Homepage_


![Form App Form Widgets Demo screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nqsdpmtxn0sgsel9lgwb.png)

_Figure 6: Form App Form Widgets Demo screen_


![Form App Validation screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0n6r1p2i76hgtuubysmy.png)

_Figure 7: Form App Validation screen_

## Unit Testing

First let's write a couple of unit tests for testing the `FavoriteFoods` class in the models folder of the `calorie_tracker_app` application. To do this we'll have to add the following code to the `calorie_tracker_app/test/unit-tests/models/favorite-food-tests.dart` file:

```
// calorie_tracker_app/test/unit-tests/models/favorite-food-tests.dart

import 'package:calorie_tracker_app/src/model/favorite_foods.dart';
import 'package:calorie_tracker_app/src/model/food.dart';
import 'package:test/test.dart';

void main() {
  group("Testing Model classes", () {
    var favoriteFoods = FavoriteFoods();

    test(
        "Given that we instantiate a FavoriteFoods instance"
        "When new Food instances are added to it"
        "Then the FavoriteFoods instance's _favoriteFoodItems List should contain that Food instance",
        () {
      var newFood = Food(id: 1, food_name: "Sandwich");

      favoriteFoods.add(newFood);

      expect(favoriteFoods.getFavoriteFoodItems.contains(newFood), true);
    });

    test(
        "Given that we instantiate a FavoriteFoods instance"
        "When Food instances are deleted from its _favoriteFoodItems List"
        "Then the FavoriteFoods instance's _favoriteFoodItems List should contain not contain that Food instance",
        () {
      var newFood = Food(id: 2, food_name: "Pasta");

      favoriteFoods.add(newFood);

      expect(favoriteFoods.getFavoriteFoodItems.contains(newFood), true);

      favoriteFoods.remove(newFood);

      expect(favoriteFoods.getFavoriteFoodItems.contains(newFood), false);
    });
  });
}
```

Now for an explanation of the following code:

- First we instantiate an instance `favoriteFoods` of the type `FavoriteFoods`. This model class will be the subject of testing for the two unit tests below
- `test("A new Food instance should be added to Favorite Foods array")`: This unit test will test the `_favoriteFoodItems` array by adding a `Food` instance and then checking for its existence inside the array using the `contains()` method
- `test("A specified Food instance should be deleted from Favorite Foods array")`: This unit test will test the delete functionality of the `FavoriteFood` class's `_favoriteFoodItems` array. This is done by first inserting a `Food` instance into the array and then deleting. Finally, the `expect()` method is used to verify that the deleted item does not exist in the `_favoriteFoodItems` array

Next we'll write some unit tests involving the testing of the `DatabaseService` class in the `calorie_tracker_app` application. This class's main purpose is to communicate with a Firebase Firestore database and manipulate data in the `foodTracks` collection by adding, fetching, or deleting data.

Let's navigate to the `calorie_tracker_app/test/unit-tests/database.dart` file and add the following code to it:

```
// calorie_tracker_app/test/unit-tests/database.dart

import "package:calorie_tracker_app/src/services/database.dart";
import "package:calorie_tracker_app/src/utils/constants.dart";
import 'package:flutter_test/flutter_test.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:calorie_tracker_app/src/model/food_track_task.dart';

void main() {
  DatabaseService databaseService;

  group('testing DatabaseService', () {
    test(
        "Given that we instantiate a DatabaseService instance"
        "When we fetch all foodTrack instances from the Firestore database"
        "Then retrieved List should not be empty", () async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp();
      databaseService =
          DatabaseService(uid: DATABASE_UID, currentDate: DateTime.now());

      List<dynamic> getAllFoodTrackData =
          await databaseService.getAllFoodTrackData();

      print(getAllFoodTrackData);
      expect(getAllFoodTrackData.length > 0, true);
    });

    test(
        "Given that we instantiate a DatabaseService instance"
        "When we fetch all foodTrack instances from the Firestore database and instantiate a FoodTrackTask instance using the first element from that List"
        "Then the FoodTrackTask instance should contain should valid fields",
        () async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp();
      databaseService =
          DatabaseService(uid: DATABASE_UID, currentDate: DateTime.now());

      List<dynamic> getAllFoodTrackData =
          await databaseService.getAllFoodTrackData();

      dynamic firstFoodTrack = getAllFoodTrackData[0];
      FoodTrackTask foodTrack = FoodTrackTask(
          food_name: firstFoodTrack["food_name"],
          calories: firstFoodTrack["calories"],
          carbs: firstFoodTrack["carbs"],
          protein: firstFoodTrack["protein"],
          fat: firstFoodTrack["fat"],
          mealTime: firstFoodTrack['mealTime'],
          createdOn: firstFoodTrack['createdOn'].toDate(),
          grams: firstFoodTrack["grams"]);

      expect(foodTrack.food_name.isEmpty, false);
      expect(foodTrack.calories.isNaN, false);
      expect(foodTrack.carbs.isNaN, false);
      expect(foodTrack.protein.isNaN, false);
      expect(foodTrack.fat.isNaN, false);
      expect(foodTrack.mealTime.isEmpty, false);
      expect(foodTrack.createdOn.isAfter(DateTime.now()), false);
      expect(foodTrack.grams.isNaN, false);
    });
  });
}
```

Now for an explanation for the code above:

- `databaseService`: This is the `DatabaseService` class used to communicate with the Firebase Firestore database. It will be used to fetch all `foodTrack` instances for the purposes of our testing
- `group('testing DatabaseService'`: The `group` function is used to combine tests that are similar in functionality. In our case, we will be grouping all unit tests related to testing the `DatabaseService` class in one `group()` function
- `test('DatabaseService.getAllFoodTrackData()' should return non-empty list...)`: This is our first unit test for testing whether we receive a non-empty list of `foodTrack` instances from the `DatabaseService` class. If you're wondering why the `WidgetsFlutterBinding.ensureInitialized()` and `await Firebase.initializedApp()` methods are called, it is because they are required in order to establish a connection to the Firebase Firestore instance. Once the `foodTrack` instances are assigned to the `List<dynamic> getAllFoodTrackData` variable, we assert that its length must be greater than zero in order for the unit test to pass
- `test('First element of the list returned by DatabaseService.getAllFoodTrackData()...')`: This test is designed to test the fields of the `foodTrack` instances that are received from the Firestore database instance. If we are able to create a `FoodTrackTask` instance, which is a class that uses all the fields in the `foodTrack` instances stored in the Firestore database, using the data recevied from the first `foodTrack` instance in the list that we've retrived, then we know that the instances in the database have all the fields that are required by the application(or at least the first one does ;)). The setup looks identical to the first unit test with the only difference being the creation of the `FoodTrackTask` instance that is created using the data from the first instance contained in the `List<dynamic> getAllFoodTrackData` list variable. The assertions for this test will basically check if the newly created `foodTrackTask` instance's fields contain the data received from the `foodTrack` instance in the Firestore database

Ok that's it for unit testing, let's move on to the Widget Testing section

## Widget Testing

Widget testing can be considered one step up from unit testing because instead of testing a single class and block of code, widget tests, as the name implies, tests widgets. The primary method used for this type of testing is the `WidgetTester` class, which allows for the building and interacting with widgets in a test environment. `WidgetTester` instances are created by using the `testWidgets()` function, which are the functions that encapsulate each individual widget test.

Now let's test the `DatePicker` widget in the Flutter Calorie Tracker application, as shown here:


![DatePicker widget in Day View screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yst7szmeqtz5oldv9se5.png)

_Figure 8: DatePicker widget in Day View screen_

First let's define a `ShowDatePicker` widget in order to test it. In the `calorie_tracker_app/lib/src/page/day-view` folder we'll add the following code to the `showDatePicker.dart` file:

```
// calorie_tracker_app/lib/src/page/day-view/showDatePicker.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class ShowDatePicker extends StatefulWidget {
  @override
  _ShowDatePicker createState() => _ShowDatePicker();
}

class _ShowDatePicker extends State<ShowDatePicker> {
  Color _rightArrowColor = Color(0xffC1C1C1);
  Color _leftArrowColor = Color(0xffC1C1C1);
  DateTime _value = DateTime.now();
  DateTime today = DateTime.now();

  Future _selectDate() async {
    DateTime? picked = await showDatePicker(
      context: context,
      initialDate: _value,
      firstDate: new DateTime(2019),
      lastDate: new DateTime.now(),
      builder: (BuildContext context, Widget? child) {
        return Theme(
          data: ThemeData.light().copyWith(
            primaryColor: const Color(0xff5FA55A), //Head background
          ),
          child: child!,
        );
      },
    );
    if (picked != null) setState(() => _value = picked);
    _stateSetter();
  }

  void _stateSetter() {
    if (today.difference(_value).compareTo(Duration(days: 1)) == -1) {
      setState(() => _rightArrowColor = Color(0xffEDEDED));
    } else
      setState(() => _rightArrowColor = Colors.white);
  }

  String _dateFormatter(DateTime tm) {
    DateTime today = new DateTime.now();
    Duration oneDay = new Duration(days: 1);
    Duration twoDay = new Duration(days: 2);
    String month;

    switch (tm.month) {
      case 1:
        month = "Jan";
        break;
      case 2:
        month = "Feb";
        break;
      case 3:
        month = "Mar";
        break;
      case 4:
        month = "Apr";
        break;
      case 5:
        month = "May";
        break;
      case 6:
        month = "Jun";
        break;
      case 7:
        month = "Jul";
        break;
      case 8:
        month = "Aug";
        break;
      case 9:
        month = "Sep";
        break;
      case 10:
        month = "Oct";
        break;
      case 11:
        month = "Nov";
        break;
      case 12:
        month = "Dec";
        break;
      default:
        month = "Undefined";
        break;
    }

    Duration difference = today.difference(tm);

    if (difference.compareTo(oneDay) < 1) {
      return "Today";
    } else if (difference.compareTo(twoDay) < 1) {
      return "Yesterday";
    } else {
      return "${tm.day} $month ${tm.year}";
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // width: 250,
        body: Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Expanded(
              child: IconButton(
                key: Key("left_arrow_button"),
                icon: Icon(Icons.arrow_left, size: 25.0),
                color: _leftArrowColor,
                onPressed: () {
                  setState(() {
                    _value = _value.subtract(Duration(days: 1));
                    _rightArrowColor = Colors.white;
                  });
                },
              ),
            ),
            Expanded(
              child: TextButton(
                // textColor: Colors.white,
                onPressed: () => _selectDate(),
                // },
                child: Text(_dateFormatter(_value),
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 18.0,
                      fontWeight: FontWeight.w700,
                    )),
              ),
            ),
            Expanded(
              child: IconButton(
                  key: Key("right_arrow_button"),
                  icon: Icon(Icons.arrow_right, size: 25.0),
                  color: _rightArrowColor,
                  onPressed: () {
                    if (today.difference(_value).compareTo(Duration(days: 1)) ==
                        -1) {
                      setState(() {
                        _rightArrowColor = Color(0xffC1C1C1);
                      });
                    } else {
                      setState(() {
                        _value = _value.add(Duration(days: 1));
                      });
                      if (today
                              .difference(_value)
                              .compareTo(Duration(days: 1)) ==
                          -1) {
                        setState(() {
                          _rightArrowColor = Color(0xffC1C1C1);
                        });
                      }
                    }
                  }),
            ),
          ],
        ),
      ),
    );
  }
}
```

Next let's add the corresponding test for the above widget in the `calorie_tracker_app/test/widgets-test/day-view.dart` file:

```
// calorie_tracker_app/test/widgets-test/day-view.dart

void main() {
  testWidgets(
      "Given that ShowDatePicker widget in Day View screen is tested"
      "When the ShowDatePicker widget is rendered"
      "Then it should be found when searching by find.byType()",
      (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;

    await tester.pumpWidget(ShowDatePicker());

    expect(find.byType(ShowDatePicker), findsOneWidget);
    debugDefaultTargetPlatformOverride = null;
  });
}
```

This first `testWidgets()` test is testing whether the `ShowDatePicker` widget is able to be rendered. The `expect` call of looking for one widget of the type `ShowDatePicker` is what validates this testcase.

Now let's look at the other Flutter application used for testing: `form_app`([its GitHub repo here](https://github.com/ShehanAT/FlutterMiniProjects/tree/master/form_app)) where we'll add some more widget tests.

In this app's following file: `form_app/test/widget-tests/form-widgets.dart`, let's add the following code:

```
// form_app/test/widget-tests/form-widgets.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:form_app/src/http/mock_client.dart';
import 'package:form_app/src/sign_in_http.dart';
import 'package:form_app/src/form_widgets.dart';

void main() {
  Future<void> _enterFormWidgetsScreen(WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: FormWidgetsDemo(),
    ));

    await tester.pumpAndSettle();
  }

  testWidgets(
      'Given the user navigates to the Form Widgets screen'
      'When the user types into the title text field'
      'Then the TextFormField widget should contain the matching text',
      (WidgetTester tester) async {
    await _enterFormWidgetsScreen(tester);

    var titleTextFormField = find.byKey(ValueKey("title_text_field"));
    await tester.enterText(titleTextFormField, "Know Thyself");

    await tester.pumpAndSettle();

    expect(find.text("Know Thyself"), findsOneWidget);
    expect(find.text("Know Thyselves"), findsNothing);
  });
}

```

Here is a screenshot of the field we're testing:


![Title field in Form Widgets Demo page for `form_app` application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wonyhrxb02l24xvmsem7.png)

_Figure 9: Title field in Form Widgets Demo page for `form_app` application_

Now for an explanation of what we just added:

- `_enterFormWidgetsScreen()`: This method can be considered a test fixture because it sets up the environment that is to be used for testing. In this case, we navigate to the `FormWidgetsDemo` screen of this application
- `testWidgets(...'Then the TextFormField widget should contain the matching text')`: This test method first navigates to the `FormWidgetsDemo` screen and locates the `TextFormField` widget with the key: `title_text_field`. Then the string `"Know Thyself"` is entered, after which the `expect()` method would search for a widget with a text value of the string we just entered. The assertion of this widget is done by the `expect()` method and we look for the coresponding widget by using the `find.text()` method(more on `find.text()` in [the Flutter docs](https://docs.flutter.dev/cookbook/testing/widget/finders#1-find-a-text-widget))

Let's add one more widget test to the same file:

```
// form_app/test/widget-tests/form-widgets.dart

 testWidgets(
      'Given the user navigates to the Form Widgets screen'
      'When the user selects a date from the DatePicker'
      'Then the DatePicker\'s value should be the picked date',
      (WidgetTester tester) async {
    await _enterFormWidgetsScreen(tester);

    var datePickerFieldEditButton =
        find.byKey(ValueKey("form_date_picker_edit"));
    await tester.tap(datePickerFieldEditButton);

    await tester.pumpAndSettle();

    await tester.tap(find.text("15"));
    await tester.tap(find.text("OK"));

    await tester.pumpAndSettle();

    expect(find.textContaining("/15/"), findsOneWidget);
    expect(find.textContaining("/14/"), findsNothing);
    expect(find.textContaining("/16/"), findsNothing);
  });
```

So this test is targeting the `DatePicker` widget using the `find.byKey()` method(more on `find.byKey()` [in the Flutter docs](https://api.flutter.dev/flutter/flutter_test/CommonFinders/byKey.html)).

Here is a screenshot of it:

![`DatePicker` widget in `form_app`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4jeuevv6oa0ih41wum9c.png)

_Figure 10: `DatePicker` widget in `form_app`_

Then we select the 15th day of whatever month we're currently in and tap the `OK` button to set the value of this `DatePicker` widget. Finally, we validate that the value of the `DatePicker` widget is actually `15` and not `14` or `16`. This widget test is a simple way to test `DatePicker` widgets.

Let's add one widget test to test the `Slider` widget in the `FormWidgetsDemo` screen.

Here is a screenshot of it:

![Slider widget in Form App](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ao6va6uxqvo0oefh9nzz.png)

_Figure 11: Slider widget in Form App_

All we have to do is add the following code to the `widget-tests.dart` file:

```
// form_app/test/widget-tests/form-widgets.dart

  testWidgets(
      'Given the user navigates to the Form Widgets screen'
      'When the user slides the Estimated Value Slider to a certain value'
      'Then the Estimated Value Slider\'s value should be the specified value',
      (WidgetTester tester) async {
    await _enterFormWidgetsScreen(tester);

    var estimatedValueSlider = find.byKey(ValueKey("estimated_value_slider"));

    await SlideTo(tester).slideToValue(estimatedValueSlider, 20);

    await tester.pumpAndSettle();

    Slider slider = tester.firstWidget(estimatedValueSlider);

    expect(slider.value, 100);
    expect(slider.value < 100, false);
    expect(slider.value > 100, false);
  });
```

Also before we forget, the above test requires an extra `extention` method(more on `extension` methods in [the Dart docs](https://dart.dev/guides/language/extension-methods)) in the `form_app/test/extensions/slide-to.dart` file with the following code:

```
// form_app/test/extensions/slide-to.dart
import 'package:flutter_test/flutter_test.dart';

extension SlideTo on WidgetTester {
  Future<void> slideToValue(Finder slider, double value,
      {double paddingOffset = 24.0}) async {
    final zeroPoint = this.getTopLeft(slider) +
        Offset(paddingOffset, this.getSize(slider).height / 2);
    final totalWidth = this.getSize(slider).width - (2 * paddingOffset);
    final calculatedOffset = value * (totalWidth / 100);
    await this.dragFrom(zeroPoint, Offset(calculatedOffset, 0));
  }
}
```

I'll explain what the `slideToValue()` method does shortly...

So the above test first navigates to the `FormWidgetsDemo` screen using the test fixture we defined above. Then, we locate the `Slider` widget by searching for the key value of `estimated_value_slider`.

Now we use the `slideToValue()` extension method in the `slide-to.dart` file to slide the `Slider` widget's value to `100`. Now this method will take some trail and error to get the value perfected. This is because the `value` parameter which is supposed to indicate the value to slide the widget to, does not map one-to-one with the actual `Slider` widget(meaning passing in a `value` value of `10` would slide the `Slider` to `50` whereas a `value` value of `20` would slide the `Slider` to `100`). This is why we have the passed in a value of `20` for our widget test.

Next, we wait for all scheduled frames to stop using the `pumpAndSettle()` method(more on this method in [the Flutter docs](https://api.flutter.dev/flutter/flutter_test/WidgetTester/pumpAndSettle.html)), after which we use the `firstWidget()` method(more on this method in [the Flutter docs](https://api.flutter.dev/flutter/flutter_test/WidgetController/firstWidget.html)) to assign the `Slider` widget to a variable called `slider`. This is done so that the value of this `Slider` widget can be extracted.

Finally, we compare the value of the `Slider` widget and make sure it is `100` using three `expect()` methods.

Next, let's add a widget test to test the error messages from the form field widgets in the `Validation` screen.

Here is a screenshot of the validation error messages we are going to test:

![Validation Errors in `form_app`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hkp05qo02rybvvlckdg.png)

_Figure 12: Validation Errors in `form_app`_

We can do this by adding the following code to the `form_app/test/widget-tests/form-widgets.dart` file:

```
// form_app/test/widget-tests/form-widgets.dart

  Future<void> _enterValidationScreen(WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: FormValidationDemo(),
    ));

    await tester.pumpAndSettle();
  }

  ...
  ...
  ...

  testWidgets(
      'Given the user navigates to the Validation screen'
      'When the user submits the form without any values'
      'Then error messages should be shown under the text fields',
      (WidgetTester tester) async {
    await _enterValidationScreen(tester);

    var submitButton = find.byKey(ValueKey("submit_button"));

    await tester.tap(submitButton);

    await tester.pumpAndSettle();

    expect(find.text("Please enter an adjective."), findsOneWidget);
    expect(find.text("Please enter a noun."), findsOneWidget);
    expect(
        find.text("You must agree to the terms of service."), findsOneWidget);
  });
```

Here is an explanation of the above code:

- `_enterValidationScreen()`: This method is a test fixture that navigates to the `FormValidationDemo` screen
- `testWidgets('... Then error messages should be shown under the text fields')`: This method tests whether the appropriate error messages for having empty text fields upon form submission show up when submitting the form. This is done by first entering the `Validation` screen via `await _enterValidationScreen(tester)` and then tapping the `submitButton`. After waiting for the scheduled frames to die down, we use `find.text()` to check whether the appropriate error messages are present in the screen.

Ok that's it for widget testing, see you in the next section!

## Integration Testing

Now on to integration testing. There are four main screens in the `calorie_tracker_app` application: Homepage, Day View screen, History screen, and Settings screen. So our integration tests will be targeting these screens and their functionalities.

First, let's start with the Homepage screen. Let's go through some of the tests in the `calorie_tracker_app/test/integration-tests/pages/homepage.dart` file:

```
// calorie_tracker_app/test/integration_tests/pages/homepage.dart

 testWidgets(
      "Given the user opens the app"
      "When the user is shown the homepage"
      "Then the user is shown the homepage title", (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    expect(find.text("Flutter Calorie Tracker App"), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
});
```

So this is our first integration test for the Homepage, and it basically renders the Homepage using the `tester.pumpWidget()` method and checks for the existence of the Text widget with the text `"Flutter Calorie Tracker App"`. This text will act as the title for the Homepage.

Next, let's examine a different integration test contained in the `calorie_tracker_app/test/integration_tests/pages/day-view.dart` file:

```
// calorie_tracker_app/test/integration_tests/pages/day-view.dart

  testWidgets(
      "Given user opens the app"
      "When user taps the Day View Screen button"
      "Then Day View Screen is shown", (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    final Finder dayViewButton = find.text("Day View Screen");
    await tester.tap(dayViewButton, warnIfMissed: true);
    await tester.pumpAndSettle();

    expect(find.text("Today"), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
  });
```

This test validates whether tapping on the `"Day View Screen"` button does the expected behavior of navigating to the Day View screen. This is done through first finding the Day View button via the `find.text()` method and then using the `tester.tap()` method to simulate tapping on it.

Afterwards, a `pumpAndSettle()` method is issued to wait for all scheduled frames to stop.

Finally, we use the `expect()` call to find a `TextButton` widget with a text value of `Today`. Finding this widget would definitively indicate that we have indeed navigated to the Day View screen.

Here is another integration test in the `calorie_tracker_app/test/integration_tests/pages/day-view.dart` file:

```
// calorie_tracker_app/test/integration_tests/pages/day-view.dart

 testWidgets(
      "Given user opens the Day View screen"
      "When user taps the Add Food button"
      "Then Add Food modal opens", (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    final Finder dayViewButton = find.text("Day View Screen");
    await tester.tap(dayViewButton, warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("add_food_modal_button")),
        warnIfMissed: true);

    await tester.pumpAndSettle();

    expect(find.byKey(ValueKey("add_food_modal")), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
  });
```

This test's main purpose is to check whether a modal would open when clicking the appropriate button to open it.

Here is a GIF demonstrating this feature:

![Open Add Food modal Workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gopyvaw4cv6ivczowxu1.gif)

_Figure 13: Open Add Food modal Workflow_

First, we navigate to the Day View screen, locate the `IconButton` widget with a `+` icon as its value and then tap it.

After waiting for a `tester.pumpAndSettle()` call to complete, we use the `find.byKey()` method to check whether the modal has been opened. The `find.byKey()` method, which you can learn more about in [the Flutter docs](https://docs.flutter.dev/cookbook/testing/widget/finders#2-find-a-widget-with-a-specific-key), uses the `key` parameter specified in select widgets to check for the modal's existence on the screen. To elaborate this finding process further: the `find.byKey` method searches for a `ValueKey` class instance that is used as the `key` parameter value in widgets.

Next up, let's test whether adding a new food entry via the Add Food modal would create a `FoodTile` instance in the bottom portion of the Day View screen.

To give a better idea of this workflow, here is a GIF that goes through the process:

![Create Food Track Entry Workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cm394qrw1qt03bgii9ly.gif)

_Figure 14: Create Food Track Entry Workflow_

Here is the test that is validating this feature, contained in the `calorie_tracker_app/test/integration_tests/pages/day-view.dart` file:

```
// calorie_tracker_app/test/integration_tests/pages/day-view.dart

testWidgets(
      "Given user opens the Day View Screen"
      "When user submits the Add Food modal form"
      "Then a new FoodTrack instance is created", (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;

    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    final Finder dayViewButton = find.text("Day View Screen");
    await tester.tap(dayViewButton, warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("add_food_modal_button")),
        warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_food_name_field")), "Cheese");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_calorie_field")), "500");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_carbs_field")), "15");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_protein_field")), "25");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_fat_field")), "20");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_grams_field")), "20");

    await tester.tap(find.byKey(ValueKey("add_food_modal_submit")));

    await tester.pumpAndSettle();

    expect(find.text("Cheese").at(0), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
  });
```

Picking up from the previous integration tests, we tap the `+` `IconButton` widget and enter the details of a food item in the form that is presented in the modal. The entering of text is done by the `tester.enterText()` method, more details of which can be learned through [the Flutter docs](https://api.flutter.dev/flutter/flutter_test/WidgetTester/enterText.html).

Then, after hitting the `Submit` button we check for the existence of a `FoodTile` instance that should contain the data entered in the form. Namely, the food name `Cheese` should be present in the first position of the list that is shown in the Day View screen.
We make sure to explicitly check the first element in the food list, via `find.text("Cheese").at(0)`, because as integration tests are run multiple times there are chances that the newly created food entry might render off screen, toward the bottom. In order to avoid complications of having to scroll down, we only check the first item in the food track list.

Here's another interesting integration test for the Day View screen in the `calorie_tracker_app` app:

```
// calorie_tracker_app/test/integration_tests/pages/day-view.dart

 testWidgets(
      "Given user opens the Day View screen"
      "When the user taps the Left Arrow Button then Right Arrow Button"
      "Then DatePicker's value changes from Yesterday to Today",
      (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    final Finder dayViewButton = find.text("Day View Screen");
    await tester.tap(dayViewButton, warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("left_arrow_button")),
        warnIfMissed: true);
    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("right_arrow_button")),
        warnIfMissed: true);
    await tester.pumpAndSettle();

    expect(find.text("Today"), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
  });
```

So this test is designed to test the `ShowDatePicker` widget's functionality of moving between dates upon tapping the arrow buttons.

Here is a GIF showing this functionality:

![Day View Screen Date Switching Workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5r7vkfwqykyevxgovm9.gif)

_Figure 15: Day View Screen Date Switching Workflow_

After the usual `tester.tap()` method that simulates the tapping on the left and right arrow buttons we switch from yesterday's date to today's date and then check for the string `'Today'` via `find.text()` in order to validate this testcase.

Ok, last but not least we can add this testcase in the Day view integration test file:

```
// calorie_tracker_app/test/integration_tests/pages/day-view.dart

 testWidgets(
      "Given user opens the Day View Screen"
      "When the user taps a Food Tile Delete Button"
      "Then that Food Tile is removed from the Food Track List",
      (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    await SharedPreferencesService().init();
    await tester.pumpWidget(CalorieTrackerApp());

    final Finder dayViewButton = find.text("Day View Screen");
    await tester.tap(dayViewButton, warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("add_food_modal_button")),
        warnIfMissed: true);

    await tester.pumpAndSettle();

    Random random = new Random();
    int randomNumber = random.nextInt(100);
    String foodName = "Cheese" + randomNumber.toString();

    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_food_name_field")), foodName);
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_calorie_field")), "500");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_carbs_field")), "15");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_protein_field")), "25");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_fat_field")), "20");
    await tester.enterText(
        find.byKey(ValueKey("add_food_modal_grams_field")), "20");

    await tester.tap(find.byKey(ValueKey("add_food_modal_submit")));

    await tester.pumpAndSettle();

    await tester.dragUntilVisible(
        find.ancestor(
            of: find.text(foodName), matching: find.byType(ExpansionTile)),
        find.byKey(ValueKey("food_track_list")),
        const Offset(0, 500));

    await tester.tap(
        find.ancestor(
            of: find.text(foodName), matching: find.byType(ExpansionTile)),
        warnIfMissed: true);

    await tester.pumpAndSettle();

    await tester.tap(find.byKey(ValueKey("delete_button")), warnIfMissed: true);

    await tester.pumpAndSettle();

    expect(find.text(foodName), findsNothing);

    debugDefaultTargetPlatformOverride = null;
  });
```

Here is a GIF demostrating what this test encompases:

![Add Then Delete Food Entry Workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6haxxx22luxj2j6ax4wp.gif)

_Figure 16: Add Then Delete Food Entry Workflow_

Quite possibly our most involved integration test, this test validates the ability to add and then delete a food entry. Here is a detailed breakdown of this test:

- First, the Add Food modal is opened and the details of a new food is added. The reason for generating a random food name, by adding a random number to the string `"Cheese"`, is to prevent duplicate food names in the food list that can be problematic when using `find.text()` to search for the newly created food entry in the food list.
- After adding a new food entry a new food item will appear in the food list. Now before searching for the newly added food entry we make sure to scroll to the bottom of the food list via the `tester.dragUntilVisible()` method. This method drags the current `view` until the first parameter is visible on screen(for more info on `dragUntilVisible()` here is the [Flutter docs link](https://api.flutter.dev/flutter/flutter_test/WidgetController/dragUntilVisible.html)).
- Speaking of the first parameter which in this case is the newly added food entry, the `find.ancestor()` method is used in combination with `find.text()` to find the food tile itself.
- The `find.text()` method would locate the food entry `Text` widget and then the `find.ancestor()` method finds the parent widget containing that `Text` widget(more on the `find.ancestor()` in [the Flutter docs](https://api.flutter.dev/flutter/flutter_test/CommonFinders/ancestor.html)). This parent widget is the food tile itself.
- Now with the appropriate food tile located we can tap it via the `tester.tap()` method.
- Afterwards, we find the delete button via the `find.byKey()` method and tap it.
- Finally, we validate that the deleted food tile is not able to be found via the `expect()` method.

Ok although we can go on and on with more examples of integration testing, let's stop here for the sake of brevity.

## Conclusion

Whew! If you made it this far, congrats! You now know some ways of testing Flutter applications! Thanks for reading this blog post.

If you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.

If you found this article helpful please share it and make sure to follow me on [Twitter](https://twitter.com/Shehan_Atuk) and [GitHub](https://github.com/ShehanAT), connect with me on [LinkedIn](https://www.linkedin.com/in/shehan-atukorala-8631491a5/) and subscribe to my [YouTube channel](https://www.youtube.com/channel/UCtxed_NljgtAXrQMMdLvhrQ).