How to Create Unit Tests in Flutter using Dart

Flutter Jun 21, 2023

Unit testing is an essential part of the software development process that helps ensure the reliability and correctness of your code. In Flutter, unit testing plays a crucial role in verifying the functionality of individual units of code, such as functions or methods. By writing unit tests, you can catch bugs early, improve code quality, and enhance the overall stability of your application.

In this post, we will explore the process of creating unit tests in Flutter, using an example of an AuthenticationPage widget with a validate function. We will walk through the setup, write and run the tests,  and discuss best practices.

1. Setting up

We will start by setting up our main.dart file. In this file, we define our MyApp widget, which extends a StatelessWidget and sets up our MaterialApp with the AuthenticationPage as the home screen, see the code below:

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: 'Unit test demo',
      home: AuthenticationPage(),
    );
  }
}

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

  String? validate({required String? value, required String field}) {
    if (value!.isEmpty) {
      return 'This field cannot be empty.';
    }

    if (field == 'email' &&
        RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value) == false) {
      return 'This is not a valid email address.';
    }

    if (field == 'password' && value.length < 6) {
      return 'The password must be at least 6 characters.';
    }

    return null;
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

The AuthenticationPage widget contains a validate function responsible for validating input values for email and password fields. The function checks for empty fields, validates email addresses using a regular expression and ensures that the password has a minimum length of six characters.

2. Writing our Unit Tests

Let us start by creating our test file. A good start is to create a unit directory inside your test directory. Inside the unit directory, we want to create our test file. We take the name of our function validate and suffix it with _test.dart to create a file with the following name validate_test.dart.

|-- test/
  |-- unit/
    |-- validate_test.dart 

Inside our test file, we will start with the main function. Inside the main function, we will write our tests. For unit tests, we use the test function. Inside the test function, we provide the name of the test as the first parameter and a callback in the second parameter. In order to access the test function we need to import flutter_test.dart. See the code below:

import 'package:codeonwards_demo/main.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('it returns an error message when email field is empty', () {
    
  });
}

Usually flutter_test is pre-installed, if this is not the case install it by executing the following command: flutter pub add flutter_test --dev.

As the name of the test suggests we want to make sure that it will return an error message when the email field is empty.

So for this, we need to call the validate function with an empty value and the email. But first, how are we going to call this function? We have to call this function from the AuthenticationPage widget so let us create an instance.

import 'package:codeonwards_demo/main.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  const authenticationPage = AuthenticationPage();

  test('it returns an error message when email field is empty', () {
    
  });
}

We put the authenticationPage variable above the test because we want to reuse it in other tests.

2.1 Email field Unit Tests

Now that we have our instance of the AuthenticationPage let us call the validate function inside the test and make sure that we get the expected output:

import 'package:codeonwards_demo/main.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  const authenticationPage = AuthenticationPage();

  test('it returns an error message when email field is empty', () {
    final actual = authenticationPage.validate(value: '', field: 'email');

    expect(actual, 'This field cannot be empty.');
  });
}

In this code snippet, we are calling the validate function with an empty value and the email field on line 8. On line 10 we are using the expect function from the flutter_test package to assert if our actual output matches what we expect. In this case, we expect the "this field cannot be empty" error message.

With this, we finished our first test. You can run the test in multiple ways. You can execute the flutter test command inside the CLI, make sure you are inside your project. This command will run all tests. If you are using an IDE like IntelliJ or VSCode they have built-in support to run tests. See the GIF below:

flutter_running_unit_tests_using_flutter_test_and_intellij

It is also possible to run a single test from the CLI by executing the following command: flutter test --plain-name="it returns an error message when email field is empty". As you can see after the --plain-name flag you need to add the name of the test in double quotes "".

flutter_running_single_test_using_plain_name_flag

Now that we have our first email test down, let us write the other email tests, see the code below:

