如何在 CI 中使用 Apifox 捕获并验证 Stripe Webhook

别再让支付回调测试成为盲区!本文教你如何在CI中利用Apidog,通过“先捕获后查询”模式,将Stripe Webhook数据存入数据库并进行断言验证,轻松搞定异步事件测试,保障支付逻辑稳健。

用 Apifox,节省研发团队的每一分钟

如何在 CI 中使用 Apifox 捕获并验证 Stripe Webhook

免费使用 Apifox

相关推荐

最新文章

API

一体化协作平台

API 设计

API 文档

API 调试

自动化测试

API Mock

API Hub

立即体验 Apifox
目录

客户付款后,Stripe 会向您的后端触发一个 payment_intent.succeeded 事件,而您的接口应该将订单标记为已支付。正是这最后一步往往会静默出错。Webhook 送达,您的处理程序抛出异常,却无人察觉,直到有客服工单反馈说:“我付了钱,但我的账户还是显示未付款。”您需要的是每次发布代码时,CI 中的测试都能证明该事件已送达并被正确处理。

棘手之处在于,webhook 是 Stripe 发送给您的入站 HTTP 调用,而不是您主动发起的请求。大多数 API 测试工具都是为了发送请求并检查响应而设计的,这与 webhook 的方向相反。那么问题来了:在 CI 运行期间,在没有人为监控的情况下,您该如何对一个随时可能送达的事件进行断言?本指南将介绍使用 Apifox 实现这一目标的可靠且受支持的方法,但首先需要了解一个局限性。如果您想先宏观了解如何测试事件驱动型接口,可以参考我们关于如何测试 webhook 的指南,同时 Stripe 官方的 webhook 文档也涵盖了事件传输模型。

必须围绕其进行设计的限制

以下是关键的事实,正如 Apifox 自己的文档中所坦言的那样:“ApiDog 原生不支持监听 webhook。”Apifox 并不会监听某个公网 URL 并实时捕获 Stripe 的入站调用。如果您寄希望于将 Stripe 指向 Apifox 的监听器并看着事件源源不断地进来,这条路是行不通的。

这听起来像是一个死胡同,但其实不然。它只是改变了测试的形式。您无需在 webhook 到达时进行拦截,而是在您自己的后端捕获并存储它,然后让 Apifox 查询该存储记录并对其进行断言。先捕获,后验证。一旦您接受了这种拆分方式,整个工作流就会变得非常简单明了,而且重要的是,它能完美契合 CI,因为数据库查询是具有确定性且可重复的。

“先捕获后查询”模式的工作原理

Apifox 文档推荐的模式包含以下四个活动部分:

  1. 在您的后端服务中创建一个接口,用于捕获传入的 Stripe webhook。
  2. 将 webhook 事件数据存储在数据库的 Stripe event logs 表中。
  3. 使用 Apifox 的后置操作查询您的数据库。
  4. 检索存储的 webhook 事件,并将其与预期结果进行校验。

这其中的两个步骤存在于您的代码中,另外两个步骤则存在于 Apifox 中。捕获接口和日志记录表由您负责构建,因为它们运行在您的应用程序内部。一旦事件进入数据库,Apifox 的工作就开始了:它会连接到该数据库并读取相应的行,以确认事件是否按照您预期的方式被处理。理清这一职责划分,剩下的工作就会水到渠成。

步骤 1:构建捕获接口

你的后端需要一个 Stripe 能够发送 POST 请求的路由。这是普通的应用程序代码,而不是 Apifox 的功能。一个用于验证签名并记录事件的极简 Express 处理程序如下所示:

import express from "express";
import Stripe from "stripe";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        req.headers["stripe-signature"],
        endpointSecret
      );
    } catch (err) {
      return res.status(400).send(`Signature check failed: ${err.message}`);
    }

    // Persist the event so a test can read it back later.
    await db.query(
      `INSERT INTO stripe_event_logs (event_id, type, payload, handled_at)
       VALUES ($1, $2, $3, now())
       ON CONFLICT (event_id) DO NOTHING`,
      [event.id, event.type, JSON.stringify(event.data.object)]
    );

    if (event.type === "payment_intent.succeeded") {
      const intent = event.data.object;
      await markOrderPaid(intent.metadata.order_id);
    }

    res.json({ received: true });
  }
);

