Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 45 — Microservices Testing Strategy

Mở đầu — vì sao bài này quan trọng

Cách đây mười năm, hầu hết ứng dụng doanh nghiệp là một khối "monolith" — toàn bộ code nằm trong một chương trình duy nhất. Muốn test, bạn dựng lên một bản, chạy toàn bộ kịch bản end-to-end, xong. Nhưng khi các công ty như Tiki, Shopee, MoMo, VNPay hay các ngân hàng số như Cake, Timo chuyển sang kiến trúc microservices — chia hệ thống thành hàng chục, thậm chí hàng trăm dịch vụ nhỏ độc lập — thì chiến lược test cũ sụp đổ hoàn toàn.

Hãy hình dung: một giao dịch "đặt hàng và thanh toán" trên một sàn thương mại điện tử có thể đi qua 15 service khác nhau — service giỏ hàng, service tồn kho, service giá, service khuyến mãi, service thanh toán, service ví, service vận chuyển, service thông báo... Mỗi service do một team khác nhau sở hữu, deploy độc lập, thậm chí viết bằng ngôn ngữ khác nhau. Câu hỏi đặt ra cho một QA Leader là: làm sao đảm bảo chất lượng khi không ai kiểm soát được toàn bộ hệ thống cùng một lúc?

Đây chính là trọng tâm của Bài 45. Testing microservices không phải là "test nhiều hơn" mà là "test khác đi". Nếu bạn mang tư duy monolith vào microservices, bạn sẽ rơi vào bẫy: cố dựng cả 50 service lên để test end-to-end, môi trường tốn kém khủng khiếp, test chạy hàng giờ, flaky liên tục, và mỗi khi một team thay đổi API là hàng chục test của team khác đỏ lòm. Với vai trò QA Leadership, bạn cần một chiến lược giúp mỗi team test nhanh và tự tin trong phạm vi của mình, đồng thời cả hệ thống vẫn được bảo vệ khỏi lỗi tích hợp.

Khái niệm cốt lõi

Bốn thách thức đặc trưng của microservices

Trước khi bàn giải pháp, ta phải gọi tên đúng bốn "nỗi đau" mà kiến trúc microservices tạo ra cho QA:

  • Phụ thuộc chồng chéo (interdependency). 50+ service gọi lẫn nhau. Một thay đổi nhỏ ở service A có thể làm hỏng service D mà team A không hề hay biết.
  • Mỗi team sở hữu một service (team ownership). Không còn một QA nào nắm toàn bộ. Trách nhiệm chất lượng bị phân mảnh theo ranh giới team.
  • Địa ngục versioning. Service Thanh toán v2.3 chạy tốt với service Ví v1.8, nhưng khi Ví nhảy lên v2.0 thì hợp đồng giao tiếp thay đổi. Ai đảm bảo các phiên bản tương thích?
  • Môi trường test đắt đỏ. Dựng cả 50 service để chạy một test end-to-end tốn tài nguyên, chậm, và gần như không thể tái lập ổn định.

Test Pyramid không đủ — cần Testing Honeycomb / Test Diamond

Trong monolith, ta quen với Test Pyramid: nhiều unit test, ít integration, rất ít end-to-end. Trong microservices, mô hình được điều chỉnh. Nhiều chuyên gia dùng hình Testing Honeycomb (tổ ong) do Spotify đề xuất: phần lớn giá trị nằm ở tầng integration test (test một service với các phụ thuộc của nó), lớp mỏng unit test ở dưới, và lớp cực mỏng end-to-end ở trên cùng. Lý do: trong microservices, phần lớn rủi ro không nằm trong logic một hàm, mà nằm ở ranh giới giao tiếp giữa các service.

Các tầng test trong microservices

  • Unit test: kiểm tra logic nội bộ một service, không gọi mạng. Nhanh, nhiều.
  • Component test: dựng nguyên một service độc lập, thay các phụ thuộc bên ngoài bằng bản giả (mock/stub), test toàn bộ API của service đó.
  • Contract test (kiểm thử hợp đồng): trái tim của chiến lược microservices. Thay vì test thật giữa hai service, ta kiểm tra rằng "hợp đồng" — cấu trúc request/response mà consumer mong đợi và provider cung cấp — luôn khớp nhau.
  • Integration test: test một service với các phụ thuộc thật (database, message queue) trong phạm vi hẹp.
  • End-to-end test: test một luồng nghiệp vụ thật đi qua nhiều service, chạy trên môi trường gần production. Rất ít, chỉ cho các luồng critical.

