Flutter Testing

Flutter Testing: A Comprehensive Guide to Unit, Widget, and Integration Testing

Wafa mohamed
Stackademic
Published in
14 min readJan 28, 2024

--

Table of Contents

  1. Test pyramids Types.
  2. Comparison of different kinds of testing.

3. Building UI to test it.

4. Unit Testing and mockito article

5. Widget Testing

6. Integration Testing

7. Additional Example[Optional]: Flutter CodeLabs (unit, widget, integration testing,Performance profiling)

Introduction

Testing is a crucial aspect of software development, ensuring the reliability and functionality of applications. In the context of Flutter, testing can be categorized into unit, widget, and integration. This article explores these testing techniques using practical examples. The more features your app has, the harder it is to test manually. Automated tests help ensure that your app performs correctly before you publish it.

1.Test Pyramid

1- unit Test: tests a single function, method, or class.

2- Widget Test (Component Test): (in other UI frameworks referred to as component test) tests a single widget.

3- Integration Test (End-to-End Testing or GUI testing): tests a complete app or a large part of an app.

2.Comparison of different kinds of testing

3.Building the UI

This example will display:

The InteractiveShowcasePage is a Flutter StatefulWidget designed to showcase the interaction between a button and a dynamically changing message. The page includes a list of strings, and upon tapping the “Tap Me!” button, the displayed message transitions to the next string in the list. The overarching theme of this example is to illustrate the responsiveness of the UI to user actions.

UI

Code:

import 'package:flutter/material.dart';

class InteractiveShowcasePage extends StatefulWidget {
const InteractiveShowcasePage ({super.key});

@override
_InteractiveShowcasePageState createState() => _InteractiveShowcasePageState();
}

class _InteractiveShowcasePageState extends State<InteractiveShowcasePage > {
// List of strings to be displayed on the screen
List<String> listOfStrings = [
'Hello',
'testing',
'widget testing',
'integration testing',
'unit testing',
'Flutter',
'Community',
'Welcome',
'Dart',
'Programming',
];
// Index to keep track of the current string being displayed
int currentIndex = 0;
// Function to increment the index and change the displayed string
void incrementFunction() {
setState(() {
currentIndex = (currentIndex + 1) % listOfStrings.length;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.purple[100],
appBar: AppBar(
backgroundColor: Colors.purple[100],
centerTitle: true,
title: const Text('Flutter Testing'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
listOfStrings[currentIndex],
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: incrementFunction,
child: const Text('Tap Me!'),
),
],
),
),
);
}
}
Simple UI App

4.Unit Testing

Unit testing involves testing individual units of code in isolation to ensure they perform as expected. In Flutter, the test package provides the core framework for writing unit tests. Let’s consider an example: the Increment Function. and the flutter_test package provides additional utilities for testing widgets.

  1. Add the test or flutter_test dependency.

2. Create a test file inside a test folder and remove the default test file.

testing_app/
lib/
interactive_showcase_page.dart
test/
interactive_showcase_unit_test.dart

3. Write a test for our class or Combine multiple tests in a group.

// Import the Flutter testing or test library 
import 'package:flutter_test/flutter_test.dart';

// Define the main function for the test
void main() {
// Define a unit test description
test("Unit test to check list increment based on current index", () {
// Initialize variables: currentIndex, and a list of strings
int currentIndex = 0;
final list = ["a", "b", "c"];

// Define a function named incrementFunction
void incrementFunction() {
// Increment currentIndex and cycle through the list
currentIndex = (currentIndex + 1) % list.length;
}

// Verify the initial state of currentIndex
expect(currentIndex, 0);

// Call incrementFunction and verify the new state of currentIndex
incrementFunction();
expect(currentIndex, 1);

// Call incrementFunction again and verify the updated state
incrementFunction();
expect(currentIndex, 2);

//Try to fail the test by adding index out of range.
// incrementFunction();
// expect(currentIndex, 5);
});
}

Explanation:

  • The test function is used to define a unit test. It takes two parameters a description string and a callback function containing the actual test logic.
  • Use expect to check if currentIndex has the expected value after calling incrementFunction.
  • you can add group function if you have more than one test case.

4. Run the tests.

//To run the all tests from the terminal
flutter test test/shocase_test.dart

