Testing Flutter Apps - bounswe/bounswe2022group7 GitHub Wiki

For a mobile application, automated testing falls into a few categories:

  • A unit test tests a single function, method, or class.
  • A widget test(in other UI frameworks referred to as component test) tests a single widget.
  • An integration test tests a complete app or a large part of an app.

Unit Testing

A unit test tests a single function, method, or class. The goal of a unit test is to verify the correctness of a unit of logic under a variety of conditions. External dependencies of the unit under test are generally mocked out.

The test package provides the core framework for writing unit tests, and the flutter_test package provides additional utilities for testing widgets. To write a unit test, there are a few steps to follow:

  1. Add the test or flutter_test dependency to the project.

    dev_dependencies:
        test: <latest_version>
    
  2. Create two files: counter.dart and counter_test.dart. The counter.dart file contains a class that you want to test and resides in the lib folder. The counter_test.dart file contains the tests themselves and lives inside the test folder.

    In general, test files should reside inside a test folder located at the root of your Flutter application or package. Test files should always end with test.dart, this is the convention used by the test runner when searching for tests.

    When you’re finished, the folder structure should look like this:

    counter_app/
        lib/
            counter.dart
        test/
            counter_test.dart
    
  3. Next, you need a “unit” to test. For this example, create a Counter class inside the lib/counter.dart file. It is responsible for incrementing and decrementing a value starting at 0.

    class Counter {
        int value = 0;
    
        void increment() => value++;
    
        void decrement() => value--;
    }
    
  4. Write a test for Counter class. Inside the counter_test.dart file, write the first unit test. Tests are defined using the top-level test function, and you can check if the results are correct by using the top-level expect function. Both of these functions come from the test package.

    // Import the test package and Counter class
    import 'package:counter_app/counter.dart';
    import 'package:test/test.dart';
    
    void main() {
        test('Counter value should be incremented', () {
            final counter = Counter();
    
            counter.increment();
    
            expect(counter.value, 1);
        });
    }
    
  5. If you have several tests that are related to one another, combine them using the group function provided by the test package.

    import 'package:counter_app/counter.dart';
    import 'package:test/test.dart';
    
    void main() {
        group('Counter', () {
            test('value should start at 0', () {
                expect(Counter().value, 0);
            });
    
            test('value should be incremented', () {
                final counter = Counter();
    
                counter.increment();
    
                expect(counter.value, 1);
            });
    
            test('value should be decremented', () {
                final counter = Counter();
    
                counter.decrement();
    
                expect(counter.value, -1);
            });
        });
    }
    
  6. Use a terminal to run the tests by executing the following command from the root of the project:

    flutter test test/counter_test.dart
    

Widget Testing

Above, you learned how to test Dart classes using the test package. 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.

In order to test a widget, there are a few steps to follow:

  1. Before writing tests, include the flutter_test dependency in the dev_dependencies section of the pubspec.yaml file. If creating a new Flutter project with the command line tools or a code editor, this dependency should already be in place.

    dev_dependencies:
        flutter_test:
            sdk: flutter
    
    
  2. Next, create a widget for testing. For this recipe, create a widget that displays a title and message.

    class MyWidget extends StatelessWidget {
        const MyWidget({
            super.key,
            required this.title,
            required this.message,
        });
    
        final String title;
        final String message;
    
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                title: 'Flutter Demo',
                home: Scaffold(
                    appBar: AppBar(
                        title: Text(title),
                    ),
                    body: Center(
                        child: Text(message),
                    ),
                ),
            );
        }
    }
    
  3. With a widget to test, begin by writing your first test. Use the testWidgets() function provided by the flutter_test package to define a test. The testWidgets function allows you to define a widget test and creates a WidgetTester to work with.

    This test verifies that MyWidget displays a given title and message. It is titled accordingly, and it will be populated in the next section.

    void main() {
        testWidgets('MyWidget has a title and message', (tester) async {
            // Create the widget by telling the tester to build it.
            await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
        });
    }
    
  4. With a widget in the test environment, search through the widget tree for the title and message Text widgets using a Finder. This allows verification that the widgets are being displayed correctly.

    For this purpose, use the top-level find() method provided by the flutter_test package to create the Finders. Since you know you’re looking for Text widgets, use the find.text() method.

    void main() {
        testWidgets('MyWidget has a title and message', (tester) async {
            await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
    
            // Create the Finders.
            final titleFinder = find.text('T');
            final messageFinder = find.text('M');
        });
    }
    
  5. Finally, verify the title and message Text widgets appear on screen using the Matcher constants provided by flutter_test. Matcher classes are a core part of the test package, and provide a common way to verify a given value meets expectations.

    Ensure that the widgets appear on screen exactly one time. For this purpose, use the findsOneWidget Matcher.

    void main() {
        testWidgets('MyWidget has a title and message', (tester) async {
            await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
    
            // Create the Finders.
            final titleFinder = find.text('T');
            final messageFinder = find.text('M');
    
            // Use the `findsOneWidget` matcher provided by flutter_test to verify
            // that the Text widgets appear exactly once in the widget tree.
            expect(titleFinder, findsOneWidget);
            expect(messageFinder, findsOneWidget);
        });
    }
    

Integration Testing

Unit tests and widget tests are handy for testing individual classes, functions, or widgets. However, they generally don’t test how individual pieces work together as a whole, or capture the performance of an application running on a real device. These tasks are performed with integration tests.

Integration tests are written using the integration_test package, provided by the SDK.

In this recipe, learn how to test a counter app. It demonstrates how to setup integration tests, how to verify specific text is displayed by the app, how to tap specific widgets, and how to run integration tests.

This recipe uses the following steps:

  1. First, create an app for testing. In this example, test the counter app produced by the flutter create command. This app allows a user to tap on a button to increase a counter.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
        const MyApp({super.key});
    
        @override
        Widget build(BuildContext context) {
            return const MaterialApp(
                title: 'Counter App',
                home: MyHomePage(title: 'Counter App Home Page'),
            );
        }
    }
    
    class MyHomePage extends StatefulWidget {
        const MyHomePage({super.key, required this.title});
    
        final String title;
    
        @override
        State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
        int _counter = 0;
    
        void _incrementCounter() {
            setState(() {
                _counter++;
            });
        }
    
        @override
        Widget build(BuildContext context) {
            return Scaffold(
                appBar: AppBar(
                    title: Text(widget.title),
                ),
                body: Center(
                    child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                            const Text(
                                'You have pushed the button this many times:',
                            ),
                            Text(
                                '$_counter',
                                // Provide a Key to this specific Text widget. This allows
                                // identifying the widget from inside the test suite,
                                // and reading the text.
                                key: const Key('counter'),
                                style: Theme.of(context).textTheme.headline4,
                            ),
                        ],
                    ),
                ),
                floatingActionButton: FloatingActionButton(
                    // Provide a Key to this button. This allows finding this
                    // specific button inside the test suite, and tapping it.
                    key: const Key('increment'),
                    onPressed: _incrementCounter,
                    tooltip: 'Increment',
                    child: const Icon(Icons.add),
                ),
            );
        }
    }
    
  2. Next, use the integration_test and flutter_test packages to write integration tests.

    dev_dependencies:
        integration_test:
            sdk: flutter
        flutter_test:
            sdk: flutter
    
  3. Create a new directory, integration_test, with an empty app_test.dart file:

    counter_app/
        lib/
            main.dart
        integration_test/
            app_test.dart
    
  4. Now you can write tests. This involves three steps:

    1. Initialize IntegrationTestWidgetsFlutterBinding, a singleton service that executes tests on a physical device.
    2. Interact and tests widgets using the WidgetTester class.
    3. Test the important scenarios.
    import 'package:flutter_test/flutter_test.dart';
    import 'package:integration_test/integration_test.dart';
    
    import 'package:counter_app/main.dart' as app;
    
    void main() {
        IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    
        group('end-to-end test', () {
            testWidgets('tap on the floating action button, verify counter',
                    (tester) async {
                app.main();
                await tester.pumpAndSettle();
    
                // Verify the counter starts at 0.
                expect(find.text('0'), findsOneWidget);
    
                // Finds the floating action button to tap on.
                final Finder fab = find.byTooltip('Increment');
    
                // Emulate a tap on the floating action button.
                await tester.tap(fab);
    
                // Trigger a frame.
                await tester.pumpAndSettle();
    
                // Verify the counter increments by 1.
                expect(find.text('1'), findsOneWidget);
            });
        });
    }
    
  5. The process of running the integration tests varies depending on the platform you are testing against. You can test against a mobile platform or the web.

    To test on a real iOS / Android device, first connect the device and run the following command from the root of the project:

    flutter test integration_test/app_test.dart

    Or, you can specify the directory to run all integration tests:

    flutter test integration_test

    This command runs the app and integration tests on the target device.

References

⚠️ **GitHub.com Fallback** ⚠️