Contract Testing — giải pháp cho địa ngục versioning

Đây là khái niệm quan trọng nhất bạn cần nắm. Trong Consumer-Driven Contract Testing (điển hình là công cụ Pact), service tiêu dùng (consumer) khai báo: "Tôi gọi endpoint này, tôi mong nhận về JSON có các trường A, B, C". Khai báo đó được đóng gói thành một "contract" và gửi tới team của service cung cấp (provider). Provider chạy contract này trong pipeline của mình. Nếu provider thay đổi API làm mất trường B, contract test đỏ ngay trong pipeline của provider, trước khi deploy — không cần dựng cả hệ thống, không cần chờ end-to-end.

Đây chính là cách hóa giải "địa ngục versioning": thay vì test tổ hợp mọi phiên bản của mọi service (điều bất khả thi), mỗi cặp consumer–provider chỉ cần đảm bảo hợp đồng của họ không bị phá vỡ.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT giả định "ChợViệt": cái bẫy end-to-end

ChợViệt là một sàn thương mại điện tử tại TP.HCM, có 42 microservice và 9 team. Ban đầu, đội QA quyết định giữ tư duy cũ: dựng một môi trường "staging đầy đủ" chứa cả 42 service, và viết 380 kịch bản end-to-end tự động chạy mỗi đêm.

Sáu tháng sau, tình hình thảm hại. Bộ test end-to-end mất 4 giờ 20 phút mỗi lần chạy. Tỷ lệ flaky (đỏ giả) lên tới 35% — cứ 3 lần chạy thì 1 lần đỏ vì một service nào đó restart, timeout, hoặc dữ liệu test bị team khác ghi đè. Đội QA dành phần lớn thời gian đi "điều tra" xem lỗi thật hay flaky. Tệ hơn, mỗi khi team Thanh toán deploy lúc 2 giờ chiều, cả staging sập, và test của 8 team còn lại vô dụng đến hết ngày.

QA Leader mới về đã làm ba việc. Thứ nhất, cắt 380 end-to-end xuống còn 22 kịch bản cho các luồng sinh tiền quan trọng nhất (đăng nhập, tìm kiếm, đặt hàng, thanh toán COD, thanh toán ví). Thứ hai, đưa contract test bằng Pact vào pipeline của từng cặp service. Thứ ba, yêu cầu mỗi team viết component test cho service của mình với các phụ thuộc được mock.

Kết quả sau 3 tháng: thời gian phản hồi lỗi tích hợp giảm từ "phát hiện lúc chạy end-to-end ban đêm" xuống "phát hiện trong pipeline của chính team gây lỗi, trong vòng 8 phút". Số incident production do lỗi tương thích API giảm 70%.

Bài học: End-to-end test trong microservices là con dao hai lưỡi — hữu ích khi ít và tập trung, thảm họa khi nhiều và tham lam. Phần lớn niềm tin phải đến từ contract và component test.

Ví dụ 2 — Ngân hàng số "MBank" giả định: contract test cứu một sự cố ví điện tử

MBank, một ngân hàng số tại Hà Nội, có service payment-service (consumer) gọi wallet-service (provider) để trừ tiền trong ví. Team Ví đang refactor, quyết định đổi tên trường balance_after trong response thành remaining_balance cho "nhất quán đặt tên".

Trong mô hình cũ, thay đổi này chỉ bị phát hiện khi ai đó đặt hàng thật trên staging và thấy màn hình báo số dư sai. Nhưng MBank đã áp dụng Pact. Khi team Ví đẩy commit đổi tên trường, contract test do team Thanh toán publish chạy ngay trong CI của team Ví và đỏ lập tức: "Consumer payment-service mong đợi trường balance_after, nhưng provider không còn cung cấp".

Team Ví nhận thông báo, hiểu rằng đây là breaking change, và xử lý đúng cách: giữ cả hai trường trong giai đoạn chuyển tiếp, thông báo team Thanh toán cập nhật, rồi mới gỡ trường cũ sau khi consumer đã chuyển. Không có incident nào chạm tới khách hàng.

