Mở đầu — vì sao bài này quan trọng
Nếu bạn làm QA cho một ứng dụng thương mại điện tử, gọi xe, giao đồ ăn hay bất kỳ sản phẩm số nào tại Việt Nam, sớm muộn gì bạn cũng sẽ chạm vào bài toán "test thanh toán qua ví điện tử". Ở bài trước, chúng ta đã nói về In-App Purchase (IAP) — kênh thanh toán "chính chủ" của Apple và Google. Nhưng thực tế tại thị trường Việt Nam, phần lớn giao dịch mua hàng thật (hàng hóa vật lý, dịch vụ, nạp tiền) lại không đi qua IAP mà đi qua các cổng thanh toán nội địa: MoMo, ZaloPay, VNPAY, cùng thẻ ATM nội địa qua NAPAS.
Đây là mảng test mà rất nhiều QA "sợ" vì ba lý do. Thứ nhất, tiền là thật — một bug ở luồng thanh toán không phải là "nút bị lệch", mà là khách hàng bị trừ tiền hai lần, hoặc mua hàng thành công mà không được ghi nhận. Thứ hai, luồng thanh toán là luồng bất đồng bộ (asynchronous): app của bạn không trực tiếp biết kết quả, mà phải chờ callback từ phía ví hoặc kiểm tra lại qua server. Thứ ba, bạn không thể test bằng tiền thật mỗi lần, nên bắt buộc phải hiểu và làm chủ môi trường sandbox mà các nhà cung cấp cấp cho developer.
Bài học này sẽ trang bị cho bạn tư duy và kỹ thuật cụ thể để test thanh toán qua MoMo và ZaloPay sandbox trên mobile: hiểu kiến trúc luồng tiền, biết cách lấy tài khoản test, thiết kế test case cho các nhánh success/fail/timeout, và tránh những cái bẫy khiến tiền thật "bốc hơi" trên production.
Khái niệm cốt lõi
Landscape các cổng thanh toán tại Việt Nam
Trước khi test, bạn cần bức tranh tổng thể về các nhà cung cấp và mức độ hỗ trợ sandbox của họ:
| Nhà cung cấp | Có Sandbox? | Cổng đăng ký | Ghi chú cho QA |
|---|---|---|---|
| MoMo | Có | partner.momo.vn (MoMo Developers) | Cấp partnerCode/accessKey/secretKey test; có app MoMo Test hoặc số điện thoại test |
| ZaloPay | Có | ZaloPay for Business (sbgateway/openapi sandbox) | Cấp app_id/key1/key2 sandbox; có ứng dụng ZaloPay sandbox riêng |
| VNPAY | Có | sandbox.vnpayment.vn | Cấp mã website (vnp_TmnCode) + secret; test bằng thẻ ATM giả lập |
| ZaloPay QR / VietQR | Một phần | Tùy đối tác | Thường test qua chuyển khoản sandbox |
| Thẻ quốc tế (Visa/Master) | Qua cổng (Stripe, Onepay) | Tùy cổng | Có bộ thẻ test riêng của từng cổng |
Ba mô hình tích hợp thanh toán trên mobile
Điều quan trọng nhất một QA phải nắm là app của bạn tích hợp theo mô hình nào, vì test case sẽ khác nhau hoàn toàn:
- App-to-App (deep link / universal link): App của bạn mở thẳng app MoMo/ZaloPay đã cài trên máy để người dùng xác nhận, rồi ví "đá" ngược trở lại app của bạn qua deep link kèm kết quả. Đây là trải nghiệm mượt nhất và phổ biến nhất trên mobile Việt Nam.
- QR Code: App hiển thị mã QR, người dùng dùng app ví khác quét để trả. Thường dùng khi thanh toán trên web hoặc trên thiết bị khác.
- Webview / Gateway redirect: App nhúng một trang web của cổng thanh toán bên trong WebView để nhập thông tin. Cách này liên quan chặt tới Bài 35 (Webview & Hybrid App Testing).
Luồng tiền và vì sao "IPN" là trái tim của test
Đây là khái niệm mà QA hay bỏ sót. Khi người dùng thanh toán xong trong app ví, có hai kênh báo kết quả về hệ thống của bạn:
- Redirect / return URL (client-side): Ví đá người dùng quay lại app của bạn kèm tham số kết quả. Kênh này không đáng tin để xác nhận tiền, vì người dùng có thể tắt app, mất mạng, hoặc bị kẻ gian giả mạo tham số.
- IPN — Instant Payment Notification (server-to-server): Ví gọi thẳng vào một endpoint trên server của bạn để báo "giao dịch X đã thành công". Đây mới là nguồn sự thật (source of truth) để ghi nhận đơn hàng.
Chữ ký (signature) và tính toàn vẹn dữ liệu
Cả MoMo lẫn ZaloPay đều ký dữ liệu bằng HMAC (MoMo dùng HMAC-SHA256 với secretKey; ZaloPay dùng key1 để tạo request MAC và key2 để verify callback). QA không cần tự tay tính chữ ký, nhưng phải test được: nếu ai đó sửa số tiền hoặc mã đơn giữa đường, hệ thống có từ chối không? Đây là ranh giới với Bài 41 (Security Testing — OWASP MASVS), ở bài này ta chỉ test ở mức chức năng: sai chữ ký thì phải fail.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT "Chợ Sạch" và lỗi double-charge khi mạng chập chờn
Một startup thương mại điện tử giả định tên Chợ Sạch (bán thực phẩm sạch, ~30.000 đơn/ngày) tích hợp MoMo theo mô hình app-to-app. Trên môi trường sandbox, QA test luồng "happy path" thấy mọi thứ ổn nên cho lên production. Hai tuần sau, bộ phận CSKH nhận hàng loạt khiếu nại: khách bị trừ tiền hai lần cho cùng một đơn.
Điều tra ra: khi khách thanh toán trong 4G yếu, sau khi trả tiền trong app MoMo, deep link callback về Chợ Sạch bị chậm. Khách sốt ruột bấm "Thanh toán lại", app tạo một mã đơn (orderId) mới và mở MoMo lần hai — khách trả thêm lần nữa. Cả hai IPN đều hợp lệ nên server ghi nhận cả hai.
Bài học: QA đã bỏ sót test idempotency (tính bất biến khi lặp) và test trên điều kiện mạng xấu. Test case đúng phải bao gồm: cùng một giỏ hàng, nếu người dùng bấm thanh toán nhiều lần trong lúc giao dịch trước chưa kết thúc, hệ thống phải khóa nút hoặc tái sử dụng đúng orderId đang chờ, và phải chặn ghi nhận trùng. Điểm giao với Bài 17 (Network Conditions) rất rõ: bug này chỉ lộ ra khi mô phỏng mạng chậm.
Tình huống 2 — App gọi xe "Vivu" và cái bẫy "chỉ tin redirect"
Vivu là một app gọi xe khu vực Đông Nam Á (giả định) tích hợp ZaloPay để nạp ví. QA test trên sandbox, thấy sau khi thanh toán ZaloPay đá về app kèm status=1 là app cộng tiền vào ví ngay lập tức — test pass, release.
Trên production, một người dùng am hiểu kỹ thuật phát hiện: nếu chặn IPN không cho tới server (bằng cách bật rồi tắt mạng đúng thời điểm) nhưng vẫn để redirect về với status=1, app vẫn cộng tiền vào ví dù thực tế ZaloPay chưa xác nhận hoàn tất. Kẻ gian khai thác để nạp ví "chùa".
Bài học: không bao giờ ghi nhận tiền dựa trên redirect client-side. QA phải thiết kế một test case ác ý: cho redirect trả kết quả thành công nhưng chặn IPN, kết quả mong đợi là ví KHÔNG được cộng tiền, trạng thái phải là "đang xác nhận". Trong sandbox, bạn có thể mô phỏng bằng cách tạm dừng server IPN hoặc dùng công cụ proxy (Bài 40 — Network Mocking) để nuốt request IPN.
Tình huống 3 — Fintech "Sen Pay" và lỗi làm tròn tiền tệ VND
Một ví fintech giả định Sen Pay cho phép chia hóa đơn nhóm. Khi ba người chia hóa đơn 100.000đ, hệ thống chia 33.333,33đ mỗi người. Do VND không có đơn vị lẻ, mỗi người bị làm tròn xuống 33.333đ, tổng thu chỉ 99.999đ — lệch 1đ so với số tiền gửi MoMo. Callback từ MoMo báo amount = 100.000 nhưng bản ghi nội bộ cộng lại là 99.999, hệ thống reconcile báo "mismatch" và tự động đánh dấu giao dịch nghi vấn, giữ tiền của khách.
Bài học: với VND, amount luôn là số nguyên, không có phần thập phân, và mọi phép chia phải có logic xử lý phần dư rõ ràng. QA test thanh toán VN nhất định phải có test case về làm tròn, số tiền tối thiểu (MoMo/ZaloPay thường yêu cầu tối thiểu 1.000đ hoặc 10.000đ tùy sản phẩm) và số tiền lớn (chạm hạn mức giao dịch của ví).
Hướng dẫn từng bước
Dưới đây là quy trình test thanh toán MoMo/ZaloPay sandbox cho một app mobile từ đầu tới cuối.
Bước 1 — Lấy thông tin và tài khoản sandbox. Với MoMo, đăng ký tại partner.momo.vn để được cấp bộ partnerCode, accessKey, secretKey môi trường test, cùng số điện thoại/app test. Với ZaloPay, vào ZaloPay for Business để lấy app_id, key1, key2 sandbox và cài app ZaloPay sandbox. Xác nhận với dev rằng build bạn đang test đang trỏ endpoint tới môi trường sandbox (ví dụ test-payment.momo.vn), không phải production.
Bước 2 — Dựng ma trận test case theo nhánh kết quả. Đừng chỉ test "happy path". Ít nhất phải phủ:
- Thanh toán thành công → redirect + IPN đều đúng → đơn chuyển "đã thanh toán".
- Người dùng hủy giữa chừng trong app ví → đơn về "đã hủy", không trừ tiền.
- Thất bại (số dư không đủ trong tài khoản test, sai OTP) → đơn "thất bại".
- Timeout → người dùng không thao tác gì đến khi hết hạn → đơn "hết hạn".
- Redirect thành công nhưng IPN chậm/thiếu → trạng thái trung gian "đang xử lý", không mở khóa hàng hóa.
- Trùng lặp / bấm nhiều lần → chỉ ghi nhận một lần (idempotency).
Bước 4 — Xác minh bằng cả hai kênh. Với mỗi giao dịch, kiểm tra chéo: (a) UI app hiển thị gì, (b) log/DB server ghi trạng thái gì, (c) IPN thực sự đã tới và signature verify pass chưa. Chỉ khi cả ba khớp mới coi là pass.
Bước 5 — Mô phỏng điều kiện xấu. Dùng Network Link Conditioner (iOS) hoặc chế độ throttle của Charles/Proxyman để tạo mạng chậm; tắt mạng đúng lúc callback đang chạy; chuyển app sang background. Đây là nơi phần lớn bug thật ẩn náu.
Bước 6 — Đối soát (reconciliation). Với các đơn nghi ngờ, hầu hết cổng có API truy vấn trạng thái giao dịch (query transaction). Test rằng khi IPN thất lạc, cơ chế truy vấn lại (polling/manual query) của hệ thống có tự chữa được trạng thái đơn về đúng hay không.
Bước 7 — Ghi log bằng chứng. Với thanh toán, mọi bug phải kèm orderId/transId, thời điểm, môi trường (sandbox), và log của cả ba kênh. Đây là chất liệu cho Bài 57 (Bug Reporting cho Mobile QA).
Lỗi thường gặp & mẹo
- Chỉ test happy path. Đây là lỗi phổ biến nhất và nguy hiểm nhất. Luồng tiền có nhiều nhánh fail hơn nhánh success; nhánh fail mới là nơi khách mất tiền.
- Tin vào redirect thay vì IPN. Như tình huống Vivu — luôn coi IPN đã verify signature là nguồn sự thật duy nhất.
- Quên test idempotency. Bấm hai lần, mạng lặp request, retry của cổng — tất cả có thể tạo bản ghi trùng nếu hệ thống không chặn theo orderId.
- Nhầm đơn vị tiền. VND không có phần lẻ; một số tích hợp quốc tế lại tính theo cent. Kiểm tra kỹ amount gửi đi có đúng đơn vị không (100.000đ chứ không phải 100).
- Lẫn lộn sandbox và production. Luôn xác nhận endpoint và key đang là sandbox. Mẹo: yêu cầu dev hiển thị một banner "SANDBOX" rõ ràng trên build test để tránh vô tình test trên tiền thật.
- Bỏ qua trạng thái đơn "treo". Test cụ thể kịch bản IPN tới trước redirect, hoặc redirect tới mà IPN chưa tới — thứ tự đến của hai kênh là bất định.
- Mẹo bảng đối chiếu trạng thái: lập một bảng ánh xạ resultCode/return_code của MoMo và ZaloPay sang trạng thái đơn nội bộ, rồi test từng mã. MoMo dùng
resultCode(0 = thành công), ZaloPay dùngreturn_code(1 = thành công). Biết rõ bảng mã giúp bạn không đoán mò khi đọc log. - Mẹo an toàn: đề nghị dev bật cơ chế giới hạn số tiền tối đa trên môi trường sandbox và chặn các đầu số thật, để mọi nhầm lẫn cấu hình không biến thành giao dịch thật.
Bài tập thực hành
- Xây ma trận test case: Cho một app đặt vé xem phim tích hợp MoMo app-to-app, hãy viết đầy đủ test case cho 6 nhánh: thành công, hủy, thất bại (thiếu số dư), timeout, redirect-thành-công-nhưng-IPN-thiếu, và bấm-thanh-toán-hai-lần. Với mỗi case, ghi rõ kết quả mong đợi ở ba kênh (UI / DB server / IPN).
- Tái hiện bug double-charge: Trên môi trường sandbox, dùng công cụ throttle mạng để làm chậm callback, rồi thử bấm thanh toán lần hai. Ghi lại orderId của cả hai lần, kiểm tra DB xem có bao nhiêu bản ghi "đã thanh toán". Viết bug report hoàn chỉnh nếu phát hiện trùng.
- Kiểm chứng nguồn sự thật: Dùng Proxyman/Charles để "nuốt" request IPN (chặn không cho tới server) trong khi vẫn cho redirect về app với kết quả thành công. Xác nhận app KHÔNG mở khóa hàng hóa. Ghi lại hành vi thực tế và so với hành vi mong đợi.
- Đối chiếu bảng mã: Lập bảng ánh xạ ít nhất 4 mã kết quả của MoMo và 4 mã của ZaloPay sang trạng thái đơn nội bộ của app bạn đang test, dựa trên tài liệu sandbox chính thức của hai nhà cung cấp.
Tóm tắt
Test thanh toán qua MoMo và ZaloPay sandbox không khó về mặt thao tác, nhưng đòi hỏi bạn thay đổi tư duy: đây là luồng bất đồng bộ, liên quan tới tiền thật, và nguồn sự thật nằm ở IPN server-to-server đã verify chữ ký, chứ không phải redirect client-side. Ba trụ cột bạn phải làm chủ là: (1) hiểu mô hình tích hợp (app-to-app / QR / webview) để chọn đúng cách test; (2) phủ đủ các nhánh kết quả — thành công, hủy, thất bại, timeout, IPN thiếu, và trùng lặp; (3) mô phỏng điều kiện xấu (mạng chậm, app bị kill, IPN thất lạc) vì đó là nơi bug thật ẩn náu.
Ba tình huống Chợ Sạch, Vivu và Sen Pay cho thấy ba cái bẫy kinh điển: double-charge do thiếu idempotency, nạp tiền "chùa" do tin redirect, và mismatch do làm tròn VND. Nếu bạn ghi nhớ nguyên tắc "chỉ tin IPN đã verify" và luôn kiểm tra chéo ba kênh UI–DB–IPN cho mỗi giao dịch, bạn đã đứng vững ở phần khó nhất của mobile QA — nơi một dòng log sai có thể tốn tiền thật của người dùng.