如何测试集成 payment gateway 的网站支付功能?
结论
可以。绝大多数支付集成测试都能,而且应该在不使用真实资金的情况下完成。
主流支付网关通常提供 Sandbox、Test Mode 或测试商户环境。在这些环境中,开发者使用测试 API Key、测试卡号和模拟支付结果,不会产生真实扣款。开发阶段应覆盖支付成功、失败、取消、超时、退款和重复通知等情况。上线前,再用最小金额完成少量真实交易验证,并立即退款。
具体功能和测试参数因支付网关而异,请以对应网关的官方文档为准。
为什么不能只测试”支付成功”
网站接入支付系统并不是一次简单的接口调用,而是一条完整的处理链路:
- 用户提交注册信息。
- 网站创建本地订单。
- 后端向支付网关创建支付请求。
- 用户在托管收银台或支付组件中完成付款。
- 支付网关返回前端跳转结果。
- 支付网关通过 Webhook 异步通知最终状态。
- 网站验证通知并更新订单。
- 支付成功后开通账号或会员权益。
任何一个环节都可能出错。尤其要注意,用户浏览器跳转到”支付成功”页面,并不能证明款项已经支付。用户可能关闭页面、伪造请求,也可能在跳转时断网。订单的最终状态应以后端查询结果或通过签名验证的 Webhook 为准。
推荐的测试环境
至少准备两套独立配置:
Test:
API_BASE_URL = payment gateway test endpoint
API_KEY = test key
WEBHOOK_SECRET = test webhook secret
Production:
API_BASE_URL = payment gateway production endpoint
API_KEY = live key
WEBHOOK_SECRET = production webhook secret
测试密钥和生产密钥必须分开管理。不要将它们写入前端代码,也不要提交到 Git 仓库。可以通过环境变量注入:
PAYMENT_MODE=test
PAYMENT_API_KEY=your_test_api_key
PAYMENT_WEBHOOK_SECRET=your_test_webhook_secret
如果支付网关没有公开的 Sandbox,应向技术支持或商务人员确认其是否提供测试商户、模拟交易接口或认证环境。不要自行猜测测试卡号或特殊金额规则。
完整测试步骤
1. 先测试创建订单
用户点击付款前,服务器应先创建一条本地订单,并生成唯一订单号。金额必须由服务器根据商品或注册方案计算,不能直接采用浏览器提交的金额。
app.post("/api/checkout", async (req, res) => {
const userId = req.user.id;
const plan = await getPlanFromDatabase(req.body.planId);
if (!plan) {
return res.status(400).json({ error: "Invalid plan" });
}
const order = await createOrder({
userId,
amount: plan.price,
currency: plan.currency,
status: "pending"
});
const payment = await paymentGateway.createPayment({
amount: order.amount,
currency: order.currency,
merchantOrderId: order.id,
returnUrl: `${APP_URL}/payment/return`,
metadata: {
orderId: order.id,
userId
}
});
await saveGatewayPaymentId(order.id, payment.id);
res.json({
checkoutUrl: payment.checkoutUrl
});
});
测试时应检查:
- 订单号是否唯一;
- 金额和币种是否正确;
- 前端篡改金额后,服务端是否仍采用数据库中的价格;
- 支付网关请求失败时,本地订单是否仍保持
pending; - 同一用户重复点击付款时,是否会意外创建多笔订单。
这里的 createPayment、checkoutUrl 等名称只是示例。实际接入时,应替换为支付网关 SDK 或 API 定义的名称。
2. 使用测试支付数据完成付款
进入支付网关的测试收银台后,使用官方提供的测试卡号、测试账户或模拟支付方式。测试环境通常可以模拟以下结果:
- 支付成功;
- 卡片被拒;
- 余额不足;
- 身份验证失败;
- 需要额外认证;
- 处理中;
- 支付取消;
- 网络或接口超时。
测试卡号通常只能在 Test Mode 中使用。不同网关提供的数据和触发规则并不相同,必须查阅对应网关的官方测试文档。
3. 验证 Webhook
支付网关一般会异步发送 Webhook。接收端必须保留原始请求体,并按照网关的要求验证签名。
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = paymentGateway.verifyWebhook({
rawBody: req.body,
signature: req.headers["payment-signature"],
secret: process.env.PAYMENT_WEBHOOK_SECRET
});
} catch (error) {
return res.status(400).send("Invalid signature");
}
if (event.type === "payment.succeeded") {
await handlePaymentSucceeded(event.data);
} else if (event.type === "payment.failed") {
await handlePaymentFailed(event.data);
} else if (event.type === "refund.succeeded") {
await handleRefundSucceeded(event.data);
}
res.sendStatus(200);
}
);
payment-signature、事件名称和签名验证方法都是示意写法。实际接入时,必须使用网关规定的请求头、签名算法和 SDK。
收到支付成功通知后,还要重新核对关键字段:
async function handlePaymentSucceeded(payment) {
const order = await findOrderForUpdate(payment.merchantOrderId);
if (!order) {
throw new Error("Order not found");
}
if (
payment.amount !== order.amount ||
payment.currency !== order.currency
) {
throw new Error("Payment amount or currency mismatch");
}
if (order.status === "paid") {
return;
}
await runInTransaction(async () => {
await markOrderAsPaid(order.id, payment.id);
await activateUserRegistration(order.userId);
});
}
Webhook 测试应覆盖:
- 正确签名的通知能够正常处理;
- 签名错误或缺少签名时,接收端会拒绝处理;
- 重复收到通知时,不会重复开通账号;
- 通知顺序颠倒时,订单状态不会回退;
- 通知延迟时,订单仍能正确更新;
- 金额、币种或订单号不一致时,不会发放权益;
- Webhook 处理失败后,网关重试能够让流程恢复。
必须覆盖的测试场景
建议将这些场景纳入集成测试清单:
| 场景 | 预期结果 |
|---|---|
| 支付成功 | 订单变为 paid,账号只开通一次 |
| 用户取消支付 | 订单保持 pending 或变为 cancelled |
| 支付被拒 | 显示易于理解的提示,不开通账号 |
| 支付处理中 | 暂不发放权益,等待最终通知 |
| Webhook 延迟 | 返回页面可以显示”正在确认支付” |
| Webhook 重复 | 不重复记账,也不重复开通账号 |
| Webhook 签名错误 | 拒绝处理并记录安全日志 |
| 金额不一致 | 拒绝开通并触发告警 |
| 币种不一致 | 拒绝开通并触发告警 |
| 用户刷新成功页 | 不重复执行支付完成逻辑 |
| 创建支付请求超时 | 可以安全重试,不产生无法识别的重复交易 |
| 全额退款 | 更新订单和权益状态 |
| 部分退款 | 正确记录退款金额,不直接标记为全额退款 |
| 支付后拒付 | 根据业务规则暂停权益并转入人工处理 |
自动化测试怎么做
自动化测试可以分成三层。
单元测试
将支付 SDK 封装成独立服务,在单元测试中使用 Mock 验证业务逻辑,不访问真实支付网关。
test("activates registration after a valid payment", async () => {
paymentGateway.getPayment.mockResolvedValue({
id: "pay_test_123",
status: "succeeded",
merchantOrderId: "order_123",
amount: 9900,
currency: "CNY"
});
await confirmPayment("order_123");
expect(markOrderAsPaid).toHaveBeenCalledWith(
"order_123",
"pay_test_123"
);
expect(activateUserRegistration).toHaveBeenCalledTimes(1);
});
金额最好以最小货币单位的整数保存。例如,9900 表示 99.00 CNY,这样可以避免浮点数计算误差。是否采用最小货币单位,以及不同币种使用几位小数,应以网关文档为准。
集成测试
使用支付网关的测试 API 和测试密钥,实际创建测试支付对象,并检查:
- 请求格式是否正确;
- API 认证是否成功;
- 测试订单能否完成;
- Webhook 能否到达测试服务器;
- 签名验证是否有效;
- 本地订单状态是否正确变化。
如果本地开发环境无法接收公网 Webhook,可以使用支付网关官方提供的本地转发工具。如果官方没有提供,也可以使用经过授权的安全隧道服务,但不要暴露无关的本地接口。
端到端测试
使用浏览器自动化工具模拟用户注册和付款流程。如果支付页面由网关托管,自动化测试可能会受到跨域、安全校验或验证码的限制。可以采用以下方式:
- 在 Sandbox 中使用网关支持的测试支付方式;
- 自动化执行到跳转收银台之前,再通过测试 API 推动交易状态;
- 使用网关提供的模拟事件或测试 Webhook 功能;
- 保留少量人工验收用例。
不要为了测试方便,在生产代码中加入使用固定参数就能支付成功的后门。
前端返回页应如何处理
返回页不能直接将订单标记为已支付。更安全的做法是由前端查询服务器:
app.get("/api/orders/:orderId/status", async (req, res) => {
const order = await findOrder(req.params.orderId);
if (!order || order.userId !== req.user.id) {
return res.sendStatus(404);
}
res.json({
status: order.status
});
});
前端再根据订单状态显示结果:
const response = await fetch(`/api/orders/${orderId}/status`);
const order = await response.json();
if (order.status === "paid") {
showSuccess();
} else if (order.status === "pending") {
showPendingMessage();
} else {
showPaymentFailed();
}
如果需要主动查询支付网关,也应由后端发起。生产环境还要限制查询频率,避免用户每次刷新页面时都无节制地调用网关接口。
是否还需要真实付款测试
建议在正式上线前完成少量真实交易验证。Sandbox 无法完全复制生产环境中的以下情况:
- 生产密钥和权限配置;
- 真实支付渠道是否已经启用;
- 生产域名和回调地址;
- 银行侧认证流程;
- 实际手续费和结算记录;
- 生产 Webhook 的网络连通性;
- 退款到账过程。
真实验证应使用可控的最小金额,确认成功后按流程退款。是否允许这样操作、最低支付金额是多少、手续费能否退回,都要根据支付网关和商户协议确认。
不要在生产环境中反复制造大量付款和退款。这可能触发风控,也可能产生无法退回的手续费。
关键注意事项
- 测试 API Key 与生产 API Key 必须分开使用。
- 私钥和 Webhook Secret 不能保存在浏览器端。
- 不要相信前端传入的金额、币种和支付状态。
- 不要只依赖支付完成后的浏览器跳转。
- Webhook 必须验证签名,并尽可能校验时间戳,以防伪造或重放。
- 所有支付完成逻辑都应具备幂等性。
- 数据库更新和账号开通应尽量放在同一个事务中。
- 日志中不要记录完整卡号、CVV、支付密码、私钥或未经处理的敏感个人信息。
- 除非业务确有需要,并且已经满足相应的 PCI DSS 合规要求,否则网站不应直接处理或保存卡片敏感数据。
- 测试数据和生产数据应彼此隔离,测试订单需要有明确标识。
- 对账时不能只检查本地订单,还应定期与支付网关的交易记录核对。
- 支付、退款、拒付和争议是不同的状态,不能用一个布尔字段概括。
较稳妥的安排是:开发阶段使用 Mock 验证业务逻辑,联调阶段使用支付网关的 Sandbox 测试真实接口和 Webhook,上线前再用最小金额完成一次受控的生产交易。日常测试不需要使用真实资金,同时也能发现生产配置遗漏的问题。
备注:内容仅供参考。