Bài học: Contract test biến một lỗi runtime tiềm ẩn ở production thành một lỗi biên dịch rõ ràng trong pipeline. Nó cũng tạo ra một "hợp đồng xã hội" giữa các team: bạn không được đơn phương phá vỡ API mà consumer đang dựa vào.

Ví dụ 3 — Startup logistics "GiaoNhanh": môi trường test thông minh với ephemeral environment

GiaoNhanh, một startup giao hàng ở Đông Nam Á với 28 service, đối mặt bài toán chi phí môi trường. Dựng một bản staging đầy đủ tốn tiền cloud và luôn bị tranh chấp giữa các team.

Thay vì một staging dùng chung, họ chuyển sang ephemeral environment (môi trường tạm thời): mỗi khi có Pull Request, pipeline tự dựng một namespace Kubernetes riêng, chỉ chứa service đang thay đổi cộng với các phụ thuộc trực tiếp của nó (được cấu hình qua service mesh). Các service khác được thay bằng stub thông minh hoặc dùng bản đã deploy sẵn qua kỹ thuật service virtualization. Sau khi PR merge, môi trường tự hủy.

Nhờ vậy, họ không bao giờ cần dựng cả 28 service cho một test. Chi phí cloud cho môi trường test giảm khoảng 60%, và các team không còn giẫm chân nhau vì mỗi PR có sandbox riêng.

Bài học: Trong microservices, đừng cố dựng "tất cả". Hãy dựng "đủ" — service của bạn cộng vòng phụ thuộc trực tiếp — và ảo hóa phần còn lại. Ephemeral environment biến môi trường test từ tài nguyên khan hiếm thành thứ dùng-rồi-vứt.

Hướng dẫn từng bước

Nếu bạn là QA Leader được giao xây dựng chiến lược test cho một hệ thống microservices, đây là lộ trình thực tế:

Bước 1 — Vẽ bản đồ phụ thuộc. Lập sơ đồ service nào gọi service nào (service dependency map). Xác định các "hub" — service bị nhiều service khác phụ thuộc (thường là auth, payment, user). Đây là nơi rủi ro tương thích cao nhất và cần contract test trước tiên.

Bước 2 — Định nghĩa ranh giới trách nhiệm test. Mỗi team sở hữu: toàn bộ unit test và component test của service mình; các consumer contract mà service mình phát ra; và việc verify các contract mà service mình là provider. QA nền tảng (platform QA) sở hữu bộ end-to-end critical và hạ tầng test chung.

Bước 3 — Chuẩn hóa component test. Yêu cầu mỗi service test được độc lập: khởi động service, mock mọi phụ thuộc bên ngoài, chạy toàn bộ API của nó. Đây là "lá chắn" đầu tiên và nhanh nhất.

Bước 4 — Triển khai contract testing. Chọn công cụ (Pact cho consumer-driven, hoặc Spring Cloud Contract). Bắt đầu từ 2-3 cặp service quan trọng nhất trên bản đồ. Dựng Pact Broker làm nơi lưu và chia sẻ contract giữa các team. Tích hợp bước verify contract vào pipeline provider.

Bước 5 — Thu gọn end-to-end. Rà lại toàn bộ E2E hiện có. Chỉ giữ những kịch bản đi qua nhiều service và bảo vệ luồng sinh tiền/critical. Mục tiêu: đếm trên đầu ngón tay, chạy dưới 20 phút, flaky dưới 5%.

Bước 6 — Giải bài toán môi trường. Chuyển từ staging dùng chung sang ephemeral environment theo PR, kết hợp service virtualization cho các phụ thuộc xa. Đảm bảo mỗi môi trường có bộ dữ liệu test độc lập, tự dọn.

Bước 7 — Đặt quality gate và đo lường. Định nghĩa rõ: PR không được merge nếu contract test đỏ; deploy production phải qua bộ E2E critical. Theo dõi chỉ số: thời gian phát hiện lỗi tích hợp, tỷ lệ incident do breaking change, thời gian chạy pipeline.

