avatarTomic Riedel

Summary

The provided content is a comprehensive guide on integration testing in Flutter, detailing the process of testing multiple components together to ensure they function correctly as a whole.

Abstract

The article "The Complete Guide to Integration Testing in Flutter" is a continuation of a series on testing in Flutter, following previous discussions on unit and widget testing. It emphasizes the importance of understanding unit and widget testing before proceeding with integration testing. The guide introduces the concept of integration testing, explaining that it involves checking the interaction between different parts of an application, akin to ensuring all pieces of a puzzle fit together. The author uses a simple login process as an example, demonstrating how to write integration tests that simulate user interactions and verify navigation to the home screen upon successful login. The article also addresses the speed of test execution and provides tips on how to slow down the process for better observation. Additionally, it encourages readers to practice writing tests for invalid login attempts to ensure robust testing coverage. The guide concludes with final thoughts on the significance of integration testing and invites readers to engage with more content on Flutter development.

Opinions

  • The author believes that integration testing is crucial for ensuring that different parts of a Flutter app work together seamlessly.
  • The guide suggests that integration testing can also be referred to as end-to-end testing.
  • The author provides a personal commitment to update the article or create new content if readers encounter cases not covered in the guide.
  • There is an emphasis on the importance of observing the testing process, recommending the use of Future.delayed() to slow down test execution for better visibility.
  • The author encourages readers to subscribe to a newsletter for ongoing learning and to follow on various platforms for more content related to Flutter development.
  • The article promotes the idea that mastering integration testing is "literally that easy," implying a user-friendly process with the right guidance.

The Complete Guide to Integration Testing in Flutter

In the first episode of this testing series, we looked at Unit Testing in Flutter. Afterward, we took a look at Widget Testing in Flutter.

This article requires knowledge about Unit & Widget Testing. If you do not know it yet, take a look at my article about Unit Testing and my article about Widget Testing and come back here afterward!

With that being said, let’s get started with Integration Testing!

Integration Testing

So, what is Integration Testing?

Integration test is the process of testing multiple components of an app together, to ensure that they function correctly as a whole.

This means checking that the different parts of an app work well together, like making sure all pieces of a puzzle fit to form the right picture.

We also call Integration Testing “end-to-end testing”.

Starting Point for our Integration Testing

To get to know integration testing, we are going to test a simple login progress.

We have a login screen that has a text field for email and password, as well as a button “Login”. When the username is “tomic” and the password is “12345”, then the app pushes to the Home Screen (a simple screen with a text). If not, we are showing an error dialog stating that something went wrong.

We are going to use the following code as the starting point:

  1. main.dart
import 'package:flutter/material.dart';
import 'package:intro_to_testing/login_screen.dart';

void main() {
  runApp(
    const MaterialApp(
      home: const LoginPage(),
    ),
  );
}

2. login_screen.dart

import 'package:flutter/material.dart';
import 'package:intro_to_testing/home_screen.dart';

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  void _login() {
    // Validate login credentials and navigate to home screen if valid
    if (_usernameController.text == 'tomic' &&
        _passwordController.text == '12345') {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => const HomeScreen(),
        ),
      );
    } else {
      // Show error message if credentials are invalid
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: const Text('Error'),
            content: const Text('Invalid username or password'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('OK'),
              ),
            ],
          );
        },
      );
    }
  }

  @override
  void dispose() {
    super.dispose();
    _usernameController.dispose();
    _passwordController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFormField(
              controller: _usernameController,
              decoration: const InputDecoration(
                labelText: 'Username',
              ),
            ),
            TextFormField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
              ),
            ),
            const SizedBox(height: 16.0),
            ElevatedButton(
              onPressed: _login,
              child: const Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

3. home_screen.dart

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: const Center(
        child: Text('Home Screen'),
      ),
    );
  }
}

With these big code chunks out of the way, let’s finally start testing!

Writing our Integration Tests

To get started, first, add the integration_test package to your dev dependencies. It is from the Flutter SDK, so we do not rely on any third-party libraries:

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