这里有两点至关重要。第一,在信任任何内容之前,你需要通过 constructEvent 验证 Stripe 签名,这是任何 webhook 接收程序不可或缺的安全步骤。如果你想了解该校验背后的完整原理,我们关于 webhook 签名验证的详细指南深入探讨了为什么对比原始 body 是唯一安全的方法。第二,你需要将事件写入 Stripe event logs 表中。该行数据正是 Apifox 将要读取的内容。由于 Stripe 可能会多次推送同一个事件,ON CONFLICT DO NOTHING 子句可以保持日志的幂等性。

步骤 2:在 Apifox 环境中连接你的数据库

Apifox 支持在对应的环境中连接数据库,而正是这种连接让整个模式得以运转。为你 CI 运行所指向的环境设置数据库连接,无论是预发环境 (staging) 的 Postgres 还是专用的测试数据库。一旦连接建立,测试步骤就可以对其执行 SQL 并获取真实的数据行。

将数据库连接与你正在测试的环境相匹配。针对预发环境运行的测试应该查询预发数据库,这样你的测试所触发的事件就是你的测试所读取的事件。环境不匹配是导致原本已通过的捕获接口在断言时仍然失败的最常见原因。

步骤 3:添加后置操作以查询日志

这是核心所在。后置操作是 Apifox 的一项功能,用于在测试中查询数据库并验证记录的 webhook 事件。你可以将其附加到测试场景中的某个请求上。请求运行后,该操作将执行你的 SQL,读取存储的事件,并允许你对结果进行断言。

payment_intent.succeeded 场景的实际流程如下:

  1. 你的测试场景触发支付。这可以是一个创建 payment intent 并在 Stripe 测试模式下进行确认的请求,也可以是一个向你的捕获接口发送已知测试事件的 fixture(测试夹具)。
  2. Stripe 将 webhook 发送到你的 /webhooks/stripe 路由,该路由验证签名并将一行数据写入 stripe_event_logs 中。
  3. 下一步骤中的后置操作会查询该表以获取该事件。

该操作运行的查询是普通的 SQL:

SELECT event_id, type, payload, handled_at
FROM stripe_event_logs
WHERE type = 'payment_intent.succeeded'
ORDER BY handled_at DESC
LIMIT 1;

然后你对返回的行数据进行断言。当记录的数据符合你的预期时,测试通过:即 typepayment_intent.succeededevent_id 与你触发的事件匹配,payload 中的金额等于你的收费金额,并且 handled_at 已填充(这证明了你的处理器确实运行了,而不仅仅是一行占位符数据)。获取存储的 webhook 事件,将其与预期结果进行比较,并由断言决定通过还是失败。

由于 webhook 的传递不是即时的,在查询之前给事件一点时间来送达。添加一个简短的延迟步骤,或者在失败前重试几次查询的轮询循环,可以避免测试与 Stripe 的发送产生竞态条件。这是 webhook 的异步特性影响测试设计的唯一地方,而一个小小的重试窗口就能干净利落地处理好它。

关于本地开发期间实时转发的说明

“先捕获后查询”模式是为 CI(持续集成)设计的,在 CI 中,数据库和存储的记录正是你所需要的。本地开发则是另一回事。当你在笔记本电脑上编写处理器时,Stripe 无法直接访问 localhost,因此你需要某种工具将事件实时转发到你的机器上。

为此,Apifox 的文档指向了一个 webhook 转发(relay)服务,并以 Stripe CLI 和 Ngrok 为例。Stripe CLI 可以监听事件并直接将其转发到你的本地端口:

stripe listen --forward-to localhost:3000/webhooks/stripe

