PAYATHON 2026

如何测试集成 payment gateway 的网站支付功能?

支付阿杰

结论

可以。绝大多数支付集成测试都能,而且应该在不使用真实资金的情况下完成。

主流支付网关通常提供 Sandbox、Test Mode 或测试商户环境。在这些环境中,开发者使用测试 API Key、测试卡号和模拟支付结果,不会产生真实扣款。开发阶段应覆盖支付成功、失败、取消、超时、退款和重复通知等情况。上线前,再用最小金额完成少量真实交易验证,并立即退款。

具体功能和测试参数因支付网关而异,请以对应网关的官方文档为准。

为什么不能只测试”支付成功”

网站接入支付系统并不是一次简单的接口调用,而是一条完整的处理链路:

  1. 用户提交注册信息。
  2. 网站创建本地订单。
  3. 后端向支付网关创建支付请求。
  4. 用户在托管收银台或支付组件中完成付款。
  5. 支付网关返回前端跳转结果。
  6. 支付网关通过 Webhook 异步通知最终状态。
  7. 网站验证通知并更新订单。
  8. 支付成功后开通账号或会员权益。

任何一个环节都可能出错。尤其要注意,用户浏览器跳转到”支付成功”页面,并不能证明款项已经支付。用户可能关闭页面、伪造请求,也可能在跳转时断网。订单的最终状态应以后端查询结果或通过签名验证的 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,上线前再用最小金额完成一次受控的生产交易。日常测试不需要使用真实资金,同时也能发现生产配置遗漏的问题。

备注:内容仅供参考。