5. Test Result

Test Passed :)
  • You can also read this article to test with MOCKITO for classes that depend on database or web services:

ARTICLE: Mocking in Flutter: Testing Data Providers and Web Services for Quotes App

5. Widget Testing

To test widget classes, you need a few additional tools provided by the flutter_test package, which ships with the Flutter SDK. The flutter_test package provides the following tools for testing widgets:

  • The WidgetTester allows building and interacting with widgets in a test environment.
  • The testWidgets() function automatically creates a new WidgetTester for each test case, and is used in place of the normal test() function.
  • The Finder classes allow searching for widgets in the test environment.
  • Widget-specific Matcher constants help verify whether a Finder locates a widget or multiple widgets in the test environment.
  1. Add the flutter_test dependency.

2. Add a new file (widget_test.dart) inside the test folder.

testing_app/
lib/
interactive_showcase_page.dart
test/
interactive_showcase_unit_test.dart
interactive_showcase_widget_test.dart

3. Create a testWidgets test :

Build the widget using the WidgetTester and Search for the widget using a Finderand Verify the widget using a Matcher.

// Import Flutter material and testing libraries
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// Import the widget to be tested
import 'package:flutter_testing/view/pages/interactive_showcase_page_example/interactive_showcase_page_example.dart';

// Define the main test function
void main() {
// Define a testWidget asynchronous function that take 2 parameter (description and callback function)
testWidgets("Test Widget for increment text", (WidgetTester tester) async {
// Build our widget inside a MaterialApp and trigger a frame.
await tester.pumpWidget(
const MaterialApp(
home: InteractiveShowcasePage(),
),
);

// Verify that the initial text is displayed.
final finder = find.text("Hello");
expect(finder, findsOneWidget);

// Tap the button and trigger a frame.
await tester.tap(find.byType(ElevatedButton));
await tester.pump(); // after tap, we need to wait for the execution of one frame

// Verify that the text has changed.
expect(finder, findsNothing);
// After the previous tap, the initial text will be removed, and we find the new text
final finderNextWord = find.text("testing");
expect(finderNextWord, findsOneWidget);
});
}

Explanation:

  • The testWidgets function is used to define a widget test. It takes a description string and a callback function containing the actual test logic.
  • The pumpWidget the function builds the widget inside a MaterialApp and triggers a frame.
  • The find.text function is used to locate the text widget with the content "Hello," and expect verifies that it finds exactly one widget.
  • The tester.tap function is used to simulate tapping on the ElevatedButton.
  • After the tap, tester.pump() is used to wait for the execution of one frame.
  • Verify that the initial text “Hello” is no longer found (findsNothing), indicating it has been removed.
  • Locate the new text widget with the content “testing” and verify that it’s found.

4. Test Result

Passed :)
Test Passed

6.Integration Testing

Integration testing involves testing multiple units or components together to ensure they work as a whole. Integration tests run on a real device or emulator. Integration tests are written using the integration_test package, provided by the SDK.

  1. Add the integration_test dependency.

2. Add New Folder Called (integration_test) and inside it add the test files.

testing_app/
lib/
interactive_showcase_page.dart
main.dart
integration_test/
app_test.dart
integration_test folder

3. Write the integration test.

// Import necessary Flutter libraries and packages
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/main.dart' as app;
import 'package:integration_test/integration_test.dart';

// Define the main function for the integration test
void main() {
// Ensure that the IntegrationTestWidgetsFlutterBinding is initialized
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

// Define a test group named "end to end test"
group("end to end test", () {
// Define a widget test named "increment function test"
testWidgets("increment function test", (WidgetTester tester) async {
// Run the main function of the app
app.main();

// Wait for the app to finish its initial frame
await tester.pumpAndSettle();

// Find the initial text widget with content 'Hello'
final initialTextFinder = find.text('Hello');
// Verify that the initial text widget is found
expect(initialTextFinder, findsOneWidget);

// Find the elevated button widget
final elevatedButtonFinder = find.byType(ElevatedButton);
// Tap the elevated button
await tester.tap(elevatedButtonFinder);

// Wait for the animation to complete
await tester.pumpAndSettle();

// Find the updated text widget with content 'testing'
final updatedTextFinder = find.text('testing');
// Verify that the updated text widget is found
expect(updatedTextFinder, findsOneWidget);
});
});
}