这使你在构建处理器时能够接收到实时事件。Ngrok 也扮演着同样的角色,它将你的本地端口暴露在一个公共 URL 上,你可以将该 URL 注册为 Stripe 接口。在内部开发循环中可以使用这些工具,然后依赖数据库加后置操作流程,在你的流水线中运行断言。两者是互补的:转发用于构建,先捕获后查询用于验证。

不要将此与 Apifox 原生的 Webhook 功能混淆

Apifox 确实有一个字面意思为 Webhook 的功能,人们很容易认为这就是用来接收 Stripe 事件的。事实并非如此,把它们混为一谈会浪费你一下午的时间。原生的 Webhook 功能是用来定义和编写出站(outbound)webhook 文档的,也就是说,当某个事件发生时,你自己的系统所调用的 HTTP 接口。这是由系统发起对外部 URL 的调用,与常规接口由客户端调用你的情况正好相反。它在接口文档中用于描述状态变更通知和异步任务结果,而不是为了接收 Stripe 的入站(inbound)调用。

如果你确实想要为你自己的出站 webhook 编写文档,流程很简单:

  1. 点击左侧边栏的 + 图标。
  2. 选择 New Other Protocol APIs,然后选择 Webhook
  3. 填写必填字段:Request Method(通常为 POST)、Webhook Name、仅用于测试的可选 Debug URL,以及用于请求 body、headers 和配置的 Other Info
  4. 点击 Save

要进行尝试,请在 Debug URL 字段中输入一个 URL,然后点击 Send 来模拟 webhook 调用。值得记住的一个注意事项是:Debug URL 仅用于测试,不会出现在你发布的文档或导出的 OpenAPI 文件中。要全面了解如何设计和记录事件回调,我们在 API 设计中关于 webhook 的文章介绍了它们的应用场景。本文的简要结论是:原生的 Webhook 功能定义了你的出站事件,而“捕获-然后-查询”(capture-then-query)模式则验证了 Stripe 的入站事件。请将它们在概念上区分开来。

变体与加固

一旦基本断言正常工作,进行一些优化就可以达到生产级标准。首先,断言不仅仅局限于事件类型。端到端地校验 event_id,以确保你所验证的正是你触发的那个特定事件,而不是上一次运行遗留的内容。如果事件堆积,可以在每次测试运行时清空(truncate)或限制 stripe_event_logs 表的范围。

其次,测试失败路径。触发一个你的处理程序应该拒绝的事件——例如错误的签名或非预期的类型,并断言没有写入 handled_at 时间戳。一个只检查正常路径(happy path)的 webhook 测试套件会遗漏那些真正会在凌晨两点向你报警的异常情况。我们关于支付 webhook 最佳实践的文章涵盖了值得写入这些测试中的幂等性和重试行为。

第三,使断言紧密贴合业务含义,而不仅仅是交付结果。“事件已送达”的断言力度远不如“订单已变更为已支付”。如果你的处理程序更新了 orders 表,请添加第二个查询来确认下游状态已改变,这样测试才能证明整个链路(而不只是日志写入)都正常工作。

你还可以将此应用到合并流水线之外。一旦在 Apifox 中保存了测试场景,就可以为其设置定时任务,使其按固定频率运行,这样即使在部署间隙,出现故障的 webhook 处理程序也会暴露出来。我们关于如何在 Apifox 中设置 API 测试定时任务的指南介绍了如何为这种验证设置定时器。

使用 Apifox CLI 实现工作流自动化

当在无人值守的情况下运行时,上述所有工作都会发挥作用,这正是 Apifox CLI 的用武之地。这在本质上是一个 CI 场景,因此将保存的测试场景接入到你的流水线(pipeline)中是顺理成章的最后一步。安装 CLI 并使用你的 Token 进行身份验证:

npm install -g apifox-cli
apifox login --with-token <YOUR_ACCESS_TOKEN>

然后,在包含保存事件日志数据库的环境中,以无头模式运行你保存的 webhook 校验测试场景:

