Bách khoa toàn thư về Test trong Flutter. Tập 2: Unit Test cơ bản.
Credit: Nguyễn Thành Minh (Mobile Developer)
Mở đầu
Trong phần trước, chúng ta đã tìm hiểu tổng quan về các phương pháp test phổ biến trong Flutter. Trong loạt bài tiếp theo, chúng ta sẽ tìm hiểu chi tiết về Unit Testing bao gồm:
- Writing your first Unit Test.
- Phân biệt các khái niệm: Mock, Fake, Stub.
- Các hàm test, group, setUp, tearDown, setUpAll, tearDownAll
- Các best practices.
- Cách viết code để dễ viết test.
- Các lỗi hay mắc phải khi viết test.
- Ứng dụng AI như ChatGPT và Github Copilot để viết test nhanh hơn.
- Tích hợp CI để kiểm tra các bài test và report Test Coverage.
Bây giờ chúng ta sẽ bắt đầu với một vài example đơn giản.
Thử viết một Unit Test đơn giản
Giả sử chúng ta đang cần test chức năng login và chức năng validate email và password.
Chúng ta sẽ có 2 static function đặt trong file lib/part2/util/utils.dart
class Validator {
static bool validateEmail(String value) {
return value.isNotEmpty;
}
static bool validatePassword(String value) {
return value.isNotEmpty;
}
}
Chúng ta cũng có 1 extension method đặt trong file lib/part2/ext/extension.dart
.
extension StringExtension on String? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
}
Cuối cùng là hàm login được đặt trong file lib/part2/feature/login.dart
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!);
}
Chúng ta cần test 4 hàm được đặt trong 3 file khác nhau, vậy chúng ta cần tạo ra 3 file test trong folder test
. Để phân biệt các file unit test và các file liên quan đến các phương pháp test khác như widget test, mình sẽ tạo ra 1 folder tên unit_test
bên trong folder test
.
Quy tắc đặt tên cho file test là sử dụng tên file code cộng với suffix _test.dart
. Ngoài ra, có thêm 1 quy tắc nữa là cấu trúc folder test
phải mirror folder lib
như thế này:
Repository: https://github.com/ntminhdn/testing_examples
Chúng ta sẽ bắt đầu viết test cho hàm validateEmail
trước.
Mỗi test file cần bắt đầu với hàm main()
như là entry point. Bên cạnh đó, để viết unit test, ta cần import package flutter_test
vào
import 'package:flutter_test/flutter_test.dart';
void main() {
}
Để tạo ra 1 unit test, ta sử dụng hàm test
truyền vào 2 param là description và body.
void main() {
test('validateEmail should return true when the email is not empty', () {
// body
});
}
Về việc đặt test description, không quan trọng description ngắn hay dài, nó nên được đặt sao cho người khác chỉ cần đọc là hiểu nội dung của unit test mà không cần phải đọc vào code. Mình thường đặt tên theo công thức:
[unit name] ... [should] ... [expected output] ... [when] ... context
Ví dụ: “validateEmail should return true when the email is not empty”.
Về body của một unit test, Thông thường body của một unit test sẽ follow Arrange, Act, and Assert (AAA) pattern.
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 là bước khởi tạo các biến, các input đầu vào trước khi gọi hàm cần test. Ví dụ, mình cần test hàm validateEmail khi email is not empty thì mình sẽ phải tạo 1 biến input
String validEmail = ‘test@example.com’
. - Act đơn giản là gọi hàm mà chúng ta cần test với input là những gì chúng ta chuẩn bị ở bước arrange:
Validator.validateEmail(validEmail)
. - Assert là bước kiểm tra xem kết quả trả về từ bước Act ở trên có đáp ứng mong đợi của bạn hay không bằng cách sử dụng hàm
expect
. Ví dụ:expect(result, true)
,expect(result, 1000)
,expect(result, “Minh”)
,…
Như vậy, chúng ta đã vừa viết xong 1 unit test. Hàm validateEmail
cần được test thêm 1 case nữa là khi email is empty thì hàm sẽ 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);
});
Tương tự, ta sẽ dễ dàng viết 2 unit test cho hàm validatePassword
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);
});
Như vậy file utils_test.dart
đã có 4 test cases, để dễ dàng quản lý và maintain sau này, chúng ta nhóm các test cases của cùng 1 hàm vào 1 group riêng bằng cách sử dụng hàm group
.
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
});
});
Để chạy các unit test trên, ta sử dụng command flutter test
, nếu console log ra All tests passed!
thì tất cả test cases của chúng ta đã pass. Ngược lại, nếu có test case nào đó bị fail thì nó sẽ log ra thông tin của lỗi đó trên console.
Ngoài ra, có 1 cách khác mà mình hay sử dụng hơn là chạy trực tiếp trên VSCode bằng cách click vào Run
hay Debug
của hàm main
nếu muốn chạy tất cả test cases của cả file, hoặc của hàm group
nếu muốn chạy tất cả test cases của 1 group nào đó, hoặc của hàm test
nếu chỉ muốn chạy duy nhất test case đó. Thông tin về test case nào pass, test case nào fail sẽ được hiển thị ở cửa sổ Debug Console
như trong ảnh dưới.
Bạn hãy thử viết test cho extension method isNullOrEmpty
. Bây giờ, mình sẽ viết test cho top-level function login
.
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);
});
});
Nếu ai đó vô tình xoá đi đoạn code này trong lúc refactor code, khi chạy lại test, nó sẽ báo lỗi, nhờ đó mà bạn có thể tự tin refactor code hơn.
Hàm expect
và Matcher
expect
là hàm được sử dụng để kiểm tra xem giá trị kết quả của một biểu thức có đúng với một điều kiện (matcher) hay không.
expect(actual, matcher);
Matcher có thể là giá trị kiểu bool
như true
, false
, hay kiểu String
như “OK”
, hoặc kiểu int
như 0
, -1
,…, hoặc nó cũng có thể là những biểu thức phức tạp hơn như:
isNull
: dùng để kiểm tra actual value có bằngnull
hay khôngisNotNull
: dùng để kiểm tra actual value có khácnull
hay khôngisTrue
: tương tự như khi so sánh vớitrue
isFalse
: tương tự như khi so sánh vớifalse
isList
: dùng để kiểm tra actual value có phải làList
hay khôngisMap
: dùng để kiểm tra actual value có phải làMap
hay khôngisA<T>()
: dùng để kiểm tra actual value có kiểu làT
phải khôngisException
: dùng để kiểm tra actual value có phải làException
hay khôngthrowsArgumentError
: dùng để kiểm tra lệnh gọi có throwArgumentError
hay không
Chúng ta sẽ tìm hiểu nhiều Matcher hơn trong những bài tiếp theo.
Kết luận
Trong bài này, chúng ta đã viết được các unit tests cơ bản. Trong bài tới, chúng ta sẽ tiếp tục viết unit tests cho các case phức tạp hơn với sự hỗ trợ của các kỹ thuật phức tạp như Mock, Fake, Stub.