0

Chúng mình đã chọn công cụ test các cuộc gọi giữa service trong CI như thế nào

Khoảng tháng 5 năm nay, team mình có 9 service chạy trên Kubernetes, nói chuyện với nhau chủ yếu qua REST và một ít qua Kafka. Unit test thì service nào cũng có, coverage nhìn cũng ổn. Nhưng cứ mỗi lần deploy lên staging là y như rằng có một hai chỗ gãy ở giữa các service: service A gửi thêm field mới mà service B chưa đọc, timeout cấu hình lệch nhau, header auth bị rơi mất khi đi qua gateway.

Sau lần thứ ba trong một tháng phải rollback, anh lead viết đúng một câu trong channel: "Từ sprint sau, PR nào đụng tới API giữa các service phải có test chạy trong CI." Không ai phản đối. Vấn đề là test bằng cái gì.

Bài này là ghi chép lại quá trình bọn mình chọn công cụ, gồm cả mấy hướng đã thử rồi bỏ.

Bài toán cụ thể của team

Trước khi đi xem công cụ, bọn mình ngồi viết ra những gì thực sự cần. Việc này tốn một buổi chiều nhưng tiết kiệm rất nhiều thời gian cãi nhau về sau.

Mấy điểm bọn mình chốt:

  • Test phải chạy được trên GitHub Actions, runner mặc định, không cần cluster riêng.
  • Thời gian chạy thêm cho mỗi PR không quá 6 phút. Pipeline hiện tại đã mất khoảng 9 phút, thêm nữa là dev bắt đầu than.
  • Khi test fail, người đọc log phải biết ngay request nào, gửi tới service nào, nhận về cái gì.
  • Phải xử lý được dependency bên ngoài như cổng thanh toán và dịch vụ gửi SMS, vì không thể gọi thật mỗi lần chạy CI.

Điểm cuối hóa ra là điểm khó nhất.

Vòng 1: tự viết bằng HTTP client và docker compose

Cách đầu tiên ai cũng nghĩ tới. Dựng các service cần thiết bằng docker compose, viết test bằng một thư viện HTTP client, gọi API rồi assert kết quả. Dependency bên ngoài thì dựng một mock server nhỏ trả về JSON cố định.

Ưu điểm là hiểu rõ từng dòng. Nhược điểm lộ ra sau khoảng hai tuần: thư mục mock JSON phình ra rất nhanh, và chẳng ai cập nhật khi API thật thay đổi. Một bạn trong team phát hiện mock của cổng thanh toán vẫn trả về format từ năm ngoái, trong khi bên đó đã đổi field amount từ số nguyên sang chuỗi có phần thập phân. Test vẫn xanh. Production thì không.

Thêm nữa, dựng docker compose với 5 service mất gần 3 phút chỉ để các container healthy. Cộng thời gian chạy test là vượt ngân sách 6 phút.

Vòng 2: đi xem các hướng khác

Lúc này mình mới thật sự ngồi đọc. Mình bắt đầu từ một bài tổng hợp về integration testing tools để có cái nhìn tổng quan, sau đó tự chia các lựa chọn thành mấy nhóm theo cách chúng tiếp cận vấn đề.

Nhóm đầu là contract testing, kiểu Pact. Consumer viết ra kỳ vọng của mình, provider chạy verify. Hay ở chỗ không cần dựng cả hệ thống, nhưng đòi hỏi cả hai phía cùng tham gia. Với 9 service do 3 nhóm khác nhau quản lý, chuyện này nghĩa là phải đổi quy trình khá nhiều.

Nhóm thứ hai là dùng Testcontainers để dựng dependency thật ngay trong test. Rất hợp với database và message queue, nhưng không giải quyết được chuyện mock dịch vụ bên ngoài.

Nhóm thứ ba là record và replay: ghi lại traffic thật giữa các service rồi dùng nó để sinh test và mock. Keploy là một ví dụ trong nhóm này, nó bắt traffic ở tầng network rồi tạo test case cùng mock tương ứng. Điểm hay là mock được sinh từ response thật, nên ít bị lệch so với API thực tế hơn mock viết tay.

Nhóm cuối là chạy các collection API có sẵn trong CI, ví dụ collection của Postman. Dễ bắt đầu, nhưng khó quản lý khi số test lớn dần và khá khó review trong PR.

Vòng 3: thử thật trên 2 service

Thay vì chọn trên giấy, bọn mình lấy 2 service hay gãy nhất là order và payment làm thí điểm, thử song song hai hướng trong một sprint.

Hướng contract testing chạy cực nhanh, chưa tới 1 phút. Nhưng team payment mất gần hết sprint mới setup xong phần provider verification, trong khi họ còn hai việc gấp khác. Không phải lỗi của công cụ, chỉ là chi phí phối hợp cao.

Hướng record và replay thì phía bọn mình setup nhanh hơn, vì không cần team khác thay đổi gì. Bọn mình ghi traffic trên staging vài ngày, lọc ra khoảng 40 test case có ý nghĩa, rồi cho chạy trong CI. Mất khoảng 2 phút rưỡi. Vướng một chỗ là traffic ghi lại có dữ liệu thay đổi theo thời gian như timestamp và ID ngẫu nhiên, phải cấu hình bỏ qua mấy field đó khi so sánh. Mất thêm một buổi chiều nữa.

Cái bọn mình chốt

Không có công cụ nào thắng tuyệt đối, nên bọn mình kết hợp.

Các cuộc gọi giữa service nội bộ, loại hay thay đổi, dùng record và replay để sinh test và mock, chạy trên mỗi PR. Vài API quan trọng nhất mà hai team cùng sở hữu thì dùng contract test để hai bên có cam kết rõ ràng. Database và Kafka thì dùng container thật trong CI.

Mình viết một trang wiki nội bộ giải thích khi nào dùng cái nào, kèm ví dụ cụ thể. Trang đó có lẽ là thứ hữu ích nhất cả dự án, vì người mới vào không phải đoán.

Vài điều mình rút ra

Làm API integration testing không chỉ là gọi endpoint rồi check status 200. Phần khó thật sự là quản lý dependency và giữ cho mock không trôi xa khỏi thực tế. Khi chọn công cụ, nên hỏi trước một câu: khi API thật thay đổi, mock của mình sẽ được cập nhật bằng cách nào? Nếu câu trả lời là "chắc ai đó sẽ nhớ sửa", thì gần như chắc chắn sẽ không ai sửa.

Điều thứ hai là đừng chọn công cụ chỉ qua bài so sánh. Bài so sánh giúp biết có những hướng nào, nhưng chỉ khi thử trên chính service của mình mới thấy chi phí thật nằm ở đâu. Với bọn mình, chi phí lớn nhất không nằm ở công cụ mà nằm ở việc phối hợp giữa các team.

Sau ba tháng, số lần rollback vì lỗi giao tiếp giữa service giảm từ khoảng 3 lần mỗi tháng xuống còn 1 lần trong cả quý. Lần đó là do config môi trường, không liên quan tới code. Vẫn chưa hoàn hảo, nhưng ít nhất bây giờ deploy chiều thứ Sáu không còn là chuyện đáng sợ nữa.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí