Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là QA engineer tại một ví điện tử ở TP.HCM. Tính năng mới là "hoàn tiền tự động khi giao dịch thất bại". Để test nó, bạn cần gọi tới cổng thanh toán của ngân hàng đối tác. Nhưng có ba vấn đề: (1) API ngân hàng chỉ hoạt động giờ hành chính; (2) mỗi lần gọi sandbox tính phí; (3) để tái hiện đúng lỗi "giao dịch timeout sau 30 giây", bạn phải chờ... 30 giây thật, và ngân hàng không cho bạn ép nó timeout theo ý muốn.
Nếu test suite của bạn phụ thuộc vào những thứ bạn không kiểm soát được — service của team khác chưa deploy xong, API của bên thứ ba đắt tiền và chậm, hay một hệ thống mainframe đời cũ chỉ chạy nửa ngày — thì test của bạn sẽ chậm, phập phù (flaky), và tốn kém. Đây chính là lúc service virtualization (ảo hóa dịch vụ) và mocking (giả lập) bước vào.
Ý tưởng cốt lõi rất đơn giản: thay vì gọi tới dependency thật, ta thay nó bằng một phiên bản giả — một "diễn viên đóng thế" — trả về đúng response ta cần, đúng lúc ta cần, hoàn toàn dưới sự kiểm soát của ta. Nắm vững kỹ năng này giúp bạn test được cả những kịch bản mà production không bao giờ cho phép tái hiện, tách bạch phần code của mình khỏi lỗi của người khác, và biến một test suite "chờ 10 phút, đỏ ngẫu nhiên" thành "chạy 30 giây, xanh ổn định".
Khái niệm cốt lõi
Mocking vs. Service Virtualization — khác nhau ở đâu?
Hai thuật ngữ này thường bị dùng lẫn lộn, nhưng chúng ở hai tầng khác nhau. Bài này tập trung vào ảo hóa ở tầng service/network, không đi sâu vào test double trong unit test (dummy, stub, spy, mock, fake — đó là chủ đề riêng của bài khác trong khóa học).
- Mocking (theo nghĩa hẹp trong bài này) là chặn một lời gọi HTTP/gRPC và trả về response giả. Nó thường nhẹ, chạy trong process của test, sống ngắn — dựng lên rồi tắt đi trong một bài test.
- Service virtualization là dựng một dịch vụ giả có trạng thái và hành vi phong phú, chạy như một server độc lập, mô phỏng cả latency, lỗi, và logic phản hồi khác nhau theo request. Nó "giống thật" đến mức nhiều team, nhiều môi trường có thể cùng dùng chung như một dependency ổn định.
Vì sao phải giả lập dependency?
Có bốn động lực chính, và bạn nên thuộc lòng để biện luận với PM khi họ hỏi "sao phải tốn công dựng cái giả này":
- Dependency chưa sẵn sàng. Team backend đang code service B, nhưng team frontend cần test tích hợp ngay. Nếu đã thống nhất "hợp đồng" (contract) — request/response ra sao — thì team frontend dựng một virtual service theo contract đó và làm việc song song, không phải chờ. Đây là kiểu làm việc parallel development cực kỳ giá trị.
- Gọi thật tốn tiền và chậm. Payment gateway (MoMo, VNPay), SMS OTP (Twilio, eSMS), dịch vụ AI theo token — mỗi lần gọi đều mất tiền hoặc quota. Chạy 500 test mỗi lần CI mà gọi thật thì hóa đơn cuối tháng sẽ khiến sếp giật mình.
- Khó tái hiện kịch bản lỗi. Làm sao ép API đối tác trả về HTTP 503? Làm sao mô phỏng "mạng chập chờn, response về sau 8 giây"? Với service thật, gần như bất khả thi. Với virtual service, bạn chỉ cần cấu hình một dòng.
- Cách ly để test đúng thứ mình muốn. Khi test đỏ, bạn muốn biết chắc lỗi nằm ở code của mình, chứ không phải vì sandbox của đối tác đang bảo trì. Giả lập dependency loại bỏ nhiễu, làm test deterministic (kết quả luôn nhất quán).
Các mức độ giả lập
- Stubbing tĩnh: luôn trả cùng một response cho một endpoint. Đơn giản, đủ dùng cho phần lớn happy path.
- Response theo điều kiện (request matching): trả response khác nhau tùy vào path, header, query, hoặc body. Ví dụ:
POST /payvớiamount > 10000000thì trả lỗi "vượt hạn mức". - Stateful (có trạng thái): virtual service nhớ được các lời gọi trước. Ví dụ mô phỏng luồng OTP: lần đầu
POST /otptrả "đã gửi", lần sauPOST /verifyvới đúng mã thì trả "thành công". - Fault injection (tiêm lỗi): cố tình trả lỗi, delay, ngắt kết nối, trả JSON méo mó — để test khả năng chịu lỗi của hệ thống bạn.
Công cụ phổ biến
- WireMock (Java, nhưng chạy standalone được cho mọi ngôn ngữ) — mạnh nhất cho HTTP virtualization, hỗ trợ record-and-playback, stateful scenario, fault injection.
- MockServer — tương tự WireMock, có expectation linh hoạt.
- Mockoon — GUI thân thiện, non-coder dùng được, dựng mock API trong vài phút.
- Hoverfly — chuyên record/replay traffic thật thành virtual service.
- json-server / Prism — nhẹ, dựng mock từ file JSON hoặc từ OpenAPI spec.
Tình huống thực tế
Ví dụ 1 — Ví điện tử ở TP.HCM: cắt 70% thời gian CI
Một startup fintech (gọi là "PayViet") có test suite tích hợp 320 case, trong đó khoảng 120 case chạm tới sandbox của cổng ngân hàng NAPAS. Mỗi call sandbox mất trung bình 2–4 giây, và tệ hơn, sandbox thường xuyên bảo trì ngoài giờ khiến CI đêm đỏ ngẫu nhiên. Đội QA đo được tỷ lệ "false failure" (đỏ không phải do lỗi code) lên tới 15% mỗi tuần.
Họ dựng một WireMock server mô phỏng NAPAS: ghi lại (record) các response thật từ sandbox trong một buổi, chỉnh sửa để che dữ liệu nhạy cảm, rồi dùng làm virtual service trong CI. Kết quả sau một tháng: thời gian chạy nhóm test này giảm từ ~11 phút xuống ~3 phút, false failure gần như về 0. Quan trọng hơn, họ vẫn giữ một tập nhỏ ~15 smoke test gọi sandbox thật, chạy 1 lần/ngày, để bắt trường hợp NAPAS thay đổi API.
Bài học: ảo hóa để chạy nhanh và ổn định phần lớn test, nhưng đừng ảo hóa 100% — luôn giữ một lớp test thật mỏng để phát hiện contract bị lệch.
Ví dụ 2 — Sàn TMĐT: phát triển song song khi service chưa xong
Một sàn thương mại điện tử Đông Nam Á đang xây tính năng "gợi ý sản phẩm bằng AI". Team recommendation cần 3 tuần để hoàn thành service, nhưng team frontend và team QA không thể ngồi chờ. Hai bên ngồi lại, thống nhất một OpenAPI contract: endpoint GET /recommendations?userId=... trả về mảng sản phẩm với score.
Team QA dùng Prism để sinh mock server thẳng từ file OpenAPI đó. Frontend build UI, QA viết sẵn test tích hợp — tất cả chạy trên virtual service. Khi service thật xong, họ chỉ đổi base URL từ mock sang thật. Có đúng 2 test đỏ vì service thật trả thiếu trường score trong một nhánh — và đó chính là giá trị: contract test đã bắt được sai lệch ngay ngày đầu tích hợp, thay vì phát hiện muộn ở production.
Bài học: virtual service dựng từ contract cho phép nhiều team làm song song, và biến "ngày tích hợp" từ một cơn ác mộng thành một buổi kiểm tra nhẹ nhàng.
Ví dụ 3 — Test kịch bản lỗi timeout của SMS OTP
Một app ngân hàng số cần đảm bảo: nếu nhà cung cấp SMS (eSMS) phản hồi chậm quá 5 giây, hệ thống phải hiển thị "Gửi lại mã" thay vì treo màn hình. Không đời nào eSMS chịu cố tình chậm cho bạn test. Đội QA cấu hình WireMock với withFixedDelay(6000) cho endpoint gửi OTP, và một stub khác trả HTTP 503 để test nhánh "dịch vụ quá tải".
Nhờ vậy họ phát hiện một bug nghiêm trọng: khi eSMS timeout, code cũ vẫn trừ quota gửi tin của người dùng dù tin chưa đi. Bug này gần như không thể tìm ra nếu chỉ test với dịch vụ thật luôn phản hồi nhanh.
Bài học: fault injection là "siêu năng lực" của service virtualization — nó cho bạn test những góc tối mà môi trường thật không bao giờ tự tạo ra.
Hướng dẫn từng bước
Dưới đây là quy trình dựng một virtual service với WireMock (dạng standalone, dùng được cho mọi stack). Ta lấy ví dụ giả lập cổng thanh toán.
Bước 1 — Xác định dependency và contract. Liệt kê các endpoint bạn cần giả: POST /v1/pay, GET /v1/status/{id}. Ghi rõ request mẫu và response mẫu cho từng kịch bản (thành công, hết hạn mức, lỗi hệ thống). Nếu có OpenAPI/Swagger của đối tác, dùng nó làm nguồn chân lý.
Bước 2 — Khởi động virtual server. Chạy WireMock standalone:
java -jar wiremock-standalone.jar --port 8089
Từ giờ mọi lời gọi tới http://localhost:8089 sẽ do WireMock trả lời.
Bước 3 — Định nghĩa stub cho happy path. Tạo file mappings/pay-success.json:
{
"request": {
"method": "POST",
"urlPath": "/v1/pay",
"bodyPatterns": [{ "matchesJsonPath": "$[?(@.amount <= 10000000)]" }]
},
"response": {
"status": 200,
"jsonBody": { "transactionId": "TXN123", "status": "SUCCESS" },
"headers": { "Content-Type": "application/json" }
}
}
Bước 4 — Thêm stub cho kịch bản lỗi. Với số tiền vượt hạn mức, trả lỗi nghiệp vụ:
{
"request": {
"method": "POST",
"urlPath": "/v1/pay",
"bodyPatterns": [{ "matchesJsonPath": "$[?(@.amount > 10000000)]" }]
},
"response": {
"status": 422,
"jsonBody": { "error": "LIMIT_EXCEEDED" }
}
}
Bước 5 — Tiêm lỗi latency/fault. Thêm "fixedDelayMilliseconds": 6000 vào phần response để mô phỏng chậm, hoặc dùng "fault": "CONNECTION_RESET_BY_PEER" để mô phỏng đứt kết nối.
Bước 6 — Trỏ ứng dụng đang test sang virtual service. Trong môi trường test, đặt biến cấu hình PAYMENT_BASE_URL=http://localhost:8089. Đây là lý do bạn nên luôn để base URL của dependency là biến môi trường, không hardcode.
Bước 7 — Chạy test và verify. Viết test tích hợp gọi qua app của bạn, rồi kiểm tra WireMock có nhận đúng request không (verify): số lần gọi, đúng path, đúng body. Điều này đảm bảo code của bạn thực sự gọi dependency đúng cách.
Bước 8 — Đưa vào CI. Đóng gói virtual service bằng Docker để CI khởi động được nhất quán. Với stack có sẵn container, đây là bước tự nhiên; virtual service trở thành một service phụ trong docker-compose của test.
Bước 9 — Duy trì đồng bộ contract. Định kỳ record lại từ sandbox thật hoặc kiểm tra OpenAPI mới nhất, để stub không "trôi" khỏi thực tế. Đây là mắt xích dễ bị bỏ quên nhất.
Lỗi thường gặp & mẹo
Lỗi 1 — Mock "trôi" khỏi thực tế (stale stub). Đối tác đổi tên field từ txnId thành transactionId, nhưng stub của bạn vẫn dùng tên cũ. Test vẫn xanh mượt, cho tới khi lên production thì vỡ. Mẹo: giữ một lớp mỏng contract test/smoke test chạy với dịch vụ thật để phát hiện lệch; và sinh stub từ OpenAPI spec chung thay vì gõ tay.
Lỗi 2 — Ảo hóa quá đà, mất niềm tin vào test. Nếu mock cả những thứ đáng lẽ nên test thật (như logic tính toán cốt lõi của chính bạn), test xanh nhưng vô nghĩa. Mẹo: chỉ ảo hóa dependency bên ngoài tầm kiểm soát của bạn, không ảo hóa phần hệ thống mình cần thực sự kiểm chứng.
Lỗi 3 — Mock quá "hiền". Stub luôn trả 200, response luôn đẹp. Hệ thống trông rất khỏe cho tới khi gặp lỗi thật. Mẹo: chủ động tạo stub xấu — trả 500, JSON thiếu field, delay lâu, body rỗng — để ép code xử lý lỗi tử tế.
Lỗi 4 — Không verify lời gọi. Bạn stub response nhưng không kiểm tra app có gọi đúng không. Có khi app không gọi dependency mà test vẫn xanh vì... chẳng ai kiểm tra. Mẹo: luôn verify số lần gọi và nội dung request quan trọng.
Lỗi 5 — Dữ liệu nhạy cảm lọt vào stub. Khi record từ sandbox thật, response có thể chứa token, số thẻ, thông tin cá nhân. Mẹo: scrub (che) dữ liệu nhạy cảm trước khi commit stub vào repo.
Mẹo vàng: đặt virtual service ở một cổng/URL cố định qua biến môi trường, và tách rõ hai chế độ chạy — "test với mock" (nhanh, mọi commit) và "test với thật" (chậm, chạy định kỳ). Đừng trộn lẫn hai chế độ trong cùng một lần chạy.
Bài tập thực hành
- Dựng virtual service đầu tiên. Cài WireMock hoặc Mockoon. Tạo stub cho
GET /v1/status/{id}trả về{ "status": "PENDING" }. Gọi thử bằngcurlvà xác nhận nhận đúng response.
- Response theo điều kiện. Thêm stub sao cho
GET /v1/status/999trả vềSUCCESS, còn các id khác trảPENDING. Kiểm chứng bằng hai lời gọi khác nhau.
- Fault injection. Cấu hình một endpoint delay 6 giây và một endpoint trả HTTP 503. Viết một đoạn code nhỏ gọi hai endpoint đó và xử lý timeout/lỗi cho tử tế (hiển thị thông báo thay vì crash).
- Từ OpenAPI tới mock. Lấy một file OpenAPI bất kỳ (hoặc tự viết một spec nhỏ 2 endpoint), dùng Prism sinh mock server tự động. So sánh trải nghiệm với việc gõ stub tay.
- Tình huống tổng hợp. Mô phỏng luồng thanh toán MoMo giả định:
POST /paytrả vềpayUrlvàtransactionId; sau đóGET /pay/{transactionId}lần đầu trảPENDING, lần thứ hai trảSUCCESS(stateful scenario). Verify rằng ứng dụng của bạn poll đúng số lần.
Tóm tắt
Service virtualization và mocking là công cụ để bạn giành lại quyền kiểm soát đối với những dependency mà bình thường bạn không điều khiển được. Bạn dùng chúng khi dependency chưa sẵn sàng, khi gọi thật tốn tiền và chậm, khi cần tái hiện các kịch bản lỗi hiếm, và khi muốn cách ly để test đúng phần code của mình.
Ba nguyên tắc cần khắc cốt: (1) ảo hóa cái ngoài tầm kiểm soát, không ảo hóa cái mình cần kiểm chứng; (2) luôn giữ một lớp mỏng test với dịch vụ thật để phát hiện contract bị lệch; (3) tận dụng fault injection để test những góc tối mà môi trường thật không bao giờ tự tạo ra. Làm đúng, bạn sẽ có một test suite chạy nhanh, ổn định, rẻ, và bắt được cả những bug mà người khác không bao giờ chạm tới. Đó chính là dấu ấn của một QA engineer trưởng thành.