Explanation:

  1. Alias Main Dart File:
  • flutter_testing/main.dart (as app): Refers to the main.dart file in our project and is aliased as app.

2. Initialize Integration Test Binding:

  • IntegrationTestWidgetsFlutterBinding.ensureInitialized();: singleton service that executes tests on a physical device.

3. Define Test Group and Widget Test:

  • group("end to end test", () { ... });: Defines a test group.
  • testWidgets("increment function test", (WidgetTester tester) async { ... });: Defines a widget test named "increment function test." This test simulates interactions with widgets and verifies their behavior.

4. Run the App’s Main Function:

  • app.main();: Runs the main function of the app. This initializes the app's initial state.

5. Wait for Initial Frame:

  • await tester.pumpAndSettle();: Waits for the app to finish its initial frame.

6. Find and Verify Initial Text:

  • final initialTextFinder = find.text('Hello');: Locates the text widget with the content 'Hello.'
  • expect(initialTextFinder, findsOneWidget);: Verifies that exactly one widget with the specified text is found.

7. Find and Tap Elevated Button:

  • final elevatedButtonFinder = find.byType(ElevatedButton);: Locates the elevated button widget.
  • await tester.tap(elevatedButtonFinder);: Simulates tapping on the elevated button.
  • await tester.pumpAndSettle();: Waits for the app to stabilize after the button tap.

4. Run Test.

Integration Test

4. Test Result.

Test passed :)

7.Additional Example: Flutter CodeLabs [Optional]

By reaching this point, you’ve covered various aspects of testing in Flutter. If you choose to skip the additional example, you’ve already gained valuable knowledge on unit testing, widget testing, and integration testing. Feel free to proceed to the conclusion of the article.

This Example is provided by Flutter codelabs developers, 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. You can get the source code from codelabs or:

My Repo:https://github.com/wafaMohamed/flutter_testing_example

UI:

codelab Example

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 using the provider package.

Unit testing the provider

start by unit testing the favorites model.

Example favorites model:

import 'package:flutter/material.dart';

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

List<int> get favoritesItems => _favoritesItems;

// Add 2 Function for adding and removing items from the list
void addFavoriteItem(int item) {
_favoritesItems.add(item);
notifyListeners();
}

void removeFavoriteItem(int item) {
_favoritesItems.removeWhere((element) => element == item);
notifyListeners();
}
// removeWhere method to remove all occurrences of the specified item
}

Example Unit Test for favorites model:

  1. Add test model file
test/
models/
favorites_test.dart

2. Unit Test Code

// Import necessary libraries for testing
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/models/favorites.dart';

// Unit Testing
void main() {
// Create a group for unit testing related to the FavoritesProvider model
group('Unit Testing: test model provider ', () {
// Instantiate a FavoritesProvider to be used for testing
var favoritesModel = FavoritesProvider();

// Test case: Add a new item to the favorites list
test('New Item Should be Added', () {
// Define a number to be added
var num = 30;

// Call the addFavoriteItem method
favoritesModel.addFavoriteItem(num);

// Verify that the favoritesItems list contains the added number
expect(favoritesModel.favoritesItems.contains(num), true);
});

// Test case: Remove an item from the favorites list
test('Item Should be Removed', () {
// Define a number to be added and then removed
var num2 = 31;

// Add the number to the favorites list
favoritesModel.addFavoriteItem(num2);
// Print the list before removal (for demonstration purposes)
print('Before removal: ${favoritesModel.favoritesItems}');

// Verify that the favoritesItems list initially contains the number
expect(favoritesModel.favoritesItems.contains(num2), true);

// Call the removeFavoriteItem method
favoritesModel.removeFavoriteItem(num2);
// Print the list after removal (for demonstration purposes)
print('After removal: ${favoritesModel.favoritesItems}');

// Verify that the favoritesItems list no longer contains the removed number
expect(favoritesModel.favoritesItems.contains(num2), false);
});
});
}

3. Test Result

Test Passed :)

Widget testing

This step tests the HomePage and FavoritesPage screens individually.

  1. Example of Widget testing for HomePage code: Note( please see all the source code from code labs or my repository to get right test)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/models/favorites.dart';