apifox run --access-token $APIFOX_ACCESS_TOKEN -t <SCENARIO_ID> -e <ENV_ID> -r cli

其中 -t 是测试场景 ID,-e 是环境 ID,-r 用于选择报告器(reporter)。如果你希望在 CI 产物中除了控制台输出外,还能获得一个可浏览的报告,请使用 -r html,cli。该测试场景包含 后置操作 及其数据库查询,因此单个命令即可触发整个流程,读取 stripe_event_logs 数据行。如果断言失败,它将返回非零退出代码,这正是流水线拦截合并所需要的。Apifox CLI 安装指南介绍了 Token 的设置,而我们的 CI/CD 流水线教程则展示了围绕该命令的完整 GitHub Actions 配置。

常见问题

Apifox 可以直接接收 Stripe webhook 吗? 不能。Apifox 的文档中明确指出它“原生不支持监听 webhook”。你需要在自己的后端接口中捕获该事件,将其存储在数据库中,然后 Apifox 会通过 后置操作 读取该事件。对于本地开发期间的实时转发,建议使用 Stripe CLI 或 Ngrok 等中继工具。

断言实际上是在哪里进行的? 在你的测试场景中请求的 后置操作 步骤里。它会通过你在环境中配置的数据库连接查询 Stripe event logs 表,获取存储的事件,并将其与你的预期值进行对比。当记录的数据匹配时,测试即通过。

进行数据库验证流程需要付费方案吗? Apifox 关于此工作流的文档并未说明有任何方案限制,因此本指南在此不作臆测。最稳妥的办法是查看定价页面上的当前方案详情。你可以下载 Apifox 并创建一个测试项目,亲自体验后置操作和环境数据库连接。

如何处理触发和交付之间的延迟? Webhook 的交付并不是即时的,因此在查询前添加简短的等待或轮询重试,以避免你的测试与 Stripe 产生竞态条件。通常在几秒钟内重试几次就足够了。如果你是第一次对异步接口进行断言,建议在深入研究 Stripe 特性之前,先阅读通用的“如何测试 webhook”指南。

原生的 Webhook 功能在这里有用吗? 对于捕获 Stripe 事件来说完全没用。该功能旨在定义和记录您自己的外发 webhook(即您的系统调用外部 URL)。它是一个文档和设计工具,与本文所使用的“先捕获后查询”的入站模式是完全不同的。请务必将两者清晰地区分开来。

总结

你无法直接将 Stripe 对接至 Apifox 并实时捕获事件,如果非要这样做,只会让你白白沮丧一下午。实际上,支持的解决路径比看起来要清晰得多:在您自己的接口中捕获 webhook,将其记录到 Stripe event logs 表中,然后让 Apifox 的后置操作查询该记录并断言事件已被处理。将保存的测试场景封装在 apifox run 中,您的流水线就能在每次合并时证明,一个真实的支付事件确实能将您的订单状态变更为已支付。立即免费试用(无需信用卡),为您最关键的 webhook 提供真实的断言保障。

开发必备:API 全流程管理神器 Apifox

介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、调试、设计、测试、Mock、自动化测试于一体的工具,Apifox 是目前提升研发效率的首选。

如果你正在开发项目,不妨试试其极其友好的界面设计,它完全兼容 Postman 和 Swagger 数据格式,导入数据非常方便,,即使是新手也能很快上手,点击这里即可注册使用

Apifox

值得一提的是,除了个人和常规团队使用,针对有高安全合规要求、或需要在内网环境协作的企业,Apifox 还提供了深度定制的私有化部署方案

获取专属报价与部署方案

icon 详细的私有化部署系统架构与安全白皮书
icon 针对您公司规模的专属报价单
icon 免费的 1v1 专属产品演示 (Demo) 机会
获取部署方案
* 提交后,我们的客户经理将在 1 个工作日内与您联系
林俊锋 企业微信
@Apifox 专属顾问
扫码备注: 私有化 + 公司名