Bách khoa toàn thư về Test trong Flutter. Tập 1: Các phương pháp test.

NALSengineering
8 min readJan 23, 2024
Source: Unsplash

Credit: Nguyễn Thành Minh (Mobile Developer)

Giới thiệu

Đây là một series toàn tập về Testings trong Flutter bao gồm Unit Testing, Widget Testing, Golden Testing, và Integration Testing. Series này sẽ giúp các bạn từ không biết gì đến hiểu rất rõ về các loại Testing trong Flutter. Trong mỗi bài viết, ngoài việc giải thích những khái niệm, mình sẽ đưa ra rất nhiều ví dụ cùng với những lỗi thường gặp khi viết test. Ngoài ra, mình cũng sẽ hướng dẫn cách viết code làm sao để dễ viết test, và đặc biệt là mình sẽ giới thiệu cách sử dụng AI như ChatGPT hay Github Copilot để cải thiện tốc độ viết test. Bây giờ chúng ta sẽ bắt đầu với phần đầu tiên: Các phương pháp test trong Flutter.

Tại sao chúng ta cần phải viết test?

Việc viết test mang lại rất nhiều lợi ích cho dự án. Theo mình thì có 3 lý do chính tại sao việc viết test là điều cần thiết:

  1. Đảm bảo chất lượng: Điều này là tất nhiên rồi. Việc viết test sẽ giúp chúng ta phát hiện sớm các bug và đảm bảo app hoạt động chính xác trong nhiều tình huống khác nhau trên nhiều hệ điều hành với nhiều version khác nhau và thậm chí là trên nhiều thiết bị khác nhau. Ngoài ra, có những test case rất khó để test bằng cách thủ công, ví dụ như việc gọi hơn 1000 API cùng một lúc, nhưng chúng ta có thể dễ dàng viết các bài test cho những trường hợp như vậy.
  2. Hỗ trợ việc refactor code: Việc refactor code là việc cực kỳ rủi ro bởi vì trong quá trình refactor, chúng ta rất dễ vô tình tạo ra bug. Nhưng nhờ việc chạy lại các bài test sau khi refactor, chúng ta sẽ phát hiện ra các bug đó nếu có. Điều đó khiến chúng ta cực kỳ tự tin khi refactor code.
  3. Tiết kiệm thời gian và chi phí: Mặc dù việc viết test làm tăng thêm chi phí cho dự án trong thời gian đầu nhưng về lâu dài nó sẽ giúp chúng ta tiết kiệm thời gian và chi phí. Rõ ràng việc phát hiện và fix bug sớm trong quá trình code hay trước khi build app sẽ ít tốn kém hơn so với sau khi build app cho tester test hoặc sau khi đã publish app. Hơn nữa, chúng ta chỉ cần viết một bài kiểm thử mà có thể chạy trên nhiều hệ điều hành, nhiều phiên bản, và nhiều thiết bị khác nhau. Trong khi đó, việc kiểm thử thủ công đòi hỏi chi phí lớn để thực hiện điều này. Ngoài ra, việc giảm thiểu rủi ro trong quá trình refactor code cũng giúp chúng ta tiết kiệm thời gian và chi phí một cách đáng kể.

Phân biệt các phương pháp test

Chúng ta có 4 phương pháp test phổ biến trong Flutter là Unit Testing, Widget Testing, Golden Testing, và Integration Testing. Chúng sẽ khác nhau về mục đích sử dụng, scope, và thời gian chạy test. Cụ thể là:

1. Unit Tests

Unit tests được dùng để test các function như static function, top-level function hoặc các method một cách riêng lẻ. Mục tiêu của unit test là xác minh tính đúng đắn của một function, một method trong nhiều điều kiện khác nhau. Ví dụ, giả sử chúng ta có 3 hàm saveToken, getToken, và login:

bool saveToken(String token) {
return sharedPreferences.saveToken(token);
}

String get token => secureStorage.token; // cố ý code sai

bool login(String email, String password) {
final token = apiClient.login(email, password);
return saveToken(token);
}

Như vậy, với unit testing, thì chúng ta cần phải viết test cho từng hàm một cách độc lập. Ví dụ, đối với hàm saveToken thì khi mình truyền token vào thì output của hàm phải là true hoặc false tuỳ kịch bản test. Tương tự, khi mình gọi getter token thì phải trả về được token đang được lưu trong SecureStorage. Đối với hàm login thì tuỳ vào input là emailpassword mà hàm sẽ trả về true hoặc false. Ngoài việc test Output dựa vào Input ra, chúng ta còn thể test xem liệu hàm apiClient.login có được gọi hay không và được gọi bao nhiêu lần. Nếu nó không được gọi hoặc được gọi nhiều hơn 1 lần thì dù output của hàm có trả về true thì code chúng ta cũng có thể đã có bug.

Tuy nhiên, việc viết unit test chỉ là điều kiện cần để đảm đảo app của chúng ta hoạt động chính xác. Trong ví dụ ở trên, mình đã cố ý code sai cụ thể là mình đã save token và SharedPreferences nhưng khi get token thì mình lại get từ SecureStorage thì chắc chắn không thể get được đúng token. Tại sao Unit Tests không thể phát hiện ra lỗi này? Vì nó chỉ tập trung test từng hàm một cách độc lập. Đối với hàm saveToken thì nhiệm vụ của nó là save token vào SharedPreferences chứ nó chẳng quan tâm chúng ta sẽ get token ra từ đâu. Tương tự, đối với getter token thì nhiệm vụ của nó là get token từ SecureStorage chứ nó cũng chẳng quan tâm chúng ta đã save token vào đâu. Tất nhiên, khi mỗi hàm làm đúng nhiệm vụ của nó thì xem như chúng ta pass các bài unit test. Tuy nhiên, trong thực tế khi run app thì 2 hàm này phối hợp với nhau lại gây ra bug. Lúc này, chúng ta cần đến Integration Tests.

2. Integration Tests

Integration Tests được dùng để test xem các class, function riêng lẻ phối hợp với nhau như thế nào hoặc dùng để test hiệu suất của một ứng dụng chạy trên thiết bị thực.

Source: Reddit

Ví dụ, giả sử chúng ta cần test tính năng login, khi user nhập đúng email và mật khẩu thì chúng ta sẽ di chuyển đến màn hình Home.

Source: Youtube

Nếu như Unit Test chỉ test output dựa trên input của từng function riêng lẻ thì khi run Integration Tests, nó sẽ khởi động app trên thiết bị thật hoặc máy ảo và tự động thao tác giống như một tester đang test app. Chính vì vậy, nó sẽ đảm bảo được app hoạt động chính xác hơn là chỉ dựa vào Unit Tests. Tuy nhiên, nhược điểm của nó là thời gian thực thi sẽ lớn hơn rất nhiều so với Unit Tests. Ngoài ra, khi nó phát hiện được bug thì chúng ta sẽ rất khó biết được chính xác cụ thể hàm nào đang bị bug.

Unit tests và integration tests thường được sử dụng để test logic của app. Nếu chúng ta muốn test về UI ví dụ như màu sắc của button có đúng với thiết kế không, button đó đang enable hay disable, và button đó có đang visible hay không thì chúng ta cần sử dụng đến Widget Tests và Golden Tests.

3. Widget Tests

Mục tiêu của Widget Tests là xác minh rằng UI của widget có giống thiết kế không và các tương tác của nó có như mong đợi không.

Cũng giống như các bài unit tests, các widgets tests cũng không yêu cầu phải run app trên máy thật hay máy ảo, vì vậy chúng ta cũng sẽ không mất nhiều thời gian để thực thi widget tests.

4. Golden Tests

Golden Tests cơ bản là Widget Tests nhưng Golden Tests có thể xác minh rằng vị trí của widget trên màn hình có chính xác hay không. Ví dụ, ngoài việc xác minh màu sắc của button có đúng với thiết kế không, button đó đang enable hay disable, và button đó có đang visible hay không thì nó còn xác minh được vị trí của button trên màn hình có đang giống với thiết kế không. Sở dĩ nó làm được điều đó là nhờ nó generate hình ảnh UI mong đợi của widget hay còn gọi là các golden images và so sánh nó với hình ảnh UI hiện tại của widget. Nếu cả hai hình ảnh khớp nhau, bài test sẽ pass. Nó có thể generate các golden images trên nhiều device với nhiều kích thước khác nhau như phone/tablet.

Ví dụ, đây là golden images, ảnh chụp UI mong đợi của trạng thái ban đầu và trạng thái sau khi đã click vào Floating Action Button 1 lần trên 2 device khác nhau là phone và tablet_landscape.

Expected UI (golden images)

Nếu chúng ta vô tình đổi màu sắc của Floating Action Button thành màu đỏ và thay đổi vị trí của 2 cái Text lên phía trên cùng như này:

Actual UI

Golden Tests sẽ phát hiện ra sự khác nhau này và báo lỗi cho chúng ta biết bằng những hình ảnh so sánh như dưới đây:

isolatedDiff
maskedDiff

Về thời gian thực thi, vì Golden Tests liên quan đến việc so sánh hình ảnh nên có thể chậm hơn Widget Tests nhưng Golden Tests sẽ giúp chúng ta tiết kiệm nhiều thời gian và chi phí của dự án hơn vì chỉ cần so sánh hình ảnh là chúng ta có thể xác minh được UI đúng hay không. Trong khi đó đối với Widget Tests, chúng ta phải kiểm tra màu sắc, thuộc tính của từng widget con trong màn hình nhưng cũng chưa xác minh được UI đúng hay không vì rất có thể bố cục, vị trí của từng widget con bị sai.

Kết luận

Qua bài viết này, mình đã giới thiệu tổng quan của 4 phương pháp test phổ biến trong Flutter. Trong bài viết tiếp theo, mình sẽ giới thiệu chi tiết về phương pháp Unit Testing.

--

--

NALSengineering

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