Hướng dẫn viết Unit Test cho service gọi API

Tóm tắt nhanh
Tìm hiểu cách viết Unit Test cho các service gọi API trong Flutter, sử dụng mocking để test mà không cần gọi API thật.
Mục lục bài viết (42)
- Setup Dependencies
- 1. Thêm packages
- 2. Generate mocks
- Ví dụ: API Service
- 1. Tạo API Service
- Tạo Mock với Mockito
- 1. Tạo Mock Class
- 2. Generate Mock
- Viết Unit Tests
- 1. Test GET Request
- 2. Test POST Request
- 3. Test List Request
- Test Error Cases
- 1. Network Errors
- 2. Invalid JSON
- Best Practices
- 1. Arrange-Act-Assert Pattern
- 2. Test Edge Cases
- 3. Verify Calls
- Vì sao phải tách lớp service trước khi test
- Chọn công cụ test
- Cấu trúc một test đọc được
- Kiểm tra dữ liệu gửi lên
- Test các trường hợp lỗi
- Mô hình hoá kết quả: ném lỗi hay trả về Result
- Test thời gian chờ mà không cần chờ thật
- Test cache và phân trang
- Chọn test cái gì và đo độ bao phủ
- Chạy test trong quy trình tự động
- Lỗi thường gặp khi viết test cho service
- Câu hỏi thường gặp
- Có nên test cả tầng repository lẫn tầng service không?
- Test service có cần chạy trên thiết bị thật không?
- Làm sao test khi API dùng chứng chỉ tự ký hoặc môi trường nội bộ?
- Khi nào nên viết test tích hợp với server thật?
- Ví dụ đầy đủ: service có cache và phân trang
- Bốn nhóm test tối thiểu cho mỗi service
- Test nhánh xác thực: 401 và làm mới token
- Ba dấu hiệu bộ test đang mất giá trị
- Kết luận
- Tóm tắt:
- Học Flutter bài bản theo lộ trình
Viết Unit Test cho API services là kỹ năng quan trọng. Bài viết này sẽ hướng dẫn bạn cách test API services với mocking.
Setup Dependencies
1. Thêm packages
[object Object]2. Generate mocks
[object Object]Ví dụ: API Service
1. Tạo API Service
[object Object]Tạo Mock với Mockito
1. Tạo Mock Class
[object Object]2. Generate Mock
[object Object]Viết Unit Tests
1. Test GET Request
[object Object]2. Test POST Request
[object Object]3. Test List Request
[object Object]Test Error Cases
1. Network Errors
[object Object]2. Invalid JSON
[object Object]Best Practices
1. Arrange-Act-Assert Pattern
[object Object]2. Test Edge Cases
[object Object]3. Verify Calls
[object Object]Vì sao phải tách lớp service trước khi test
Test service chỉ dễ khi service được thiết kế để test được. Ba nguyên tắc thiết kế quyết định điều đó:
1. Service nhận phụ thuộc từ ngoài. Nếu service tự tạo Dio() hay http.Client() bên trong, bạn không thể thay bằng bản giả và mọi test sẽ gọi mạng thật.
[object Object]2. Service trả về mô hình của ứng dụng, không trả về Map. Nếu hàm trả Map<String, dynamic>, tầng gọi phải tự hiểu cấu trúc; đổi API là đổi ở nhiều nơi. Ngược lại, khi service trả về Course, test có thể khẳng định trên đối tượng rõ ràng.
3. Service chỉ làm một việc: giao tiếp với API. Việc lọc, sắp xếp, cache phức tạp nên nằm ở tầng repository để mỗi tầng có thể test riêng.
Chọn công cụ test
Ba lựa chọn phổ biến:
mockito— sinh lớp giả bằng trình sinh mã. Phù hợp dự án lớn, nhiều interface; cần chạybuild_runner.mocktail— không cần sinh mã, viết tay ít hơn, phù hợp dự án nhỏ hoặc khi không muốn thêm bước sinh mã vào quy trình.httpvớiMockClient— gọn nhất khi service dùng góihttpchuẩn, vì bạn chỉ cần trả về phản hồi giả mà không cần mock lớp nào.
Với service dùng dio, cách gọn nhất là thay HttpClientAdapter hoặc dùng một adapter giả. Cách này kiểm tra được cả interceptor và cấu hình, thay vì chỉ kiểm tra lớp service.
[object Object]Cấu trúc một test đọc được
Mọi test nên theo ba phần rõ ràng: Arrange (chuẩn bị), Act (hành động), Assert (khẳng định). Viết theo cấu trúc này giúp test đọc như một câu chuyện, và khi test đỏ bạn biết ngay phần nào sai.
[object Object]Một mẹo thực tế: đặt khẳng định về yêu cầu gửi lên ngay trong adapter (như expect(options.path, '/courses')). Nhờ vậy test kiểm tra cả đầu vào lẫn đầu ra, chứ không chỉ đầu ra.
Kiểm tra dữ liệu gửi lên
Phần lớn lỗi tích hợp API nằm ở dữ liệu gửi sai: thiếu trường, sai kiểu, sai tên khoá. Hãy khẳng định rõ những gì service gửi:
[object Object]Với Dio, cũng nên test các thiết lập chung: header xác thực có được gắn không, tham số truy vấn có đúng kiểu không, và thời gian chờ có được cấu hình không. Đây là những thứ mà lỗi chỉ lộ ra trên production nếu không có test.
Test các trường hợp lỗi
Đây là phần quan trọng nhất và cũng là phần bị bỏ qua nhiều nhất. Một service chỉ chắc chắn khi các nhánh lỗi đã được kiểm tra.
[object Object]Điểm quan trọng về thiết kế: service nên chuyển lỗi kỹ thuật thành lỗi nghiệp vụ mà tầng giao diện hiểu được. DioException là chi tiết của thư viện; NetworkException và ApiException là ngôn ngữ của ứng dụng bạn. Nhờ vậy khi đổi từ dio sang gói khác, tầng giao diện không phải sửa gì.
Mô hình hoá kết quả: ném lỗi hay trả về Result
Hai trường phái, và cả hai đều hợp lệ nếu dùng nhất quán:
- Ném ngoại lệ: hàm trả về dữ liệu khi thành công, ném khi lỗi. Code gọi ngắn hơn nhưng dễ quên
try/catch. - Trả
Result<T>: hàm luôn trả về một đối tượng có hai nhánh. Không thể quên xử lý lỗi vì trình biên dịch buộc bạn xử lý cả hai nhánh.
[object Object]Với Result, test cũng rõ ràng hơn: bạn khẳng định kiểu của kết quả thay vì bắt ngoại lệ, và có thể kiểm tra thêm thông tin như isRetryable — thứ mà giao diện cần để quyết định có hiện nút thử lại hay không.
Test thời gian chờ mà không cần chờ thật
Đây là kỹ thuật giúp bộ test vẫn chạy nhanh dù kiểm tra các hành vi liên quan tới thời gian.
[object Object]Cách này tốt hơn việc dùng Future.delayed thật trong test: bạn kiểm tra đúng logic thử lại, và bộ test không bị chậm thêm vài giây mỗi lần chạy. Nếu buộc phải kiểm tra hành vi theo thời gian thật, hãy truyền khoảng chờ qua constructor như trên thay vì hard-code trong service.
Test cache và phân trang
Hai phần này thường bị bỏ qua nhưng lại chứa nhiều lỗi logic:
[object Object]Test thứ hai kiểm tra một lỗi rất hay gặp: quên truyền cursor nên luôn tải lại trang đầu.
Chọn test cái gì và đo độ bao phủ
Đừng đặt mục tiêu "bao phủ 100%". Hãy ưu tiên theo mức thiệt hại khi sai:
- Chuyển đổi dữ liệu (parse JSON, tính toán) — sai là hỏng toàn bộ tính năng.
- Nhánh lỗi — ít được thử bằng tay nhưng xảy ra thường xuyên trong thực tế.
- Gửi dữ liệu lên — sai trường nào là sai bản ghi đó.
- Logic cache và phân trang — lỗi chỉ lộ ra khi dữ liệu lớn hoặc dùng lâu.
- Cấu hình chung (header xác thực, thời gian chờ) — sai là lỗi toàn hệ thống.
Đo độ bao phủ bằng flutter test --coverage. Con số bao phủ hữu ích để phát hiện file chưa có test nào, nhưng đừng dùng nó như thước đo chất lượng: một file có 100% dòng được chạy vẫn có thể chứa lỗi logic.
Chạy test trong quy trình tự động
Test chỉ có giá trị khi được chạy thường xuyên. Cấu hình mức tối thiểu cho một dự án:
[object Object]Chi tiết đáng lưu ý: chạy flutter analyze trước flutter test giúp phát hiện lỗi tĩnh nhanh hơn nhiều so với chờ test chạy xong. Và nên đặt test làm điều kiện bắt buộc trước khi merge — nếu không, test sẽ dần bị bỏ qua.
Lỗi thường gặp khi viết test cho service
- Gọi mạng thật trong test. Test sẽ chậm, hay lỗi khi mạng yếu, và không kiểm tra được nhánh lỗi.
- Chỉ test trường hợp thành công. Đây là lý do phổ biến nhất khiến lỗi chỉ xuất hiện ở môi trường thật.
- Phụ thuộc thứ tự chạy test. Nếu test chỉ đúng khi chạy sau một test khác, bạn đang có trạng thái dùng chung — hãy khởi tạo lại mọi thứ trong
setUp. - Dùng
sleepđể chờ. Thay bằng truyền khoảng chờ giả hoặc chờ đúngFuturecần chờ. - Khẳng định quá chặt vào thông báo lỗi. Chỉ cần khẳng định loại ngoại lệ; đổi câu chữ thông báo sẽ làm test đỏ hàng loạt mà không có lỗi thật.
- Test phụ thuộc thời gian hệ thống. Truyền thời gian hiện tại vào service để có thể kiểm tra ổn định.
- Không test việc xoá token khi gặp 401. Đây là nhánh liên quan bảo mật, rất đáng test.
Câu hỏi thường gặp
Có nên test cả tầng repository lẫn tầng service không?
Có, nhưng nội dung khác nhau: service kiểm tra giao tiếp HTTP (đường dẫn, tham số, chuyển đổi dữ liệu), repository kiểm tra logic nghiệp vụ như cache, phân trang, kết hợp nhiều nguồn. Nếu bạn thấy hai tầng test trùng nhau, có thể bạn đang đặt logic ở sai tầng.
Test service có cần chạy trên thiết bị thật không?
Không. Test service là test Dart thuần, chạy bằng flutter test trên máy tính. Chỉ những bài kiểm tra liên quan tới quyền, thông báo hệ thống hay API nền tảng mới cần chạy trên thiết bị.
Làm sao test khi API dùng chứng chỉ tự ký hoặc môi trường nội bộ?
Đừng cố mô phỏng điều đó ở tầng test. Hãy cấu hình một BaseOptions khác cho môi trường kiểm thử và dùng adapter giả như các ví dụ trên.
Khi nào nên viết test tích hợp với server thật?
Chỉ cho một số ít luồng quan trọng (đăng nhập, thanh toán) và chạy riêng, không nằm trong bộ test nhanh. Test tích hợp với server thật giúp phát hiện thay đổi API, nhưng chậm và dễ lỗi vì môi trường — đừng dùng nó thay cho test đơn vị.
Ví dụ đầy đủ: service có cache và phân trang
Ghép các phần trên thành một service thực tế, kèm test tương ứng. Đây là cấu trúc gần với dự án thật nhất:
[object Object]Bộ test cho service này nên phủ bốn nhóm:
[object Object]Hàm _dioWith và _jsonResponse là các hàm tiện ích nhỏ trong chính file test. Việc gom chúng lại giúp mỗi test chỉ tập trung vào điều nó muốn kiểm tra, thay vì lặp lại phần dựng Dio và tạo phản hồi giả.
Bốn nhóm test tối thiểu cho mỗi service
Đây là danh sách mà bạn có thể áp dụng cho mọi service gọi API trong dự án, không cần suy nghĩ lại từ đầu:
Nhóm 1 — đường dẫn và tham số. Kiểm tra URL, phương thức, tham số truy vấn, phần thân yêu cầu. Đây là nhóm phát hiện nhiều lỗi tích hợp nhất, vì các lỗi kiểu sai tên khoá hay thiếu tham số thường không gây lỗi rõ ràng mà chỉ trả về dữ liệu rỗng.
Nhóm 2 — chuyển đổi dữ liệu. Với mỗi phản hồi mẫu, kiểm tra đối tượng trả về có đúng kiểu, đúng số phần tử, và xử lý được trường hợp thiếu khoá. null ở một số trường là chuyện rất hay gặp với API thật, và nếu bạn không kiểm tra, ứng dụng sẽ gặp lỗi ở màn hình thay vì ở tầng dữ liệu.
Nhóm 3 — bốn loại lỗi. Mỗi service nên có test cho: lỗi mạng (mất kết nối), hết thời gian chờ, lỗi 4xx và lỗi 5xx. Bốn nhánh này dẫn tới bốn cách xử lý khác nhau ở giao diện, nên chúng không thể thay thế cho nhau.
Nhóm 4 — trạng thái phụ thuộc. Với service có cache, thử lại, phân trang hoặc xác thực, nhóm này kiểm tra hành vi nhiều bước: gọi hai lần có dùng cache không, lần thử lại thứ ba mới thành công thì kết quả ra sao, gặp 401 có xoá token và thử refresh không.
Một cách kiểm tra bạn đã phủ đủ chưa: đọc lại từng dòng trong service và tự hỏi "nếu dòng này sai, test nào sẽ đỏ?". Những dòng không có test nào bảo vệ thường là những dòng chứa điều kiện đặc biệt — tức là chính những chỗ dễ sai nhất.
Nếu bạn muốn đi sâu hơn vào công cụ tạo lớp giả cho những phụ thuộc này, xem bài hướng dẫn Mock API với Mockito. Còn để hiểu bức tranh toàn cảnh về các loại test trong Flutter, xem bài giới thiệu unit test, widget test, integration test.
Test nhánh xác thực: 401 và làm mới token
Đây là nhánh liên quan bảo mật và cũng là nhánh hay bị bỏ qua nhất. Luồng thực tế gồm ba tình huống, và cả ba đều nên có test:
[object Object]Test thứ ba kiểm tra đúng lỗi khó thấy nhất trong thực tế: nhiều yêu cầu cùng nhận 401 và cùng kích hoạt làm mới token, tạo ra vòng lặp hoặc vô hiệu hoá phiên của người dùng. Nếu service của bạn chưa có cơ chế hàng đợi cho tình huống này, test này sẽ đỏ và chỉ ra vấn đề trước khi người dùng gặp phải.
Ba dấu hiệu bộ test đang mất giá trị
Bộ test không tự động tốt lên theo thời gian; nó thường thoái hoá âm thầm. Ba dấu hiệu sau cho biết đã đến lúc phải xem lại:
- Test luôn xanh dù bạn cố tình làm sai logic. Nếu sửa một dòng trong service mà không test nào đỏ, dòng đó đang không được bảo vệ — hãy kiểm tra lại bằng cách tạm thời đổi kết quả trả về và chạy lại bộ test.
- Sửa một hàng đợi nhỏ phải sửa hàng chục test. Nghĩa là test đang khẳng định vào cách triển khai thay vì hành vi. Hãy chuyển khẳng định về kết quả cuối: dữ liệu trả về, trạng thái sau cùng, dữ liệu gửi lên API.
- Bộ test chạy quá vài phút. Thường là do gọi mạng thật hoặc chờ bằng
Future.delayed. Bộ test nhanh mới được chạy thường xuyên; chậm là dấu hiệu đầu tiên của việc bị bỏ qua trong các lần sửa nhỏ.
Một cách duy trì đơn giản: mỗi khi sửa một lỗi thật, hãy viết một test tái hiện lỗi đó trước khi sửa. Nhờ vậy bộ test lớn lên theo đúng những lỗi đã từng xảy ra — đây là loại bao phủ có giá trị hơn mọi con số phần trăm.
Kết luận
Viết Unit Test cho API services:
- ✅ Mock dependencies: Sử dụng Mockito
- ✅ Test success cases: Verify correct behavior
- ✅ Test error cases: Handle errors properly
- ✅ Verify calls: Ensure correct API calls
Tóm tắt:
- Setup: Thêm mockito và build_runner
- Create Mocks: Generate mock classes
- Write Tests: Test success và error cases
- Verify: Verify API calls được thực hiện đúng
Viết Unit Test tốt sẽ giúp API services của bạn reliable và maintainable hơn! 🚀
Học Flutter bài bản theo lộ trình
Nếu bạn muốn học có người sửa bài thay vì tự ghép tài liệu, Flutter Hướng Nghiệp đào tạo theo 3 cấp:
- Flutter Cơ Bản — Dart, widget, giao diện, 28 buổi.
- Flutter Nâng Cao — state management, REST API, Firebase, 28 buổi.
- Flutter Chuyên Sâu — back-end, AI/ML, publish lên store, 53 buổi.
Lớp online trực tiếp qua Zoom/Google Meet, có bản ghi lại — không bắt buộc đến cơ sở.
Xem lịch khai giảng hoặc so sánh 5 cách học lập trình mobile nếu còn phân vân.
