Tối ưu CI/CD cho Hệ thống Microservices - Bài 5 - Chiến lược Testing không làm nghẽn Pipeline
I. Nghịch lý của Testing trong thế giới phân tán
Khi làm việc với Monolith, bạn chỉ cần dựng một database, chạy bộ test từ đầu đến cuối là xong. Nhưng với microservices, mọi thứ phức tạp hơn nhiều. Giả sử bạn đang làm việc ở Service Order, để tạo một đơn hàng thành công, nó phải gọi sang Service Inventory (kiểm tra tồn kho), Service User (lấy thông tin khách), và bắn message vào Kafka.
Nỗi đau bắt đầu xuất hiện khi cấu hình CI/CD:
-
Flaky Tests (Test chập chờn): Lúc pass lúc fail chỉ vì mạng nội bộ chậm, hoặc Service
Inventoryđang bị down trên môi trường staging. -
Nặng nề và tốn tài nguyên: Để test một đoạn code nhỏ của
Order, CI Runner phải "còng lưng" dùngdocker-composedựng cả database, Kafka, Redis và 5 services khác lên. Pipeline chạy mất 30 phút thay vì 3 phút.
II. Tái cấu trúc "Kim tự tháp Testing" cho Microservices
Để không làm nghẽn Trunk-based Development (nơi developer merge code liên tục), tư duy testing cần thay đổi: Fail Fast (Phát hiện lỗi càng nhanh càng tốt).
-
Unit Test (Chiếm 70-80%): Đây là vòng bảo vệ đầu tiên và phải chạy chớp nhoáng (dưới 1 phút cho toàn bộ service). Bí quyết là Mock mọi thứ. Không kết nối tới Database thật, không gọi API thật, không kết nối tới Kafka thật. Chỉ test thuần túy logic của các hàm (Business Logic).
-
Integration Test (Chiếm 15-20%): Chạy ngay sau khi Unit Test thành công. Ở bước này, chúng ta test xem service của mình có tương tác đúng với các thành phần phụ trợ (Database, Cache, Message Broker) hay không, nhưng tuyệt đối không phụ thuộc vào các microservices khác.
-
End-to-End (E2E) Test (Rất hạn chế): Chỉ chiếm 5%. Việc test luồng xuyên suốt hàng tá services nên được chạy định kỳ (nightly build) hoặc chạy trên một pipeline tách biệt ở môi trường Staging, không block quy trình CI của từng service riêng lẻ.
III. Vũ khí bí mật cho Integration Test: Testcontainers
Việc dùng Mock cho Database thường không đáng tin cậy (ví dụ: mock PostgreSQL trả về kết quả đúng, nhưng câu query thật lại bị lỗi cú pháp). Nhưng dùng chung Database Dev cho CI thì lại hay bị xung đột dữ liệu giữa các lần build.
Testcontainers chính là chân ái (hỗ trợ cực tốt cho cả Node.js và Go). Khi pipeline CI chạy Integration Test:
-
Code test của bạn sẽ ra lệnh cho Docker khởi tạo một container PostgreSQL hoặc Redis "sạch tinh tươm" và hoàn toàn độc lập (ephemeral container).
-
Chạy migration và seed dữ liệu mẫu vào đó.
-
Thực thi các test case trực tiếp với Database thật.
-
Khi test chạy xong, container tự động bị hủy (destroy), không để lại rác. Pipeline kết thúc.
IV. Làm sao để test giao tiếp giữa các Service? (Contract Testing)
Để test xem Service Order có gọi đúng API của Service Inventory hay không mà không cần dựng Service Inventory lên, chúng ta dùng Mock Server (như Wiremock) hoặc Consumer-Driven Contract Testing (Pact).
Thay vì gọi thật, Service Order (Consumer) và Service Inventory (Provider) thống nhất với nhau một "Bản hợp đồng" (Contract - file JSON định nghĩa Request/Response).
-
Bên
Orderchỉ cần test xem code của mình có tạo ra Request đúng định dạng như Hợp đồng hay không. -
Bên
Inventorychỉ test xem API của mình có trả về đúng định dạng đã cam kết trong Hợp đồng không. Luồng CI của ai người nấy chạy, cực kỳ nhanh gọn nhưng vẫn đảm bảo hai bên nói chuyện trơn tru.
V. Tổng kết bài học
Luồng CI lý tưởng cho một microservice sẽ là: Bắt đầu -> Chạy Unit Test (Mock toàn bộ) -> Chạy Integration Test (Dùng Testcontainers cho DB/Redis, dùng Wiremock/Pact để giả lập service khác) -> Build Docker Image. Toàn bộ quá trình này phải gói gọn dưới 5 phút.
All rights reserved