Testing Microservices — Test Pyramid & E2E (Building Microservices)
Mở đầu
Trong hệ thống microservices, testing là chủ đề dễ gây tranh cãi nhất. Monolith test bằng cách chạy cả ứng dụng trong một process duy nhất; microservices thì phân tán, giao tiếp qua mạng, mỗi service lại sở hữu database riêng. Chapter 9 của "Building Microservices" không chỉ giới thiệu test pyramid mà còn đưa ra thứ tự ưu tiên rõ ràng: test những thứ dễ vỡ nhất, và dùng contract test để giữ cho các service vẫn deploy độc lập mà không làm vỡ nhau.
Ảnh: Jakub Zerdzicki — Pexels
Test Pyramid — vẫn đúng, nhưng phải điều chỉnh
Test pyramid chia test thành ba tầng: unit test ở đáy (nhiều, nhanh, rẻ), service test ở giữa, và E2E test trên đỉnh (ít, chậm, đắt). Hình dung như kim tự tháp Ai Cập — đáy càng to càng vững, đỉnh càng nhỏ càng đỡ tốn.
Điểm mấu chốt mà Sam Newman nhấn mạnh: trong microservices, tỷ lệ này không thể sao chép máy móc từ monolith. Unit test chỉ kiểm tra được logic nội bộ của một service; sự tương tác giữa các service — thứ thường xuyên gây lỗi nhất — nằm ở tầng service test và E2E. Vì vậy đừng mù quáng chạy theo tỷ lệ 70/20/10 kiểu sách giáo khoa, hãy điều chỉnh theo mức độ rủi ro thực tế của hệ thống.
Unit Tests — lớp nền kim tự tháp
Unit test kiểm tra một hàm, một class, một behavior đơn lẻ. Không gọi database, không gọi network, không đụng filesystem — nếu cần thì dùng test double (stub, mock) để cô lập. Nhờ vậy chúng chạy trong mili giây, không bao giờ flaky, và có thể chạy lại trong CI sau mỗi lần commit.
Nhưng đừng kỳ vọng unit test giải quyết hết mọi thứ. Một service có thể có unit test phủ 90% logic nội bộ mà vẫn vỡ toàn tập khi giao tiếp với service khác — vì lỗi thường nằm ở biên, không nằm trong lõi. Đó là lý do tầng service test quan trọng hơn nhiều trong thế giới microservices.
Service Tests — tầng quan trọng nhất
Service test (còn gọi là integration test hay subsystem test) kiểm tra một service một cách đầy đủ: xử lý request, đọc ghi database, gửi nhận message — trong khi các dependency bên ngoài được thay bằng stub hoặc chạy thật. Đây là tầng đáng đầu tư nhất vì nó bắt được lỗi ở biên giao tiếp mà unit test không với tới.
Một lưu ý thực tế: in-memory database kiểu H2 có behavior khác hẳn Postgres thật — cái test pass trên H2 có thể fail trên production. Xu hướng hiện tại là dùng testcontainers để chạy database thật trong test, như bài về Testcontainers đã nói khá kỹ. Chi phí cao hơn một chút, nhưng độ tin cậy tăng lên rất nhiều.
Contract Tests — vũ khí bí mật của microservices
Kịch bản kinh điển: team A deploy service của mình lên production, team B vẫn gọi API cũ — hệ thống vỡ ngay vì không ai deploy hai bên cùng lúc. Contract test sinh ra để giải quyết đúng vấn đề này.
Ý tưởng của consumer-driven contract (CDC): bên gọi (consumer) định nghĩa rõ hợp đồng — request thế nào, response ra sao — rồi publish lên một nơi chung (Pact Broker). Bên cung cấp (provider) chạy verify hợp đồng đó trong CI của mình. Nếu thay đổi API làm vỡ hợp đồng, provider biết ngay trước khi deploy, thay vì phát hiện ra khi production đã sập. Nhờ đó các service vẫn giữ được khả năng deploy độc lập — thứ mà integration test full-stack không bao giờ cho phép.
Ảnh: ThisIsEngineering — Pexels
Contract test không thay thế integration test, nó bổ sung: contract test nhanh, tập trung vào hợp đồng API; integration test đắt hơn nhưng kiểm tra hành vi tổng thể. Tỷ lệ hợp lý là contract test cho mọi cặp consumer-provider quan trọng, integration test chỉ cho những luồng rủi ro cao.
E2E Tests — cần thiết nhưng phải kiểm soát
E2E test chạy toàn bộ hệ thống trên môi trường giống production nhất có thể. Nó là kiểm chứng cuối cùng rằng mọi thứ phối hợp đúng, nhưng cái giá rất đắt: cần dựng môi trường đầy đủ, seed dữ liệu, chạy chậm, và dễ flaky vì network timeout, thứ tự sự kiện, dữ liệu không deterministic.
Sam Newman khuyên giữ số lượng E2E rất ít, chỉ cho happy path chính của hệ thống. Nếu một E2E test flaky quá mức, đừng ngại xoá nó — một test không đáng tin còn tệ hơn không có test, vì nó giết chết niềm tin của cả team vào bộ test. Sau deploy, smoke test nhanh (kiểm tra service chạy, connect được database, trả về response) cũng là lớp bảo vệ quan trọng, nối tiếp chủ đề deployment và K8s của bài trước.
Ảnh: MedPoint 24 — Pexels
Key Takeaways
- Test pyramid vẫn là kim chỉ nam, nhưng trong microservices tầng service test mới là quan trọng nhất — đừng sao chép tỷ lệ của monolith.
- Consumer-driven contract test (Pact) giúp các service deploy độc lập mà không làm vỡ nhau, phát hiện breaking change trước khi lên production.
- E2E test giữ ở mức tối thiểu, chỉ cho happy path; smoke test sau deploy là lớp bảo vệ rẻ mà hiệu quả.
- Test theo rủi ro: dành công sức cho phần dễ vỡ và quan trọng nhất, không chạy theo chỉ số coverage.
Glossary
| Thuật ngữ | Ý nghĩa |
|---|---|
| Test pyramid | Mô hình phân bổ test: nhiều unit test ở đáy, ít E2E test trên đỉnh |
| Unit test | Test đơn vị nhỏ nhất (hàm/class), không chạm I/O bên ngoài |
| Service test | Test một service đầy đủ với dependency bên ngoài được thay bằng stub hoặc chạy thật |
| Test double | Stub, mock, fake — vật thay thế dependency thật trong test |
| Contract test | Test hợp đồng API giữa consumer và provider, không cần chạy cả hai cùng lúc |
| Consumer-Driven Contract | Hợp đồng do bên gọi định nghĩa, bên cung cấp phải verify trong CI |
| Pact | Công cụ CDC phổ biến nhất, dùng Pact Broker để chia sẻ hợp đồng |
| E2E test | Test toàn bộ hệ thống trên môi trường giống production |
| Smoke test | Test nhanh sau deploy, kiểm tra service hoạt động cơ bản |
Kết
Testing trong microservices không phải chuyện "viết nhiều test là xong", mà là chuyện phân bổ đúng nguồn lực: unit test cho logic nội bộ, service test cho biên giao tiếp, contract test cho sự độc lập giữa các team, và E2E chỉ cho những gì thật sự cần. Có test tốt làm nền, bước tiếp theo của series là chuyện vận hành — làm sao biết hệ thống đang chạy ổn hay không, khi mà lỗi có thể nằm ở bất kỳ service nào trong hàng chục service. Bài sau sẽ nói về monitoring.