Bách khoa toàn thư về Test trong Flutter. Tập 2: Unit Test cơ bản.

NALSengineering
7 min readJan 23, 2024
Source: Unsplash

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:

  1. Writing your first Unit Test.
  2. Phân biệt các khái niệm: Mock, Fake, Stub.
  3. Các hàm test, group, setUp, tearDown, setUpAll, tearDownAll
  4. Các best practices.
  5. Cách viết code để dễ viết test.
  6. Các lỗi hay mắc phải khi viết test.
  7. Ứng dụng AI như ChatGPT và Github Copilot để viết test nhanh hơn.
  8. 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ằng null hay không
  • isNotNull: dùng để kiểm tra actual value có khác null hay không
  • isTrue: tương tự như khi so sánh với true
  • isFalse: tương tự như khi so sánh với false
  • isList: dùng để kiểm tra actual value có phải là List hay không
  • isMap: dùng để kiểm tra actual value có phải là Map hay không
  • isA<T>(): dùng để kiểm tra actual value có kiểu là T phải không
  • isException: dùng để kiểm tra actual value có phải là Exception hay không
  • throwsArgumentError: dùng để kiểm tra lệnh gọi có throw ArgumentError 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.

--

--

NALSengineering

Knowledge sharing empowers digital transformation and self-development in the daily practices of software development at NAL Solutions.