Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là QA của một ứng dụng gọi xe. Bạn cần kiểm tra xem màn hình "Đặt xe" hiển thị đúng thông báo lỗi khi server trả về mã 500 (Internal Server Error). Vấn đề là: backend hầu như không bao giờ trả về 500 khi bạn muốn. Bạn không thể yêu cầu đội backend "làm hỏng server giùm tôi 5 phút" mỗi lần cần test. Bạn cũng không thể ngồi đợi cả ngày để đúng lúc mạng chập chờn xảy ra một cách tự nhiên.
Đây chính là lý do Network Mocking và API Stub trở thành một trong những kỹ năng tách biệt giữa một mobile tester "chạm được vào nút bấm" và một QA automation engineer thực thụ. Khi bạn kiểm soát được tầng mạng — phần dữ liệu đi vào và ra khỏi app — bạn có thể tái tạo bất kỳ kịch bản nào một cách chủ động, lặp lại được, và nhanh gấp nhiều lần so với việc phụ thuộc backend thật.
Ở Bài 17 chúng ta đã bàn về việc mô phỏng điều kiện mạng (3G, mất kết nối) ở tầng hệ điều hành, và Bài 18 nói về offline mode. Bài 40 này đi sâu vào một tầng khác: can thiệp trực tiếp vào nội dung của response HTTP để ép app nhận đúng dữ liệu bạn muốn nó nhận. Đây là kỹ thuật nền tảng cho hầu hết automation test ổn định trên mobile.
Khái niệm cốt lõi
Mock, Stub và Fake — phân biệt cho rõ
Ba từ này hay bị dùng lẫn lộn, nhưng trong ngữ cảnh network testing bạn nên hiểu như sau:
- Stub: một response cứng (hard-coded) trả về sẵn cho một request cụ thể. Ví dụ: mọi lời gọi
GET /user/profileđều trả về đúng một JSON{"name": "Nguyễn Văn A", "vip": true}. Stub không quan tâm request đến bao nhiêu lần, luôn trả cùng một thứ. - Mock: cũng trả về response giả, nhưng thường có thêm khả năng xác minh — kiểm tra xem request có được gọi đúng số lần, đúng tham số, đúng header hay không. Mock mang tính "kỳ vọng hành vi".
- Fake: một triển khai đơn giản hóa nhưng vẫn có logic thật, ví dụ một server giả lưu dữ liệu trong bộ nhớ và trả về đúng thứ bạn vừa POST lên.
Ba lý do cốt lõi để mock
Ghi nhớ ba từ khóa: Speed, Deterministic, Edge case.
Speed (tốc độ): Test không còn phụ thuộc vào backend chậm hay môi trường staging đang bảo trì. Một suite Appium 200 test case gọi API thật có thể mất 40 phút chỉ vì độ trễ mạng; cùng suite đó với response mock có thể xong trong 12 phút vì mỗi response trả về gần như tức thì.
Deterministic (tính xác định): Cùng một input luôn cho cùng một output. Backend thật hôm nay trả 10 sản phẩm, ngày mai đội content xóa mất 2 sản phẩm, thế là assertion "hiển thị đúng 10 item" của bạn đỏ lè dù app không hề có lỗi. Test flaky (chập chờn) vì dữ liệu thay đổi là cơn ác mộng kinh điển. Mock loại bỏ hoàn toàn biến số này.
Edge case (trường hợp biên): Đây là giá trị lớn nhất. Bạn có thể ép server trả về HTTP 500, ép timeout, trả về JSON méo mó (malformed), trả về danh sách rỗng, trả về 10.000 phần tử để test hiệu năng cuộn, hoặc trả về ký tự lạ để test hiển thị. Những kịch bản này gần như không thể tạo ra ổn định với backend thật.
Các tầng có thể mock trên mobile
Bạn có thể can thiệp ở nhiều tầng khác nhau, và mỗi tầng có ưu nhược điểm:
- Proxy trung gian (man-in-the-middle): Đặt một proxy giữa app và server thật (Charles Proxy, Proxyman, mitmproxy). App vẫn gọi ra ngoài như bình thường, nhưng proxy chặn và thay thế response. Ưu điểm: không cần đổi code app, dùng được cả app đã build sẵn. Nhược điểm: phải cấu hình chứng chỉ SSL, khó tự động hóa hoàn toàn.
- Mock server cục bộ: Chạy một HTTP server giả (WireMock, MockServer, json-server, hoặc mock server tích hợp) và trỏ app tới địa chỉ đó. Ưu điểm: kiểm soát toàn diện, dễ đưa vào CI. Nhược điểm: cần cơ chế để app biết trỏ về đâu (thường qua biến môi trường hoặc build variant).
- Interceptor trong app: Nếu team có quyền sửa code, chèn một interceptor ở tầng networking (ví dụ
OkHttp Interceptortrên Android, hoặcURLProtocolcustom trên iOS) để trả response giả khi ở chế độ test. Nhanh và ổn định nhất, nhưng đòi hỏi hợp tác với dev.
Tình huống thực tế
Tình huống 1 — Ví MoMo và bài toán server 500
Một đội QA làm việc cho một ví điện tử tại TP.HCM (gọi tên giả định là "PayGo") gặp vấn đề: khi cổng thanh toán trả về lỗi, màn hình xác nhận giao dịch bị treo màn hình trắng thay vì hiển thị thông báo "Giao dịch thất bại, vui lòng thử lại". Bug này chỉ xảy ra khi backend gặp sự cố — điều mà đội test không thể chủ động tạo ra trên môi trường staging.
Giải pháp: QA dùng Proxyman đặt một breakpoint và một map local rule cho endpoint POST /transactions/confirm, ép nó trả về HTTP 500 với body {"error": "internal_error"}. Ngay lập tức họ tái hiện được màn hình trắng. Không dừng ở đó, họ tạo thêm ba biến thể: trả 503 (service unavailable), trả 200 nhưng body rỗng, và trả 200 với JSON thiếu trường transaction_id. Kết quả: phát hiện tổng cộng 4 crash khác nhau, tất cả cùng gốc rễ là app không kiểm tra null trước khi parse response.
Bài học: Một mã lỗi bạn tưởng là "hiếm gặp" thực ra lại là nơi bug tập trung nhiều nhất, vì dev thường chỉ code cho luồng thành công (happy path). Mocking cho phép bạn "gõ cửa" đúng những góc tối đó.
Tình huống 2 — Ứng dụng thương mại điện tử và độ trễ mạng
Một startup thương mại điện tử ở Hà Nội (giả định "ChợViệt") có một suite automation Appium chạy trên CI mỗi lần merge code. Suite này gọi thẳng API staging. Vấn đề: tỷ lệ test đỏ giả (false failure) lên tới khoảng 15% — cứ 20 lần chạy thì 3 lần có test đỏ không phải do lỗi app, mà do staging chậm hoặc dữ liệu sản phẩm bị đội khác thay đổi giữa chừng.
QA lead quyết định dựng một WireMock server chứa các response cố định cho toàn bộ luồng "duyệt sản phẩm → thêm giỏ hàng → thanh toán". Mỗi response được lưu thành file JSON trong repo, review qua Git như code. Sau khi chuyển sang mock, tỷ lệ false failure giảm từ 15% xuống dưới 2%, và thời gian chạy suite giảm từ 28 phút còn 9 phút.
Điểm tinh tế: họ không mock 100%. Họ giữ lại một nhóm nhỏ "smoke test" vẫn gọi API thật để đảm bảo hợp đồng (contract) giữa app và backend không bị lệch âm thầm. Đây là điểm nối với Bài 53 về contract testing.
Bài học: Mock để chạy nhanh và ổn định, nhưng luôn giữ một lớp mỏng test thật để bắt lỗi tích hợp. Mock quá đà mà quên đối chiếu thực tế là cách nhanh nhất để "test xanh nhưng app hỏng".
Tình huống 3 — Test danh sách rỗng và danh sách khổng lồ
Một app tin tức phục vụ người dùng Đông Nam Á cần kiểm tra màn hình "Tin đã lưu". Với người dùng mới, danh sách này rỗng — cần hiển thị hình minh họa "Chưa có tin nào". Với người dùng lâu năm, danh sách có thể lên tới hàng nghìn bài. QA không thể tạo tài khoản thật có đúng 0 bài rồi lại có đúng 2.000 bài mỗi lần test.
Bằng cách stub endpoint GET /saved-articles, họ tạo hai response: một mảng rỗng [] và một mảng 2.000 phần tử sinh tự động. Kết quả test empty state hiển thị đúng; nhưng response 2.000 phần tử làm lộ ra lỗi giật khung hình khi cuộn (liên quan Bài 14 về FPS) do app render toàn bộ danh sách thay vì dùng lazy loading.
Bài học: Mock không chỉ để test lỗi mạng — nó là công cụ tạo dữ liệu biên (empty, một phần tử, rất nhiều phần tử) mà backend thật khó cung cấp theo yêu cầu.
Hướng dẫn từng bước
Dưới đây là quy trình dựng network mock cho một luồng đăng nhập, dùng cách tiếp cận proxy vì nó không cần sửa code app.
Bước 1 — Xác định các endpoint cần kiểm soát. Mở app, bật Charles Proxy hoặc Proxyman, thực hiện luồng cần test một lần với backend thật và ghi lại toàn bộ request. Ví dụ luồng login gồm: POST /auth/login, GET /user/profile, GET /feature-flags. Ghi rõ URL, method, header quan trọng và cấu trúc body response.
Bước 2 — Cấu hình proxy trên thiết bị. Trên điện thoại, vào cài đặt Wi-Fi, đặt proxy thủ công trỏ về IP máy tính đang chạy Charles/Proxyman và đúng cổng (thường 8888). Cài chứng chỉ SSL của proxy vào thiết bị và bật "trust" để đọc được HTTPS. Đây là bước hay vướng nhất — nếu bỏ qua, bạn chỉ thấy request mã hóa không đọc được.
Bước 3 — Tạo response giả. Với endpoint POST /auth/login, tạo một "Map Local" hoặc "Local Response" trỏ tới một file JSON bạn soạn sẵn, ví dụ:
{
"token": "fake-jwt-token-for-testing",
"user_id": 12345,
"vip": true
}
Bước 4 — Kiểm tra kịch bản happy path. Chạy lại luồng login với response mock. Xác nhận app nhận token giả và vào được màn hình chính. Đây là bước sanity check để chắc chắn cơ chế mock hoạt động trước khi nghịch các case lỗi.
Bước 5 — Tạo các biến thể edge case. Nhân bản rule vừa tạo và sửa thành các kịch bản: trả HTTP 401 (sai mật khẩu), trả 500 (lỗi server), trả 200 nhưng thiếu trường token, hoặc bật tính năng "throttle" của proxy để mô phỏng response mất 30 giây rồi mới về (test timeout). Với mỗi biến thể, ghi lại app phản ứng thế nào và có khớp thiết kế không.
Bước 6 — Tự động hóa (nếu dùng mock server). Khi đã hiểu rõ các case, chuyển các response JSON đó vào WireMock hoặc mock server tích hợp trong framework test, lưu vào repo, và cho automation trỏ app về mock server qua build variant. Từ đây mỗi test case tự chọn kịch bản response mình cần trước khi chạy.
Bước 7 — Verify request (khi cần mock thực sự). Với những luồng quan trọng như thanh toán, đừng chỉ giả response — hãy verify rằng app đã gửi đúng body và header lên. Ví dụ kiểm tra POST /transactions có gửi đúng amount và idempotency-key. Đây là ranh giới biến một stub thành một mock có giá trị bảo vệ.
Lỗi thường gặp & mẹo
Quên chứng chỉ SSL. Đây là lỗi số một với người mới. Không cài và trust chứng chỉ proxy thì mọi request HTTPS đều mã hóa, bạn không map local được. Ngoài ra, một số app bật SSL pinning khiến proxy bị chặn hoàn toàn — khi đó cần bản build đặc biệt tắt pinning cho môi trường test, phải phối hợp với dev.
Mock quá nhiều đến mức test vô nghĩa. Nếu bạn mock luôn cả logic nghiệp vụ, test của bạn chỉ đang kiểm tra... chính response bạn tự viết. Hãy mock ở ranh giới mạng, còn logic xử lý bên trong app thì để app chạy thật.
Response mock lỗi thời. Backend đổi cấu trúc JSON nhưng file mock của bạn vẫn là bản cũ. Test xanh nhưng thực tế app đã hỏng với backend thật. Mẹo: định kỳ chạy lại nhóm smoke test gọi API thật, và cân nhắc contract testing (Bài 53) để tự động phát hiện lệch schema.
Bỏ quên header và status code. Nhiều bạn chỉ chăm chăm vào body JSON mà quên Content-Type, Cache-Control, hay đúng mã HTTP. App có thể xử lý khác nhau giữa 200 và 204, hoặc từ chối parse nếu Content-Type sai. Mock phải giống response thật cả phần "vỏ".
Không test độ trễ. Response mock thường trả về tức thì, che giấu các bug liên quan loading spinner, race condition, hay double-tap gửi request hai lần. Hãy chủ động thêm delay vào một số mock để test đúng hành vi khi mạng chậm.
Mẹo tổ chức: Đặt tên file response theo kịch bản rõ ràng, ví dụ login_success.json, login_401.json, login_500.json, profile_empty.json. Khi debug một test đỏ, bạn nhìn tên file là biết ngay đang ở kịch bản nào.
Bài tập thực hành
- Dựng proxy và bắt request: Cài Charles Proxy hoặc Proxyman, cấu hình trên một thiết bị/emulator, và bắt được toàn bộ request HTTPS của một luồng đăng nhập trong một app bất kỳ bạn có quyền test. Chụp lại danh sách các endpoint.
- Stub happy path: Tạo một map local response cho endpoint profile, đổi tên hiển thị của người dùng thành một chuỗi bất kỳ, và xác nhận app hiển thị đúng tên giả đó.
- Ba edge case: Với endpoint login, tạo ba response mock: 401, 500, và một response 200 thiếu trường quan trọng. Ghi lại app phản ứng thế nào với mỗi case và đánh dấu case nào gây crash hoặc hiển thị sai.
- Test độ trễ: Bật tính năng throttle của proxy, ép một endpoint mất 20 giây mới trả về. Quan sát loading spinner và kiểm tra xem app có xử lý timeout đúng không, có cho phép double-tap gửi request hai lần không.
- Nâng cao — mock server: Dựng một WireMock hoặc json-server chạy cục bộ với 3 endpoint, trỏ app về đó, và viết một Appium test đơn giản chạy qua luồng dùng response từ mock server thay vì backend thật.
Tóm tắt
Network mocking và API stub là kỹ năng biến QA từ vị thế bị động — chờ backend "chịu" trả lỗi — sang chủ động kiểm soát toàn bộ dữ liệu đi vào app. Ba lý do cốt lõi để mock là Speed (test nhanh, không phụ thuộc backend chậm), Deterministic (cùng input luôn ra cùng output, diệt test flaky), và Edge case (ép server trả 500, timeout, JSON méo, danh sách rỗng hay khổng lồ mà backend thật không cho được).
Bạn có thể can thiệp ở tầng proxy (Charles, Proxyman — không cần sửa code), tầng mock server (WireMock, json-server — dễ đưa vào CI), hoặc interceptor trong app (nhanh nhất nhưng cần dev hỗ trợ). Hãy phân biệt stub (response cứng), mock (có verify hành vi), và fake (có logic đơn giản).
Cuối cùng, ghi nhớ nguyên tắc vàng: mock để chạy nhanh và ổn định, nhưng luôn giữ một lớp mỏng test thật để đảm bảo hợp đồng giữa app và backend không lệch âm thầm. Mock đúng chỗ, mock đúng ranh giới mạng, và bạn sẽ có một bộ test vừa nhanh, vừa tin cậy, vừa bắt được những bug mà người khác không bao giờ chạm tới.