import 'package:codeonwards_demo/main.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  const authenticationPage = AuthenticationPage();

  group('email', () {
    test('it returns an error message when email field is empty', () {
      final actual = authenticationPage.validate(value: '', field: 'email');

      expect(actual, 'This field cannot be empty.');
    });

    test('it returns an error message when email is not valid', () {
      final emailMissingDot = authenticationPage.validate(
        value: 'codeonwards@testcom',
        field: 'email',
      );

      final emailMissingAt = authenticationPage.validate(
        value: 'codeonwardstest.com',
        field: 'email',
      );

      expect(emailMissingDot, 'This is not a valid email address.');
      expect(emailMissingAt, 'This is not a valid email address.');
    });

    test('it returns null when a valid email address is provided', () {
      final actual = authenticationPage.validate(
        value: '[email protected]',
        field: 'email',
      );

      expect(actual, null);
    });
  });
}

In the code snippet above we added the additional tests for the email field. As you can see the approach is similar to our first test, the only difference is that we provide different parameters in the validate function. Of course, because of this we also have different expectations. Also, you might have noticed that in the second test, we have two expectations in one test. This is perfectly fine and you can add as many in a single test as you need.

We also wrapped all our tests in the group function and named it "email". Grouping tests allow us to run the group as a whole using the --plain-name flag and provide the name of the group: flutter test --plaine-name="email".

flutter_running_email_group_tests

2.2 Password Field Unit Tests

The tests for the password are similar to those from the email, however in the group function we will create a function, see the code below:

group('password', () {
  String? validatePassword({required String? value}) {
    return authenticationPage.validate(value: value, field: 'password');
  }

  test('it returns an error message when password field is empty', () {
    final actual = validatePassword(value: '');

    expect(actual, 'This field cannot be empty.');
  });

  test('it returns an error message when password has less than 6 characters',
      () {
    final actual = validatePassword(value: '12345');

    expect(actual, 'The password must be at least 6 characters.');
  });

  test('it returns null when password with 6 characters or more is provided',
      () {
    final actual = validatePassword(value: '123456');

    expect(actual, null);
  });
});

In this code snippet, we have added the validatePassword function in the group function. Because inside the password group, we will always be setting the field parameter of the validate function to password. By doing this we simplify our code by using a function that already sets the field to password for us. This is another benefit of grouping tests. The same can be done for the email field.

Now when we run all our tests using flutter test you will see that all our tests succeed.

flutter_running_all_our_tests_using_flutter_test

With that, we successfully tested the validate function of our AuthenticationPage widget.

3. When to write Unit Tests?

Knowing when to write unit tests is crucial for maintaining a balance between development speed and code quality. Here are some key scenarios where writing unit tests is highly recommended:

  1. New features or functionalities: When adding new features or functionalities to your application, it's a good practice to write unit tests alongside the implementation. This helps ensure that the new code behaves as expected and does not break existing functionality.
  2. Bug fixes: Whenever you encounter a bug in your application, it is essential to write a unit test that reproduces the issue. This not only helps in identifying the root cause of the bug but also makes sure the fix does not break any existing functionality.
  3. Code refactoring: During code refactoring, when you modify or optimize existing code without changing its external behavior, unit tests act as a safety net. They provide confidence that the refactored code still functions correctly and has not introduced any unintended side effects.
  4. Complex logic or critical components: Unit tests are particularly valuable for complex logic or critical components of your application. By thoroughly testing these parts, you can ensure that they work as intended and handle various edge cases.

In general, it is beneficial to adopt a test-driven development (TDD) approach, where you write tests before writing the implementation code. This ensures that your code is testable, promotes clear requirements, and helps maintain a high level of test coverage.

Flutter tests can also be run with coverage, this way you know exactly which lines of code are being tested, see the post below:

How to run Flutter tests with Coverage
When it comes to testing Flutter applications, running tests with coverage is an important step. This allows developers to measure how well their tests cover the code and find areas that need further testing. In this post, we will look at how to run Flutter tests with coverage and explore

4. Conclusion

Unit testing is a fundamental practice in software development, and Flutter provides excellent support for writing and executing unit tests. In this post, we explored the process of creating unit tests for a Flutter application, focusing on the example of an AuthenticationPage widget's validate function.

We started by setting up the main.dart file and defining the widget structure. Then, we proceeded to write unit tests using the flutter_test package. We covered scenarios such as empty email fields, invalid email addresses, and password validation.

Remember, unit tests are not a one-time effort but an ongoing process. Regularly maintaining and updating your tests as your codebase evolves is crucial for long-term success. By investing time in writing comprehensive unit tests, you can ensure the reliability, maintainability, and stability of your Flutter applications.

Tags