PAYATHON 2026

如何测试 Payment Processing 功能?

支付阿杰

结论

测试 Payment Processing 时,不应直接用真实银行卡反复付款。更稳妥的做法是建立分层测试体系:

  1. 通过单元测试验证金额计算、订单状态和幂等逻辑。
  2. 使用支付服务商提供的 sandbox/test mode 完成接口集成测试。
  3. 通过端到端测试覆盖付款、回调、退款等核心流程。
  4. 只在生产环境中进行少量且受控的真实交易验证。
  5. 除了“支付成功”,还要重点测试失败、超时、重复通知和状态不一致。

支付系统的问题往往不在付款页面,而在异步回调、网络重试、重复请求、金额精度和订单状态转换。

建议的测试分层

1. 单元测试

将支付网关封装在独立接口后面,并在单元测试中使用 mock 或 fake 实现。这样不会产生真实扣款,测试也不依赖外部服务。

重点验证以下内容:

  • 金额是否使用整数形式的最小货币单位,例如分,而不是浮点数。
  • 币种、税费、折扣和退款金额是否正确。
  • 支付请求是否包含正确的订单信息。
  • 相同请求是否具有幂等性。
  • 系统是否拒绝非法状态。
  • 支付失败后是否保留订单并允许重试。
  • 日志中是否避免记录卡号、CVV 或完整支付凭据。
type PaymentResult =
  | { status: "succeeded"; transactionId: string }
  | { status: "failed"; code: string };

interface PaymentGateway {
  charge(input: {
    orderId: string;
    amount: number;
    currency: string;
    idempotencyKey: string;
  }): Promise<PaymentResult>;
}

async function payOrder(
  gateway: PaymentGateway,
  order: { id: string; totalInCents: number; paid: boolean }
) {
  if (order.paid) {
    return { status: "already_paid" as const };
  }

  return gateway.charge({
    orderId: order.id,
    amount: order.totalInCents,
    currency: "USD",
    idempotencyKey: `payment:${order.id}`
  });
}

测试时可以完全替换第三方支付网关:

import { expect, test, vi } from "vitest";

test("使用订单总额和稳定的幂等键发起支付", async () => {
  const charge = vi.fn().mockResolvedValue({
    status: "succeeded",
    transactionId: "txn_test_001"
  });

  const gateway = { charge };
  const order = {
    id: "order_123",
    totalInCents: 1099,
    paid: false
  };

  await payOrder(gateway, order);

  expect(charge).toHaveBeenCalledWith({
    orderId: "order_123",
    amount: 1099,
    currency: "USD",
    idempotencyKey: "payment:order_123"
  });
});

这里的 1099 表示 10.99 个货币单位。核心金额运算不使用 10.99,可以减少浮点精度问题。

2. Sandbox 集成测试

主流支付服务商通常会提供 sandbox、test mode 或测试商户账户。具体名称和功能取决于服务商,应以官方文档为准。

集成测试需要连接测试环境中的真实 API,并验证:

  • 身份认证和请求签名。
  • 支付方式或 token 的创建。
  • 授权、扣款和支付确认。
  • 取消、全额退款和部分退款。
  • 服务商返回的错误码。
  • Webhook 签名校验。
  • 本地订单与远端交易的关联关系。

不要只测试一个“成功卡号”。如果测试环境支持场景模拟,还要覆盖:

  • 支付被拒绝。
  • 余额不足。
  • 需要额外身份验证。
  • 请求超时。
  • 限流或临时服务错误。
  • 退款失败。
  • 回调延迟或乱序。

测试数据和测试凭据必须与生产环境隔离。应用可以在启动时检查环境配置,防止测试密钥误连生产系统。

端到端测试流程

至少准备一条完整的 happy path:

  1. 创建订单。
  2. 打开结账页面。
  3. 使用服务商提供的测试支付方式提交付款。
  4. 接收并验证 Webhook。
  5. 将订单更新为已支付。
  6. 验证库存、收据或后续业务动作。
  7. 发起退款。
  8. 验证退款状态和订单状态。

浏览器跳转到“支付成功”页面,并不代表端到端测试已经成功。前端重定向可能被伪造,也可能发生在后端收到最终支付结果之前。业务上的支付成功,应以服务端验证后的支付状态为准。

Webhook 测试

