A Complete Guide to Testing in Flutter. Part 2: Basic Unit Testing.
Credit: Nguyễn Thành Minh (Mobile Developer)
Introduction
In the previous section, I introduced an overview of common testing methods in Flutter. In the following series of articles, we will deep dive into the Unit Testing. Now, let’s begin with some simple examples.
A simple Unit Test
Let’s say we need to test the login function with the email and password validation functions.
We will have two static functions.
class Validator {
static bool validateEmail(String value) {
return value.isNotEmpty;
}
static bool validatePassword(String value) {
return value.isNotEmpty;
}
}
We will also have an extension method named isNullOrEmpty
.
extension StringExtension on String? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
}
Finally, the login function.
import 'package:testing_examples/part2/ext/extension.dart';
import 'package:testing_examples/part2/util/utils.dart';
bool login(String? email, String? password) {
if (email.isNullOrEmpty || password.isNullOrEmpty) {
return false;
}
return Validator.validateEmail(email!) && Validator.validatePassword(password!);
}
Full source code: https://github.com/ntminhdn/testing_examples/tree/main/lib/part2
We need to test 4 functions placed in 3 different files, so we need to create 3 test files in the test
folder. To distinguish between the unit test files and files related to other testing methods such as widget tests, I will create a folder named unit_test
inside the test
folder.
The naming convention for test files is to use the name of the code file plus the suffix _test.dart
. Additionally, there is another rule that the structure of the test
folder must mirror the lib
folder like this:
We will start writing tests for the validateEmail
function first. Each test file needs to start with a main()
function as the entry point. Additionally, to write unit tests, we need to import the flutter_test
package.
import 'package:flutter_test/flutter_test.dart';
void main() {
}
To create a unit test, we use the test
function, passing in two parameters: description
and body
.
void main() {
test('validateEmail should return true when the email is not empty', () {
// body
});
}
When naming tests, it doesn’t matter if they’re short or long; they should be clear enough for others to understand without reading the code.
As for the test body, it usually follows the AAA pattern: first, set things up (Arrange), then perform the action (Act), and finally, check the outcome (Assert).
test('validateEmail should return true when the email is not empty', () {
// Arrange
String validEmail = 'test@example.com';
// Act
bool result = Validator.validateEmail(validEmail);
// Assert
expect(result, true);
});
- Arrange is the step where we set up variables and inputs before calling the function we want to test. For instance, if we want to test the
validateEmail
function when the email is not empty, we need to create a variable likeString validEmail = ‘test@example.com’
. - Act is simply calling the function we want to test with the inputs we prepared in the arrange step:
Validator.validateEmail(validEmail)
. - Assert is the step where we check if the result returned from the act step meets your expectations by using the expect function. For example:
expect(result, true)
,expect(result, 1000)
,expect(result, “Minh”)
,…
So, we’ve just finished writing one unit test. The validateEmail
function needs to be tested with another case: when the email is empty, the function should return false
.
test('validateEmail should return false when the email is empty', () {
// Arrange
String invalidEmail = '';
// Act
bool result = Validator.validateEmail(invalidEmail);
// Assert
expect(result, false);
});
We easily write unit tests for the validatePassword
function in a similar way.
test('validatePassword should return true when the password is not empty', () {
// Arrange
String validPassword = 'password123';
// Act
bool result = Validator.validatePassword(validPassword);
// Assert
expect(result, true);
});
test('validatePassword should return false when the password is empty', () {
// Arrange
String invalidPassword = '';
// Act
bool result = Validator.validatePassword(invalidPassword);
// Assert
expect(result, false);
});
The utils_test.dart
file now has 4 test cases. We should group the test cases for the same function into groups using the group
function.
group('validateEmail', () {
test('validateEmail should return true when the email is not empty', () {
// body
});
test('validateEmail should return false when the email is empty', () {
// body
});
});
group('validatePassword', () {
test('validatePassword should return true when the password is not empty',
// body
});
test('validatePassword should return false when the password is empty', () {
// body
});
});
To run the unit tests, we use the command flutter test
or click on Run or Debug on VSCode like the image below. If the console logs "All tests passed!", then all our test cases have passed. If any test case fails, it will log the error information on the console.
group('login', () {
test('login should return false when the email is empty', () {
// Arrange
String? email;
String password = 'password123';
// Act
bool result = login(email, password);
// Assert
expect(result, false);
});
test('login should return false when the password is empty', () {
// Arrange
String email = 'ntminh@gmail.vn';
String? password;
// Act
bool result = login(email, password);
// Assert
expect(result, false);
});
test('login should return false when the email and password are empty', () {
// Arrange
String? email;
String? password;
// Act
bool result = login(email, password);
// Assert
expect(result, false);
});
test('login should return true when the email and password are not empty',
() {
// Arrange
String email = 'ntminh@gmail.vn';
String password = 'password123';
// Act
bool result = login(email, password);
// Assert
expect(result, true);
});
});
If someone accidentally deletes this code while refactoring. Don’t worry, when running the tests again, it will throw an error. This makes us more confident when refactoring the code.
The expect
function and Matcher
The expect
function is used to check whether the result value of an expression matches a condition (matcher) or not.
expect(actual, matcher);
The matcher can be a boolean value like true
, false
, a string value like "OK"
, or an integer value like 0
, -1
, etc. It can also be more complex expressions such as:
isNull
: used to check if the actual value is equal tonull
.isNotNull
: used to check if the actual value is notnull
.isTrue
: similar to comparing withtrue
.isFalse
: similar to comparing withfalse
.isList
: used to check if the actual value is aList
.isMap
: used to check if the actual value is aMap
.isA<T>()
: used to check if the actual value has the typeT
.isException
: used to check if the actual value is anException
.throwsArgumentError
: used to check if the call throws anArgumentError
.
We will explore more Matcher
in the next parts.
Conclusion
In this article, we have written basic unit tests. We will continue to write unit tests for more complex cases with the support of advanced techniques such as Mock, Fake, and Stub in the next sections.