Lỗi thường gặp & mẹo

  • Lỗi: Cố test end-to-end tất cả. Đây là sai lầm số một. Bạn sẽ có bộ test chậm, flaky, đắt đỏ và không ai tin. Mẹo: coi E2E là "sản phẩm cao cấp, số lượng ít", còn niềm tin hàng ngày đến từ contract và component test.
  • Lỗi: Nhầm contract test với integration test. Contract test không gọi service thật của provider — nó verify rằng provider có thể thỏa mãn hợp đồng. Đừng dựng cả hai service lên để chạy Pact; như vậy là mất hết lợi ích tốc độ.
  • Lỗi: Không có Pact Broker, contract sống rải rác trong repo. Contract phải được chia sẻ tập trung để provider luôn verify đúng phiên bản consumer đang chạy. Mẹo: dùng Pact Broker và bật tính năng "can-i-deploy" để kiểm tra tương thích trước khi deploy.
  • Lỗi: Dữ liệu test dùng chung gây flaky. Nhiều team ghi/xóa cùng một bộ dữ liệu trên staging chung là nguyên nhân số một của test đỏ giả. Mẹo: mỗi môi trường/test tự tạo dữ liệu của mình (test data isolation), dọn sau khi chạy.
  • Mẹo về versioning: Áp dụng nguyên tắc tương thích ngược — khi đổi API, thêm trường mới thay vì đổi/xóa trường cũ; giữ giai đoạn chuyển tiếp để consumer kịp cập nhật. Contract test sẽ nhắc bạn khi nào an toàn để gỡ trường cũ.
  • Mẹo về observability: Trong microservices, không phải mọi lỗi đều bắt được bằng test trước release. Kết hợp test với giám sát production (distributed tracing, health check) để phát hiện lỗi tích hợp còn sót — đây là tinh thần bổ trợ giữa shift-left và shift-right.

Bài tập thực hành

  • Vẽ bản đồ và chọn điểm nóng. Lấy một hệ thống bạn biết (thật hoặc giả định 8-10 service như một sàn TMĐT). Vẽ sơ đồ phụ thuộc, chỉ ra 2 service "hub" bị nhiều service phụ thuộc nhất, và giải thích vì sao chúng cần contract test trước.
  • Viết một hợp đồng consumer. Chọn một cặp service (ví dụ order-service gọi inventory-service). Viết ra bằng lời hoặc pseudo-JSON: consumer gửi request gì, mong nhận response có những trường nào. Sau đó mô tả điều gì xảy ra nếu provider xóa một trường — contract test phản ứng ra sao và ở pipeline của ai.
  • Tái phân bổ test pyramid. Cho một service có 200 test end-to-end. Đề xuất phương án tái phân bổ: bao nhiêu chuyển thành unit, component, contract; giữ lại bao nhiêu E2E và cho luồng nào. Ước tính tác động lên thời gian chạy và độ ổn định.
  • Thiết kế môi trường test. Viết một đề xuất ngắn (nửa trang) chuyển từ staging dùng chung sang ephemeral environment cho một team 6 service. Nêu rõ: dựng service nào thật, ảo hóa service nào, dữ liệu test quản lý ra sao.

Tóm tắt

Testing microservices đòi hỏi thay đổi tư duy chứ không chỉ thêm công cụ. Bốn thách thức cốt lõi — phụ thuộc chồng chéo, mỗi team sở hữu một service, địa ngục versioning, và môi trường đắt đỏ — khiến chiến lược end-to-end kiểu monolith trở nên vô dụng và tốn kém.

Chiến lược đúng đặt trọng tâm vào contract testing để hóa giải versioning và phá vỡ phụ thuộc khi test; dùng component test để mỗi team test nhanh và độc lập trong phạm vi của mình; thu gọn end-to-end xuống một nhúm kịch bản critical; và giải bài toán môi trường bằng ephemeral environment cộng service virtualization thay vì cố dựng cả hệ thống. Vai trò của QA Leadership là vẽ ranh giới trách nhiệm rõ ràng giữa các team, dựng hạ tầng chung (Pact Broker, quality gate), và biến chất lượng thành một hợp đồng xã hội mà không team nào được đơn phương phá vỡ.

Ba câu chuyện của ChợViệt, MBank và GiaoNhanh cho thấy cùng một bài học: trong microservices, đừng test "tất cả cùng một lúc" — hãy test đúng thứ, đúng chỗ, đúng thời điểm, và để mỗi ranh giới giao tiếp tự bảo vệ chính nó.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi