DEV Community

Mateus Daniel
Mateus Daniel

Posted on

Testing your Flutter app

Hello everybody, today we're gonna talk about tests with flutter and the importance of doing that in your projects, so let's get started.

Why do we need to test?

As our flutter project grows the importance of testing it grows too, so since your code was well made using good patterns and following the best practices, it makes easier to implement a test to grant you the app works as you've projected for.

Concept

There are some concepts of how to write tests and provide better and well-projected apps, one of the most common is the TDD (Test Driven Development), which says that you have to write your test first and then write the code, and repeat the process of how many times you need to ensure that everything is passing in test and the code only does what it has to do.

Types of test

With Flutter we have three types of tests, they are:

  • Unit test: as the name said, this kind of test has the purpose of checking if the minimum part of your code is working as expected, this includes for example functions and classes.

  • Widget test: This one verifies if the widgets are working as expected, so the purpose of this kind of test is to check the UI consistency.

  • Integration test: The last one, the integration test, it's personally my favorite type, this is like a mix of the two types of the test described before, with this you can check an entire flow, so you can verify if the checkout of an e-commerce app is working as expected for example.

With all that said let's dive inside some examples to understand a little bit better. In this case, I will show how to test a login because it is a common feature for most of the projects.

To get started, suppose that you've already the flutter in your O.S then just create a project with the command flutter create login_test where the name login_test can be changed if you wanna give another name for your project, with this created verify in your root directory and you be able to find a folder called test, all of your testing code should be inside that and by default, all the classes created for this purpose have to be in the the _test.dart to let the language recognize this as a test class.

To get started using the TDD concept, let's create the unit test first before the implementation.

Unit test

delete the file that comes with the project and create a file called unit_login_test.dart in test folder with the following code:

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Login controller', () {
    test("success login", () {
      LoginController controller = LoginController();
      expect(controller.login("username", "password"), true);
    });

    test("failed login", () {
      LoginController controller = LoginController();
      expect(controller.login("invalidUsername", "invalidPassword"), false);
    });
  });
}

Enter fullscreen mode Exit fullscreen mode

We create a main function, which starts in this case these tests, we have to import the class flutter_test to execute as we want, to do this we create a group and inside put all our test cases, initiate our class and execute the function with some given parameters, and the expected return in expect test function to do that we imagine that we have a controller and inside a login function that receives the username and the password and if match returns a boolean. To execute now we have to create a file called login_controller.dart inside the lib folder with the following code:

class LoginController {
  bool login(String username, String password) {
    return username == "username" && password == "password";
  }
}

Enter fullscreen mode Exit fullscreen mode

With that, we only have to import this class inside our test class and execute it to see the result

import 'package:login_test/login_controller.dart';

Unit test passing

Now let's move to the second type of test

Widget test

Like the unit test, we will create the test file and implement that, to get started create a file called widget_login_test.dart and put the following code:

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

void main() {
  testWidgets("Login UI", (tester) async {
    await tester.pumpWidget(const MaterialApp(home: LoginPage()));

    final username = find.byKey(const Key("usernameTextField"));
    final password = find.byKey(const Key("passwordTextField"));
    final loginButton = find.byKey(const Key("loginButton"));

    expect(username, findsOneWidget);
    expect(password, findsOneWidget);
    expect(loginButton, findsOneWidget);

    await tester.enterText(username, "username");
    await tester.enterText(password, "password");
    await tester.tap(loginButton);

    await tester.pump();
    final successMessage = find.byKey(const Key('success'));
    expect(successMessage, findsOneWidget);
  });
}

Enter fullscreen mode Exit fullscreen mode

The widget test renders the widgets and you can perform actions like tap() and enterText() to interact with the components, in this test case we need to search using the Key parameter, you can do it using a text, but the Key gives you more confidence about getting the right thing. Now to implement the code just create a file called login_page.dart and put this code below:

import 'package:flutter/material.dart';
import 'package:login_test/login_controller.dart';

LoginController loginController = LoginController();

TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextField(
            controller: usernameController,
            key: const Key('usernameTextField'),
          ),
          TextField(
            controller: passwordController,
            key: const Key('passwordTextField'),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            key: const Key('loginButton'),
            child: const Text("Login"),
            onPressed: () {
              // Example login logic
              if (loginController.login(
                  usernameController.text, passwordController.text)) {
                showDialog(
                  context: context,
                  builder: (_) => const AlertDialog(
                    key: Key('success'),
                    title: Text("Login Successful"),
                  ),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we only need to import the LoginPage in our widget test file, just type import 'package:login_test/login_page.dart'; with that you can execute your test and should see something like

Widget test

Integration test

This is the last type of test, it works by executing a complete feature, to do that you need a real device, to start we need to add a new dependency in our file pubspec.yaml in the section dev_dependencies called integration_test: sdk: flutter, after this just type flutter pub get to update the dependencies, with this set up complete you need to create a folder called integration_test in the root directory, then insert a file who you can call app_test.dartwith the code below:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:login_test/login_page.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  WidgetsFlutterBinding.ensureInitialized();
  group('LoginPage', () {
    testWidgets("Login UI", (tester) async {
      await tester.pumpWidget(const MaterialApp(home: LoginPage()));

      final username = find.byKey(const Key("usernameTextField"));
      final password = find.byKey(const Key("passwordTextField"));
      final loginButton = find.byKey(const Key("loginButton"));

      expect(username, findsOneWidget);
      expect(password, findsOneWidget);
      expect(loginButton, findsOneWidget);

      await tester.enterText(username, "username");
      await tester.pumpAndSettle(const Duration(seconds: 1));
      await tester.enterText(password, "password");
      await tester.pumpAndSettle(const Duration(seconds: 1));
      await tester.tap(loginButton);

      await tester.pumpAndSettle(const Duration(seconds: 1));
      final successMessage = find.byKey(const Key('success'));
      expect(successMessage, findsOneWidget);
    });
  });
}

Enter fullscreen mode Exit fullscreen mode

this code is very similar to the widget test, but we have a few differences, we use IntegrationTestWidgetsFlutterBinding.ensureInitialized(); and WidgetsFlutterBinding.ensureInitialized(); to ensure that we have the integration test and the widgets ready to start the process.

Bonus

With an additional part of this article, we gonna see how to get a visual report of the test coverage, if you don't wanna learn this just skip to the conclusion.

to do that we have to run in terminal flutter test --coverage, after executing this should create a folder called coverage with a file named lcov.info inside as we see below:

Coverage folder

after that you have to convert this file into a bunch of files to be read by a web server, to continue just type in your terminal genhtml coverage/lcov.info -o coverage/html where the coverage/lcov.info is the path of the file created above and the coverage/html after the flag -o is the directory where I want to save the generated files when this part finish, you will see a folder with the given name created where you choose.

to finish we have to expose this directory to access in a web browser, to do that I recommend getting the dhttpd library from here just type in your terminal dart pub global activate dhttpd with this you've downloaded the library globally, after this just type dhttpd --path coverage/html/ and open a browser to put the URL http://localhost:8080 and finally you will be able to see a full report about your test like you see bellow:

Flutter coverage

With this we finish the bonus part, this could be essential to understand where the code is not covered and where the tests are failing.

Conclusion

As we see, the tests give us more confidence in the final result focus on the release, this process can take more time to finish a feature, but the gain with that is more valuable than if a bug was reported after distribute of the project, so hope everybody understands how to do and the importance, hope to see you next time.

If you wanna see the full code, i've uploaded the code, the link is: Repository

Top comments (0)