import 'package:flutter_testing/view/pages/codelab_flutter/home.dart';
import 'package:provider/provider.dart';

// Widget Testing
void main() {
group('Widget Testing For Home Page', () {
// Test 1
testWidgets('Testing if ListView shows up', (tester) async {
// Pump the widget tree with the simulated Home Screen
await tester.pumpWidget(simulateHomeScreen());
expect(find.byType(ListView), findsOneWidget);
});
// Test 2: Test scrolling behavior on the Home Page
testWidgets('Scroll Testing', (WidgetTester widgetTester) async {
await widgetTester.pumpWidget(simulateHomeScreen());
expect(find.text('Item: 0'), findsOneWidget);
// Perform a fling (scroll) on the ListView and wait for settling
await widgetTester.fling(find.byType(ListView), Offset(0, -200), 3000);
await widgetTester.pumpAndSettle();
expect(find.text('Item: 0'), findsNothing);
});
// Test 3: Test interaction with the favorite icon on the Home Page
testWidgets('Find Fav Icon', (widgetTester) async {
await widgetTester.pumpWidget(simulateHomeScreen());
expect(find.byIcon(Icons.favorite), findsNothing);
await widgetTester.tap(find.byIcon(Icons.favorite_border).first);
await widgetTester.pumpAndSettle(Duration(seconds: 1));
expect(find.text('Added to favorites.'), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsWidgets);
await widgetTester.tap(find.byIcon(Icons.favorite).first);
await widgetTester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Removed from favorites.'), findsOneWidget);
expect(find.byIcon(Icons.favorite), findsNothing);
});
});
}

Widget simulateHomeScreen() {
return ChangeNotifierProvider<FavoritesProvider>(
create: (context) => FavoritesProvider(),
child: MaterialApp(
home: HomePage(),
),
);
}

2. Test Result

test passed :)

Testing app UI with integration tests

This step tests the app UI by integration test

  1. Create a directory called integration_test in the project's root directory, and in that directory create a new file named app_test.dart.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/main.dart' as app;

void main() {
// Create a group for testing the entire app
group('Testing App', () {
// Test for favorites operations
testWidgets('Favorites operations test', (tester) async {
// Pump the widget tree with the main app (MyApp)
await tester.pumpWidget(const app.MyApp());

// Define keys for icons that will be tapped
final iconKeys = [
'icon_0',
'icon_1',
'icon_2',
];

// Loop through icon keys, tap each icon, and verify the result
for (var icon in iconKeys) {
await tester.tap(find.byKey(ValueKey(icon)));
await tester.pumpAndSettle(const Duration(seconds: 1));

// Verify that the message 'Added to favorites.' is displayed
expect(find.text('Added to favorites.'), findsOneWidget);
}

// Tap on the 'Favorites' button
await tester.tap(find.text('Favorites'));
await tester.pumpAndSettle();

// Define keys for icons that will be removed from favorites
final removeIconKeys = [
'remove_icon_0',
'remove_icon_1',
'remove_icon_2',
];

// Loop through remove icon keys, tap each icon, and verify the result
for (final iconKey in removeIconKeys) {
await tester.tap(find.byKey(ValueKey(iconKey)));
await tester.pumpAndSettle(const Duration(seconds: 1));

// Verify that the message 'Removed from favorites.' is displayed
expect(find.text('Removed from favorites.'), findsOneWidget);
}
});
});
}

2. Run Test

Test Passed :)
automated test

Testing app performance with Flutter Driver

Performance testing is crucial to ensure your Flutter applications run smoothly, especially as they grow in complexity. Flutter provides a powerful testing tool called Flutter Driver that allows you to write integration tests, including performance tests.

Writing a Performance Test

  1. Create a New Test File (performance_test) inside integration test Folder:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/main.dart' as app;
import 'package:integration_test/integration_test.dart';

void main() {
group('Testing App Performance', () {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

testWidgets('Scrolling test', (tester) async {
await tester.pumpWidget(const TestingApp());

final listFinder = find.byType(ListView);

await binding.traceAction(() async {
await tester.fling(listFinder, const Offset(0, -500), 10000);
await tester.pumpAndSettle();

await tester.fling(listFinder, const Offset(0, 500), 10000);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_summary');
});
});
}

This test scrolls through a list of items and records the scrolling actions for performance analysis.

  • Ensure Initialization and Set Frame Policy: The IntegrationTestWidgetsFlutterBinding.ensureInitialized() function ensures that the integration test driver is initialized. Setting framePolicy to fullyLive is useful for testing animated code.
  • Scrolling Test: The testWidgets function defines a scrolling test. It uses the traceAction function to record the scrolling actions and generates a timeline summary labeled 'scrolling_summary'.traceAction method records the performance of the app as it scrolls through the list. provided by the IntegrationTestWidgetsFlutterBinding class.
  • This method runs the provided function and records Timeline with detailed information about the performance of the app.
  • Specify the reportKey when running more than one traceAction. By default all Timelines are stored with the key timeline, in this example the reportKey is changed to scrolling_timeline.

Run

Run

Capturing Performance Results

To capture and analyze performance results, create a file named performance_driver.dart in the test_driver folder with the following code:

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
return integrationDriver(
responseDataCallback: (data) async {
if (data != null) {
final timeline = driver.Timeline.fromJson(
data['scrolling_timeline'] as Map<String, dynamic>,
);

// Convert the Timeline into a TimelineSummary that's easier to
// read and understand.
final summary = driver.TimelineSummary.summarize(timeline);

// Then, write the entire timeline to disk in a json format.
// This file can be opened in the Chrome browser's tracing tools
// found by navigating to chrome://tracing.
// Optionally, save the summary to disk by setting includeSummary
// to true
await summary.writeTimelineToFile(
'scrolling_timeline',
pretty: true,
includeSummary: true,
);
}
},
);
}

This script captures the timeline summary of the performance test and writes it to a file named ‘scrolling_summary’.

The Timeline object provides detailed information about all of the events that took place, but it doesn’t provide a convenient way to review the results.

Therefore, convert the Timeline into a TimelineSummary. The TimelineSummary can perform two tasks that make it easier to review the results:

  1. Writing a JSON document on disk that summarizes the data contained within the Timeline. This summary includes information about the number of skipped frames, slowest build times, and more.
  2. Saving the complete Timeline as a json file on disk. This file can be opened with the Chrome browser’s tracing tools found at chrome://tracing.

Running the Performance Test

  • Ensure your device is connected or start an emulator.
  • Run the Test Command (This command runs the performance test, captures results, and generates a timeline summary file).
flutter drive \
--driver=test_driver/performance_driver.dart \
--target=integration_test/performance_test.dart \
--profile \
--no-dds
  • test passed :)

After the test completes successfully, the build directory at the root of the project contains two files:

  1. scrolling_summary.timeline_summary.json contains the summary. Open the file with any text editor to review the information contained within. With a more advanced setup, you could save a summary every time the test runs and create a graph of the results.
  2. scrolling_timeline.timeline.json contains the complete timeline data. Open the file using the Chrome browser’s tracing tools found at chrome://tracing. The tracing tools provide a convenient interface for inspecting the timeline data to discover the source of a performance issue.
{
"average_frame_build_time_millis": 4.2592592592592595,
"worst_frame_build_time_millis": 21.0,
"missed_frame_build_budget_count": 2,
"average_frame_rasterizer_time_millis": 5.518518518518518,
"worst_frame_rasterizer_time_millis": 51.0,
"missed_frame_rasterizer_budget_count": 10,
"frame_count": 54,
"frame_build_times": [
6874,
5019,
3638
],
"frame_rasterizer_times": [
51955,
8468,
3129
]
}

Conclusion

Testing is a critical aspect of Flutter development, ensuring the reliability and functionality of apps. By implementing unit, widget, and integration tests, developers can catch bugs early in the development process, leading to more robust and maintainable applications. The world of Flutter testing is vast, and there’s more to uncover. For an in-depth exploration, be sure to check out the official documentation and elevate your testing skills

That’s it! Please let me know if you found this helpful ♥️. If you have any questions, feel free to ask 🙏🏼. Here are the links. 😊

LinkedIn

GitHub

Email: wafamohameddd@gmail.com

My Repository for Flutter Testing: https://github.com/wafaMohamed/flutter_testing_example

Please consider clapping and follow me! 🙏🏼

Stackademic

Thank you for reading until the end. Before you go:

--

--