Một đợt triển khai gần đây đã làm hỏng ba microservices mặc dù mọi unit test, integration test và kiểm tra mock-server đều vượt qua. Nhóm phát triển đã thay thế các API mock của họ bằng contract testing. Trong sáu tháng, số lượng contract đã tăng từ 3 lên 47 và tỷ lệ lỗi tích hợp hàng tháng đã giảm từ hai sự cố xuống còn không.

Tại sao mock không thể bảo vệ bạn

Một mock server chỉ mô phỏng cấu trúc mà consumer mong đợi; nó không bao giờ kiểm tra xem provider có thực sự cung cấp cấu trúc đó hay không. Nếu một provider đổi tên một trường—chẳng hạn từ name thành display_name—mock vẫn trả về payload cũ, các bài test của consumer vẫn báo xanh, và hệ thống thực tế sẽ bị sập. Sự cố trên môi trường production vào lúc 2 giờ chiều thứ Ba chính xác là như vậy: mock đã "nói dối" về hợp đồng thực tế.

Contract testing lấp đầy khoảng trống

Contract testing buộc hai phía của một API phải thống nhất về một định nghĩa chung trước khi bất kỳ mã nguồn nào được đưa lên production. Có hai cách tiếp cận phổ biến:

  • Consumer-driven contracts – dịch vụ tiêu thụ (consumer) viết các kỳ vọng; dịch vụ cung cấp (provider) xác thực chúng. Cách này hoạt động tốt cho các microservices nội bộ cùng phát triển với nhau.
  • Provider-driven contracts – provider công bố một đặc tả (specification); các consumer kiểm tra mã nguồn của họ dựa trên đó. Đây là mô hình thông thường cho các public API.

Cách tiếp cận đầu tiên thường giúp ngăn chặn các lỗi tích hợp bên trong kiến trúc microservice.

Cách thức hoạt động của consumer-driven contract

  1. Consumer viết một bài test mô tả chính xác những gì nó cần từ provider.
  2. Việc chạy bài test sẽ tạo ra một pact file – một tài liệu JSON ghi lại các kỳ vọng đó.
  3. Provider chạy dịch vụ thực tế của mình đối chiếu với pact file trong CI pipeline.
  4. Nếu provider thay đổi một trường, việc xác thực sẽ thất bại và quá trình build sẽ bị chặn lại.

Vì việc xác thực được thực hiện trên mã nguồn thực tế của provider, bất kỳ thay đổi gây lỗi nào cũng được phát hiện sớm, thay vì sau khi triển khai.

Vị trí của contract test trong kim tự tháp kiểm thử

  • Unit tests – nhanh, kiểm tra logic cô lập.
  • Contract tests – tốc độ trung bình, xác nhận các thỏa thuận API được duy trì.
  • End-to-end tests – chậm, thực hiện toàn bộ luồng nghiệp vụ.

Hãy coi contract test như một cầu nối giữa phản hồi nhanh chóng của unit test và phạm vi bao phủ rộng lớn của các bộ end-to-end. Hãy tập trung vào các điểm tích hợp thường xuyên bị lỗi nhất và bắt đầu với hai hoặc ba endpoint quan trọng.

Câu chuyện áp dụng trong thực tế

Nhóm phát triển khởi xướng bài viết này đã bắt đầu với ba contract bao phủ các lời gọi (calls) dễ lỗi nhất của họ. Sáu tháng sau, họ đã có 47 contract bao phủ phần lớn lưu lượng truy cập giữa các service. Trong giai đoạn đó, các sự cố phá vỡ API đã giảm từ hai lần mỗi tháng xuống còn không.

Khi nào contract testing có thể không xứng đáng để đầu tư

  • Bạn là lập trình viên làm việc độc lập và giữ tất cả các service trong một repository duy nhất.
  • API cực kỳ ổn định và không thay đổi trong nhiều năm.
  • Bạn đang xây dựng một bản mẫu (prototype) dùng một lần và sẽ sớm loại bỏ nó.

Trong những kịch bản đó, chi phí quản lý (overhead) để duy trì các contract có thể lớn hơn lợi ích mang lại.

Các nhược điểm tiềm ẩn và cách giảm thiểu

  • Giữ các contract được đánh phiên bản (versioned) cùng với mã nguồn mà chúng mô tả.
  • Tự động hóa việc xác thực trong mỗi lần chạy CI để tránh các contract lỗi thời.
  • Xem xét các thay đổi contract trong pull request để phát hiện các lỗi phá vỡ hệ thống vô ý.

Bài học rút ra

Nếu bạn vẫn đang dựa vào các mock được tạo thủ công để tự thuyết phục rằng các service của mình có thể giao tiếp với nhau, bạn đang đặt cược vào một lời hứa hão huyền. Contract testing biến sự đặt cược đó thành một thỏa thuận có thể xác minh được, giúp phát hiện các thay đổi gây lỗi trước khi chúng đến môi trường production và, như các con số của nhóm phát triển đã cho thấy, có thể loại bỏ hoàn toàn các lỗi tích hợp.