Great. Now, instead of writing our tests in the test folder, we are going to create a separate folder named integration_test at the highest level of our project. Create a new file called app_test.dart inside of this folder. Of course, if you have a bigger application and test separate parts, you can use more descriptive names, but because we are literally going to test the whole app, we are going to leave it like this for now.

Great. Now let’s think about what we want to do:

  1. We obviously want to define a new test. We already learned that in Unit & Widget Testing.
  2. We want to build our whole app. Unlike Widget Testing, we want to run the whole app, including all the real functionalities, etc. This means we need to find a way to call the main function from our main.dart in the lib folder without confusing it with the main function of our integration test
  3. We want to type in the username and password
  4. We want to click on the “Login” button
  5. We want to check if the app actually navigates to the home screen

I am going to share our whole test here now and explain each part inside of the code. I think this makes it way easier to understand:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intro_to_testing/home_screen.dart';

// We are going to prefix this with app, as we do not want to call the
// main function of our integration test over and over again
import 'package:intro_to_testing/main.dart' as app;

void main() {
  // This does the work of WidgetsFlutterBinding.ensureInitialized();
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets(
    'Login screen with valid username and password',
    (widgetTester) async {
      // Call the main function from our lib folder.
      // If we wouldn't have prefixed it with app., the program would not know
      // which main function we want to call
      app.main();

      // We wait till everything is loaded, just like we did in widget tests
      await widgetTester.pumpAndSettle();

      // We want to enter the text in our text fields. To find our widgets
      // we use find.byType. The problem: We have two TextFormFields
      // There are multiple approaches on how to solve this. One is to use
      // .at(n), just like we did below. A more complex, but also the recommended
      // approach is to give the TextFormFields some Key and then use
      // find.byKey().
      await widgetTester.enterText(
          find.byType(TextFormField).at(0), 'tomic');
      await widgetTester.enterText(
          find.byType(TextFormField).at(1), '12345');

      // Press the login button
      await widgetTester.tap(find.byType(ElevatedButton));

      // We wait till everything is loaded / the app pushed to the new screen
      await widgetTester.pumpAndSettle();

      // Now we want to check if the app actually displays our home screen
      expect(find.byType(HomeScreen), findsOneWidget);
    },
  );
}

And we have just written our first Integration Test! Now you can start your simulator and run this test.

Before reading on, do you want to boost your Flutter knowledge in no-time?

Join 100+ Flutter developers getting high-quality articles every week.

100% free!

You can join here.

But… normally you are supposed to see something happen in the Simulator. Why don’t we see anything?

The answer is simpler than you might think: The program executes everything unbelievably fast. If you want to see what happens step by step, just add a Future.delayed() with a duration of 1 or 2 seconds between each step.

Let’s implement our test for the case when the input is not valid. Try it by yourself, you only need to change 4 lines of code.

Okay, let’s take a look at it:

testWidgets(
    'Login screen with invalid username and password',
    (widgetTester) async {
      app.main();
      await widgetTester.pumpAndSettle();

      // Enter a wrong username and/or password
      await widgetTester.enterText(
          find.byType(TextFormField).at(0), 'ups');
      await widgetTester.enterText(
          find.byType(TextFormField).at(1), 'badpassword');

      await widgetTester.tap(find.byType(ElevatedButton));
      await widgetTester.pumpAndSettle();

      // Check if the alert dialog is present
      expect(find.byType(AlertDialog), findsOneWidget);
    },
  );

And voilà, we just added integration testing for our whole application. It’s literally that easy!

Final Words

We have now learned how to apply integration testing in your app. There may be some rare cases I haven’t covered yet. If you find one of those, please let me know in the comments. I will either edit this article to include it, or I will create a separate article if I haven’t covered a bigger case. But there will be an update, I promise!

If you liked this article, you will likely like my other articles too. You can subscribe to my newsletter to get high-quality articles into your inbox once a week, or read some more articles of mine (Or simply do both :)

Stackademic 🎓

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

Flutter
Dart
Integration Testing
Flutter Testing
Test Coverage
Recommended from ReadMedium