Webhook 值得单独测试。回调可能重复、延迟或乱序,也可能因为处理过程中服务重启而被再次发送。

处理逻辑至少应做到:

  • 先验证签名,再处理事件。
  • 使用事件 ID 去重。
  • 同一事件重复到达时,不会重复发货或记账。
  • 未识别的事件类型不会破坏订单状态。
  • 处理失败后可以安全重试。
  • 快速返回响应,将耗时工作交给队列执行。
async function handleWebhook(event: PaymentEvent) {
  const inserted = await eventStore.insertIfAbsent(event.id);

  if (!inserted) {
    return; // 重复事件已经处理
  }

  switch (event.type) {
    case "payment.succeeded":
      await orders.markPaidIfPending(
        event.orderId,
        event.transactionId
      );
      break;

    case "payment.failed":
      await orders.markPaymentFailedIfPending(
        event.orderId,
        event.reason
      );
      break;

    default:
      break;
  }
}

还要专门发送两次相同事件,确认系统最终只执行一次状态变更和一次发货操作。

并发与异常场景

常规 walkthrough 很难发现并发和异常情况下的问题,因此支付测试还要包含以下场景:

  • 用户连续点击两次“支付”。
  • 浏览器提交后立即关闭。
  • API 请求成功,但客户端在收到响应前超时。
  • 两个服务实例同时处理同一回调。
  • 支付已经成功,但数据库更新失败。
  • 退款请求被重复提交。
  • 订单取消后才收到支付成功事件。
  • payment.failed 和 payment.succeeded 乱序到达。
  • 支付金额或币种与订单不一致。

每个场景都要明确最终状态,不能只检查某次接口调用的响应。例如,客户端超时后不应立即创建一笔没有关联的新付款,而应使用幂等键重试,或查询原交易的状态。

状态机测试

可以显式定义订单或支付状态,例如:

pending -> processing -> paid
pending -> failed
processing -> failed
paid -> partially_refunded
paid -> refunded

测试所有允许的转换,并拒绝以下危险转换:

refunded -> paid
failed -> refunded
cancelled -> paid

实际状态可能比示例复杂。设计时要根据支付服务商是否区分 authorization、capture、settlement、dispute 等阶段进行调整。

生产环境验证

Sandbox 无法完整模拟银行网络、风控、结算和真实账单。上线前后通常仍要进行少量真实交易验证,但整个过程必须受控:

  • 使用允许进行真实测试的支付方式。
  • 采用可以接受的小额交易。
  • 测试后按照正常流程退款。
  • 核对支付平台、应用数据库和财务记录。
  • 确认生产 Webhook 地址和签名密钥正确。
  • 确认监控与告警可用。

真实交易测试是否允许、应如何执行,取决于支付服务商、收单机构和所在地区的规则。不能将 sandbox 测试卡用于生产环境,也不应通过虚假交易模拟业务收入。

安全与合规注意事项

  • 不要在自动化测试、源码、日志或截图中保存真实卡号和 CVV。
  • 优先使用支付服务商托管的支付页面、SDK 或 tokenization,减少系统接触敏感卡数据的范围。
  • 测试凭据和生产凭据必须分开,并由密钥管理系统提供。
  • 数据库通常只保存支付 token、交易 ID、卡品牌和脱敏后的末四位等必要信息。
  • 是否需要满足 PCI DSS 以及具体适用范围,应根据实际支付集成方式和合规要求确认。
  • 自动化测试不得无限制调用生产支付 API。

推荐落地顺序

第一次测试支付功能时,可以按照以下顺序实施:

  1. 抽象 PaymentGateway,为业务逻辑编写 mock 单元测试。
  2. 在服务商测试环境中跑通付款和退款。
  3. 为 Webhook 增加签名校验、去重和重试测试。
  4. 补充失败、超时、并发和乱序场景。
  5. 建立每日运行或发布前运行的 sandbox 集成测试。
  6. 上线时执行一笔受控的真实交易,并核对整个链路。
  7. 增加支付失败率、回调积压和账务差异告警。
  8. 定期对账应用订单与支付服务商的交易记录。

可靠的支付测试不仅要证明“按钮可以点击”,还要确认系统在成功、失败、超时和重复通知等情况下,都不会多扣款、重复履约、丢失交易或产生无法解释的账务状态。

备注:内容仅供参考。