用Collection Runner高效批量测试API接口的实战技巧

迷人的东正 工具 阅读 668
赞 27 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月接了个内部工具项目,需求是批量执行一组 API 调用,每个请求的参数不同,但结构类似,还要支持失败重试、结果导出、执行日志。一开始我直接想用 Postman 的 Collection Runner —— 毕竟它可视化、支持变量、能跑多组数据,看起来挺香。

用Collection Runner高效批量测试API接口的实战技巧

但很快发现不对劲:Postman 虽然能跑,但没法集成到我们自己的系统里,用户不能在我们界面上点一下就跑,而且执行过程不可控,日志也拿不到。更麻烦的是,客户要求“跑完后自动生成 Excel 报告”,这 Postman 根本做不到。

于是转头自己写一个轻量级的 Collection Runner。核心思路很简单:把请求配置成 JSON,前端读取后并发或串行发起 fetch,记录每一步状态,最后汇总结果。技术栈就用原生 JS + Vue(因为项目本身是 Vue 3),不搞复杂依赖,毕竟只是个辅助工具。

最大的坑:性能问题

开始写的时候,我天真地以为“不就是 for 循环发请求嘛”。于是第一版代码长这样:

async function runCollection(testCases) {
  const results = [];
  for (const testCase of testCases) {
    const res = await fetch(testCase.url, {
      method: testCase.method,
      body: JSON.stringify(testCase.body)
    });
    results.push({ ...testCase, status: res.status, data: await res.json() });
  }
  return results;
}

串行执行,逻辑清晰,本地测试 10 条数据没问题。但一拿到真实环境,客户说有 200+ 条用例,我本地一跑——卡死!浏览器直接无响应,等了快一分钟才跑完。更糟的是,中间某个请求超时,整个流程就断了,前面跑过的全白费。

后来改成并发:

async function runCollection(testCases) {
  const promises = testCases.map(testCase =>
    fetch(testCase.url, {
      method: testCase.method,
      body: JSON.stringify(testCase.body)
    }).then(res => ({
      ...testCase,
      status: res.status,
      data: res.json()
    }))
  );
  return Promise.all(promises);
}

结果更惨:200 个请求同时发出去,后端直接限流,一大半返回 429。而且 Promise.all 有个致命问题:只要一个 reject,整个就崩了,连 partial result 都拿不到。

折腾了半天才发现,得控制并发数,还得容忍部分失败。这里注意我踩过好几次坑:一开始用第三方库 p-limit,但项目不允许加新依赖;后来手写了一个带并发控制的 runner,才算稳住。

核心代码就这几行

最终方案是自己实现一个带并发控制和容错的执行器。关键点:最大并发数设为 5,失败自动重试 2 次,所有请求无论成败都记录结果。

async function runWithConcurrency(tasks, maxConcurrency = 5, retries = 2) {
  const results = [];
  const executing = [];

  for (let i = 0; i < tasks.length; i++) {
    const task = tasks[i];
    const attempt = async (retryCount = 0) => {
      try {
        const res = await fetch(task.url, {
          method: task.method || 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task.body)
        });
        const data = await res.json();
        return { ...task, status: res.status, success: true, data };
      } catch (err) {
        if (retryCount < retries) {
          await new Promise(r => setTimeout(r, 1000 * (retryCount + 1))); // 退避
          return attempt(retryCount + 1);
        }
        return { ...task, success: false, error: err.message };
      }
    };

    const promise = attempt().then(result => {
      results[i] = result;
    });

    executing.push(promise);

    if (executing.length >= maxConcurrency) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(p => p === promise), 1);
    }
  }

  await Promise.all(executing);
  return results;
}

调用时也很简单:

const testCases = [
  { url: 'https://jztheme.com/api/validate', method: 'POST', body: { id: 1 } },
  { url: 'https://jztheme.com/api/validate', method: 'POST', body: { id: 2 } },
  // ... 200+ 条
];

const results = await runWithConcurrency(testCases, 5, 2);

这个方案亲测有效:200 条数据 30 秒内跑完,失败的会重试,最终结果数组顺序和输入一致,方便后续处理。而且不会压垮后端,也不会让浏览器卡死。

踩坑提醒:这三点一定注意

  • 别用 Promise.all 处理可能失败的批量请求:它不是为 partial failure 设计的。改用 Promise.allSettled 或自己收集结果,如上所示。
  • 并发控制必须做:哪怕后端没限流,浏览器也有 TCP 连接数限制(Chrome 同域名约 6 个)。超过后请求会排队,反而拖慢整体速度。5-10 并发是个安全值。
  • 重试要有退避策略:我一开始 retry 是立即重试,结果后端被连续打爆。改成指数退避(1s、2s、4s)后,成功率明显提升。

还有一个小问题一直没完美解决:如果用户中途点“停止”,怎么优雅中断?目前做法是加个 abortController,但正在执行的请求无法立刻终止(fetch 不支持 cancel 已发出的请求),只能忽略后续回调。好在影响不大,毕竟用户点了停止就表示不关心结果了。

回顾与反思

整体来看,这个自制的 Collection Runner 达到了预期:稳定、可控、可嵌入业务系统。比起 Postman,虽然少了图形化编辑,但灵活性和集成度高得多。客户现在可以在我们界面上上传 JSON 配置,点运行,等几秒就出报告,体验比切 Postman 好多了。

做得好的地方:并发控制 + 重试机制 + 顺序保持,这三点是核心。不足的地方也有:比如没有做请求之间的依赖(比如 A 的返回作为 B 的输入),但需求里没要求,就没加,避免过度设计。另外错误日志不够详细,目前只存了 error.message,其实应该把完整的 response 甚至 request 都记下来,方便排查。不过这些属于“锦上添花”,当前版本已经够用。

说到底,这种工具类功能,**简单、可靠、够用**比炫技重要。我一开始还想搞个 fancy 的进度条动画,后来发现用户根本不在乎,他们只关心“跑完没”“对不对”。

以上是我个人对这个 Collection Runner 的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合 Web Worker 避免阻塞 UI),后续会继续分享这类博客。希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论