Mở đầu — vì sao bài này quan trọng
Nếu bạn từng làm QA cho một sản phẩm có thu tiền — thương mại điện tử, ví điện tử, đặt vé, khóa học online — thì phần "thanh toán" luôn là nơi áp lực nhất. Một lỗi ở trang sản phẩm khiến khách khó chịu; một lỗi ở bước thanh toán khiến bạn mất tiền thật và mất niềm tin của khách hàng. Vậy nên performance testing cho payment gateway không phải là "test cho có" — nó là bảo hiểm cho dòng doanh thu của công ty.
Nhưng đây cũng là bài học mà rất nhiều bạn QA mới làm sai ngay từ đầu. Họ áp dụng đúng tư duy đã học ở các bài trước — "cứ đẩy tải thật cao để tìm điểm nghẽn" — rồi bắn 1000 request/giây vào sandbox của VNPay. Kết quả: bị rate-limit, bị block IP, số liệu vô nghĩa, và tệ hơn là bị nhà cung cấp cổng thanh toán liên hệ cảnh cáo vì làm quá tải môi trường dùng chung của họ.
Bài học cốt lõi ở đây khác hẳn các loại test trước: với payment gateway ở Việt Nam (VNPay, MoMo, ZaloPay), bạn không phải là người kiểm soát toàn bộ hệ thống. Bạn chỉ sở hữu phần của mình — website, backend, database — còn cổng thanh toán là bên thứ ba có giới hạn TPS (transactions per second) cứng. Nhiệm vụ của performance test ở đây không phải "làm sập cổng thanh toán" mà là: kiểm tra hệ thống của bạn có xử lý đúng và ổn định trong giới hạn thực tế mà cổng cho phép hay không, và xử lý ra sao khi cổng trả về lỗi/timeout. Đây là một tư duy rất khác, và nắm được nó là bạn đã hơn phần lớn tester tự học.
Khái niệm cốt lõi
Bạn đang test cái gì — và không test cái gì
Một luồng thanh toán online điển hình đi qua nhiều chặng:
- Người dùng bấm "Thanh toán" trên website của bạn.
- Backend của bạn tạo đơn hàng, ký chữ ký (signature/HMAC), gọi API của cổng để tạo giao dịch.
- Cổng thanh toán (VNPay/MoMo/ZaloPay) trả về URL/QR để người dùng thanh toán.
- Người dùng thao tác trên trang của cổng.
- Cổng gọi ngược về IPN/webhook của bạn để báo kết quả.
- Backend của bạn xác thực callback, cập nhật đơn hàng, cộng tiền, gửi thông báo.
Vì sao có giới hạn TPS — và con số cụ thể
Sandbox (môi trường thử nghiệm) của các cổng thanh toán là tài nguyên dùng chung cho tất cả merchant đang tích hợp. Nếu mỗi công ty đều bắn tải tối đa, sandbox sẽ sập và không ai làm việc được. Vì vậy nhà cung cấp áp giới hạn TPS. Theo tài liệu tích hợp phổ biến (con số có thể thay đổi theo hợp đồng, luôn hỏi lại contact tích hợp của bạn):
- VNPay sandbox: ~50 TPS.
- ZaloPay sandbox: ~30 TPS.
- MoMo sandbox: ~20 TPS.
Ba chiến lược để test thật mà vẫn tôn trọng giới hạn
Vậy làm sao "test tải" khi bị giới hạn thấp như vậy? Có ba con đường, và một QA giỏi biết chọn đúng con đường cho từng mục tiêu:
1. Test đúng phần của bạn với mock cổng thanh toán. Đây là cách chính để tìm điểm nghẽn ở tốc độ cao. Bạn thay cổng thật bằng một mock server trả về response giống hệt cổng thật (cùng cấu trúc JSON, cùng độ trễ giả lập ~200-500ms). Giờ bạn tự do bắn 500-1000 RPS vào backend của mình để xem code tạo đơn, ký signature, ghi database có chịu tải không — mà không đụng tới sandbox thật.
2. Test tích hợp thật ở đúng giới hạn TPS. Để chắc chắn tích hợp với cổng thật hoạt động ổn định, bạn chạy một bài test giữ đúng dưới trần TPS (ví dụ 45 TPS với VNPay, chừa biên an toàn 10%). Mục tiêu không phải tìm điểm nghẽn mà là xác nhận: ở mức tải hợp lệ tối đa, tỷ lệ lỗi có thấp không, latency có ổn định không, signature có bao giờ sai không.
3. Test khả năng chịu lỗi (resilience). Mô phỏng tình huống cổng chậm, cổng timeout, cổng trả lỗi — để xem hệ thống của bạn có xử lý duyên dáng (graceful) hay sập theo.
Rate limiting trong công cụ test
Điểm kỹ thuật mấu chốt: bạn phải biết cách ghìm số request/giây trong công cụ. Trong k6, dùng executor constant-arrival-rate để cố định số iteration mỗi giây — đây là cách chuẩn để không vượt TPS. Trong JMeter, dùng Constant Throughput Timer hoặc plugin Throughput Shaping Timer để giới hạn số sample/phút. Nếu bạn không ghìm mà chỉ tăng số VU (virtual user), tốc độ request sẽ trôi tự do và vượt trần lúc nào không hay.
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT "ShopViet" và cú suýt bị VNPay khóa sandbox
ShopViet (giả định) chuẩn bị cho đợt khuyến mãi, giao cho bạn QA junior tên Minh nhiệm vụ "load test cổng thanh toán VNPay". Minh dựng script k6 với 300 VU, mỗi VU gọi API tạo giao dịch VNPay liên tục. Trong 2 phút, hệ thống bắn khoảng hơn 400 RPS vào sandbox VNPay.
Kết quả: từ phút thứ nhất, tỷ lệ lỗi vọt lên 85%, phần lớn là HTTP 429 (Too Many Requests) và một số 503. Minh tưởng "cổng VNPay yếu" và định ghi vào báo cáo. May mắn là mentor rà lại và nhận ra: Minh đã vượt trần 50 TPS gấp 8 lần. Con số lỗi đó không phản ánh gì về sản phẩm ShopViet cả — nó chỉ là sandbox tự vệ. Hôm sau, VNPay gửi email nhắc nhở về việc tạo tải bất thường trên môi trường test dùng chung.
Bài học: Trước khi test, phải xác định trần TPS của từng cổng và cấu hình công cụ để không bao giờ vượt qua. 429 từ cổng thứ ba không phải là "bug hiệu năng của bạn" — nó là dấu hiệu bạn đã test sai cách.
Ví dụ 2 — Ví "TeaPay" tách bài test làm hai lớp
Đội QA của TeaPay (giả định, một ứng dụng đặt trà sữa tích hợp cả VNPay, MoMo, ZaloPay) học từ sai lầm phổ biến trên và thiết kế lại. Họ chia thành hai bộ test:
- Bộ A — High-load nội bộ: dựng mock server bằng một service nhỏ trả JSON giống VNPay/MoMo với độ trễ giả lập 300ms ± 100ms. Họ bắn 800 RPS vào backend TeaPay để tìm điểm nghẽn. Kết quả: ở 600 RPS, thời gian tạo đơn tăng vọt vì mỗi lần ký signature họ đọc private key từ ổ đĩa thay vì cache trong bộ nhớ. Họ sửa: cache key khi khởi động, latency P95 giảm từ 1200ms xuống 180ms.
- Bộ B — Real integration: với cổng thật, họ chạy
constant-arrival-rateở đúng 18 TPS cho MoMo (chừa biên dưới trần 20), 27 TPS cho ZaloPay, 45 TPS cho VNPay, mỗi bài 10 phút. Mục tiêu: xác nhận signature luôn đúng và callback IPN được xử lý idempotent (không cộng tiền hai lần khi cổng gửi callback trùng).
Ví dụ 3 — Sự cố callback trùng ở "GiaoNhanh"
GiaoNhanh (giả định) chạy bài real integration ZaloPay ở 25 TPS. Ở tải này, thỉnh thoảng ZaloPay gửi callback IPN hai lần cho cùng một giao dịch (điều hoàn toàn hợp lệ — spec của cổng nói callback có thể lặp, merchant phải tự xử lý idempotent). Vì hàng đợi xử lý callback bị dồn khi tải cao, hai callback của cùng đơn hàng chạy gần như đồng thời, và do thiếu khóa, hệ thống cộng tiền hai lần vào ví người dùng.
Đội QA phát hiện nhờ so khớp: số callback thành công (1.500) không khớp số đơn hàng unique (1.480) — dư 20. Điều này chỉ lộ ra dưới tải, khi các callback chồng nhau về thời gian. Dev sửa bằng khóa theo transaction_id (unique constraint ở database + xử lý idempotent).
Bài học: Phần đáng test nhất của payment không phải chặng gọi cổng, mà là chặng xử lý callback dưới tải. Đó là nơi race condition và lỗi cộng tiền trùng ẩn nấp — và chỉ performance test mới lôi chúng ra ánh sáng.
Hướng dẫn từng bước
Bước 1 — Lấy giới hạn TPS chính thức. Đừng đoán. Hỏi contact tích hợp của VNPay/MoMo/ZaloPay hoặc đọc tài liệu merchant. Ghi rõ trần từng cổng và trừ 10% làm biên an toàn (ví dụ VNPay 50 → test tối đa 45).
Bước 2 — Vẽ sơ đồ luồng và khoanh vùng "phần của bạn". Xác định rõ đâu là API bạn sở hữu (tạo giao dịch, nhận callback IPN, cập nhật đơn) và đâu là bên thứ ba. Bạn chỉ đẩy tải cao vào phần của mình.
Bước 3 — Dựng mock cổng cho bài high-load. Tạo một service nhỏ trả đúng cấu trúc response của cổng thật, có độ trễ giả lập (ví dụ sleep(0.3)). Trỏ backend sang mock qua biến môi trường/config. Đây là môi trường để bạn bắn 500-1000 RPS an toàn.
Bước 4 — Viết script high-load (mock) với k6. Dùng ramping-arrival-rate để tăng dần RPS, quan sát điểm gãy của backend (latency P95, error rate, CPU/DB). Đặt threshold, ví dụ http_req_duration: p(95)<500 và http_req_failed: rate<0.01.
Bước 5 — Viết script real integration với TPS bị ghìm. Dùng constant-arrival-rate cố định đúng dưới trần. Ví dụ với k6:
export const options = {
scenarios: {
vnpay_real: {
executor: 'constant-arrival-rate',
rate: 45, timeUnit: '1s', // 45 TPS, dưới trần 50
duration: '10m',
preAllocatedVUs: 100,
maxVUs: 200,
},
},
thresholds: {
http_req_failed: ['rate<0.005'], // dưới 0.5% lỗi
http_req_duration: ['p(95)<800'],
},
};
Với JMeter, đặt Constant Throughput Timer ở 2700 samples/phút (= 45/giây) và bật ở chế độ "all active threads (shared)".
Bước 6 — Test riêng luồng callback/IPN. Mô phỏng cổng gọi ngược webhook của bạn ở tải tương đương, cố tình gửi callback trùng cho một số giao dịch, rồi kiểm tra hệ thống có xử lý idempotent không (không cộng tiền hai lần).
Bước 7 — Test resilience. Cấu hình mock cổng trả timeout/lỗi 500 cho ~10% request, xác nhận hệ thống của bạn không sập, không treo connection, đơn hàng chuyển đúng trạng thái "pending/failed" chứ không "paid" nhầm.
Bước 8 — Đối soát và báo cáo. So số giao dịch gửi đi với số callback nhận và số đơn cập nhật đúng. Ba con số này phải khớp. Chênh lệch = bug tiền bạc.
Lỗi thường gặp & mẹo
- Bắn tải vượt trần TPS vào cổng thật. Đây là lỗi số một. 429/503 nhận về không phải bug của bạn — bạn đang tự tạo tín hiệu rác và có nguy cơ bị khóa sandbox. Luôn ghìm bằng arrival-rate/throughput timer.
- Dùng tài khoản/thẻ test thật lung tung. Sandbox có bộ thẻ/OTP test riêng. Đừng bao giờ dùng thông tin thẻ thật hay chạy load test trên môi trường production của cổng — bạn có thể tạo giao dịch tiền thật.
- Quên test callback trùng và idempotency. Như ví dụ GiaoNhanh, đây là nơi lỗi cộng tiền hai lần sinh ra. Luôn chủ động gửi callback lặp trong bài test.
- Không mock được nên bỏ luôn high-load test. Nếu không mock, bạn sẽ không bao giờ biết backend chịu được bao nhiêu, vì cổng thật chặn bạn ở TPS thấp. Mock là bắt buộc, không phải tùy chọn.
- Chỉ đo latency của cổng. Latency bạn quan tâm là latency hệ thống của bạn: thời gian ký signature, ghi DB, đẩy queue. Latency của cổng nằm ngoài tầm kiểm soát; đo nó chỉ để tách bạch, không phải để tối ưu.
- Mẹo: đặt tag riêng cho từng cổng trong k6/JMeter (
tag: vnpay,momo,zalopay) để báo cáo tách bạch — mỗi cổng có trần và hành vi khác nhau, gộp chung số liệu sẽ che mất vấn đề. - Mẹo: chạy real-integration test vào giờ thấp điểm và báo trước cho contact tích hợp nếu bài test kéo dài — thể hiện sự chuyên nghiệp và tránh hiểu lầm.
Bài tập thực hành
- Xác định trần và thiết kế: Giả sử bạn test một app đặt vé tích hợp cả ba cổng. Viết ra bảng: mỗi cổng, trần TPS, mức test an toàn (trừ 10%), và loại executor/timer bạn sẽ dùng.
- Viết script ghìm TPS: Tạo một script k6 dùng
constant-arrival-rategiữ đúng 18 TPS trong 5 phút cho MoMo, với thresholdhttp_req_failed: rate<0.01vàp(95)<800ms. Chạy giả lập với một endpoint mock bất kỳ (ví dụ httpbin). - Dựng mock high-load: Viết một mock server đơn giản trả JSON giống response tạo giao dịch, có
sleep300ms. Bắn k6ramping-arrival-ratetừ 100 lên 800 RPS và ghi lại RPS mà tại đó P95 vượt 500ms — đó là điểm nghẽn backend của bạn. - Kịch bản callback trùng: Mô tả cách bạn sẽ chủ động gửi callback IPN hai lần cho cùng một
transaction_idvà cách kiểm chứng hệ thống chỉ cộng tiền một lần. Nêu chỉ số đối soát bạn sẽ theo dõi. - Phản biện báo cáo: Một đồng nghiệp báo "VNPay lỗi 60% khi load test". Viết 3 câu hỏi bạn sẽ đặt để kiểm tra xem đây có phải kết quả hợp lệ hay chỉ là do vượt trần TPS.
Tóm tắt
Test payment gateway ở Việt Nam khác căn bản với các loại load test khác vì cổng thanh toán là bên thứ ba có trần TPS cứng: VNPay ~50, ZaloPay ~30, MoMo ~20 TPS ở sandbox. Bạn không được — và không cần — bắn 1000 RPS vào cổng thật; làm vậy chỉ nhận về 429 vô nghĩa và nguy cơ bị khóa. Thay vào đó, tách bài test làm hai lớp: high-load bằng mock để tìm điểm nghẽn trong code của chính bạn (ký signature, ghi DB, xử lý queue), và real integration ghìm đúng dưới trần TPS để xác nhận tích hợp ổn định. Dùng constant-arrival-rate (k6) hoặc Constant Throughput Timer (JMeter) để ghìm tốc độ. Đừng quên phần nguy hiểm nhất — xử lý callback IPN dưới tải, nơi race condition và lỗi cộng tiền hai lần ẩn nấp — và luôn đối soát ba con số: giao dịch gửi đi, callback nhận về, đơn cập nhật đúng. Nắm được tư duy "chỉ test phần mình sở hữu, tôn trọng giới hạn của bên thứ ba" là bạn đã tránh được sai lầm phổ biến nhất trong nghề.