Postman脚本实现并发测试:从顺序执行到异步并发的实战指南
1. 项目概述从“顺序”到“并发”的测试思维跃迁在接口测试的日常工作中Postman 的 Collection Runner集合运行器是大家再熟悉不过的工具了。我们通常用它来批量、顺序地执行一系列请求验证接口的响应是否符合预期。然而当场景切换到需要模拟真实用户并发访问的压力测试时很多测试同学会感到一丝无力——点开 Runner看着请求一个接一个地执行这哪里是“并发”这分明是“排队”。于是大家纷纷转向 JMeter、LoadRunner 这类专业的压力测试工具。但有没有想过其实 Postman 本身就蕴藏着实现真正并发测试的潜力这个潜力就藏在它的脚本引擎里。我最初也认为 Postman 只是个“高级版的 API 调试工具”直到在一次紧急的、小范围并发验证需求中手头没有现成的压测环境才被迫深挖它的脚本能力。结果发现通过巧妙地组合 Pre-request Script 和 Tests Script配合 Runner 的迭代设置我们完全可以在 Postman 内部实现一个轻量级但非常有效的并发测试框架。这不仅能应对一些临时、快速的并发验证场景更能让你对“并发”的本质——异步执行和资源竞争——有更深刻的理解。今天我就来拆解这些隐藏技巧让你手里的 Postman 变身为一款灵活的并发测试利器。2. 核心思路拆解Postman 并发测试的底层逻辑在开始写脚本之前我们必须先搞清楚 Postman Runner 默认为什么是“顺序执行”以及我们实现“并发”需要突破哪些限制。2.1 Postman Runner 的默认行为与限制当你把一个请求集合Collection丢进 Runner设置迭代次数为 5你会看到 5 次请求被依次执行。这是因为 Postman 的 JavaScript 执行环境基于 Node.js 的沙箱在默认情况下对于集合中的请求采用的是同步、顺序的执行模型。Runner 会等待上一个请求从发送到接收测试脚本执行完毕全过程结束后再开始下一个请求或下一次迭代。这种设计对于功能测试和流程测试是合理的保证了测试步骤的确定性和可重复性。但对于并发测试我们需要的是多个请求在同一时刻或极短时间内同时被触发模拟多个用户同时操作。这要求我们跳出这个线性的执行流。2.2 实现并发的关键异步执行与控制权实现并发的核心思想是“异步”。在 JavaScript 中我们通常使用setTimeout、Promise、async/await等机制来让代码“同时”发起多个操作而不是等待一个完成再开始下一个。在 Postman 的上下文中我们无法直接修改 Runner 的调度引擎。但我们可以利用一个关键特性Pre-request Script预请求脚本和 Tests Script测试脚本的执行与请求本身的发送/接收在时间线上是可以被“错开”和“重叠”的。我们的策略是在 Pre-request Script 中发起请求将原本应该由 Runner 调度的请求改为在脚本中主动、异步地发送。控制 Runner 的迭代节奏让 Runner 的每一次迭代不再是执行一个请求而是瞬间通过异步发起一批请求。收集和管理响应由于请求是异步发送的我们需要一套机制来收集所有请求的响应结果并进行统一的断言和汇总而不是在单个请求的 Tests 脚本里处理。这听起来有点绕但本质上是将请求的“触发权”从 Runner 手中夺过来用我们自己的脚本逻辑来控制从而实现并发。2.3 与 JMeter 等专业工具的定位差异在深入之前必须明确一点用 Postman 脚本实现并发不是为了替代 JMeter 这类专业压测工具。JMeter 拥有完善的线程组、定时器、监听器、资源监控和分布式压测能力适合进行大规模、长时间、有复杂场景建模的正式压力测试。Postman 脚本并发的定位在于快速验证开发过程中快速验证某个接口或某几个接口是否能承受住预期的瞬时并发例如秒杀场景的前端按钮点击。环境受限在没有权限或时间搭建 JMeter 环境时进行紧急验证。与 CI/CD 流程中的集合测试结合在已有的 Postman 集合基础上快速增加一个并发测试用例而无需引入新工具链。理解并发原理作为一个轻量化的教学或理解并发概念的工具。注意Postman 的沙箱环境对脚本的执行时间和资源如内存有严格限制。因此我们的并发测试规模不宜过大例如单次迭代发起数十到数百个请求比较合适上千个可能就会遇到超时或内存问题测试时长也应控制在几分钟内。对于大规模压测请务必使用专业工具。3. 脚本实现详解构建轻量级并发测试框架接下来我们一步步构建这个框架。我将以一个简单的“查询用户信息”接口为例演示如何模拟 20 个用户并发查询。3.1 基础架构利用setTimeout与pm.sendRequest这是最直接、兼容性最好的方法主要依赖两个核心函数pm.sendRequest(): Postman 提供的内置函数允许你在脚本中动态发送 HTTP 请求。这是实现脚本发请求的基础。setTimeout(func, delay): 标准的 JavaScript 函数用于在指定的延迟毫秒后执行一个函数。将延迟设置为0或一个很小的值可以让多个sendRequest几乎同时进入事件队列达到并发的效果。实现步骤第一步准备请求数据假设我们需要并发调用GET https://api.example.com/user/{id}id 从 1 到 20。我们可以在 Pre-request Script 中构造这个数据数组。第二步在 Pre-request Script 中编写并发逻辑我们在 Collection 或某个 Folder 的 Pre-request Script 中编写以下代码// 1. 定义要并发请求的URL列表或参数列表 const concurrentUsers 20; const baseUrl pm.variables.get(“baseUrl”) || “https://api.example.com“; // 建议用变量管理 const requestUrls []; for (let i 1; i concurrentUsers; i) { requestUrls.push(${baseUrl}/user/${i}); } // 2. 初始化一个数组用于存放所有请求的Promise对象方便后续统一处理结果 const requestPromises []; // 3. 定义发送单个请求的函数 function sendSingleRequest(url) { return new Promise((resolve, reject) { // 使用 setTimeout 包裹延迟设为0让所有请求尽快进入异步队列 setTimeout(() { const req { url: url, method: ‘GET’, // 可以在这里添加headers、body等与普通请求配置一致 header: { ‘Authorization’: pm.variables.get(‘authToken’) } }; // 使用 pm.sendRequest 发送请求 pm.sendRequest(req, (err, response) { if (err) { console.error(请求 ${url} 失败:, err); reject(err); } else { // 将响应结果存入一个对象包含url和response便于追踪 resolve({ url: url, code: response.code, body: response.json(), time: response.responseTime }); } }); }, 0); // 延迟0毫秒 }); } // 4. 并发发起所有请求 requestUrls.forEach(url { requestPromises.push(sendSingleRequest(url)); }); // 5. 将Promise数组保存到全局变量或环境变量中以便在Tests脚本中等待所有请求完成并进行断言 // 注意这里不能直接用 pm.environment.set因为值可能过大。我们保存Promise数组的引用。 // 一种技巧是利用Postman的全局对象谨慎使用。 if (typeof globalThis.concurrentPromiseArray ‘undefined’) { globalThis.concurrentPromiseArray []; } globalThis.concurrentPromiseArray requestPromises; // 6. 为了不让Runner认为这个请求已经结束我们需要“阻塞”一下。 // 我们可以设置一个标志位在Tests脚本中检查。但更常见的做法是这个Pre-request Script所在的请求本身可以是一个“空请求”或“控制请求”。 // 例如这个请求的URL可以设为一个无关紧要的端点或者直接使用POSTMAN的echo服务。 // 我们这里假设这个请求本身不重要重要的是它触发的并发子请求。第三步在 Tests Script 中收集结果并断言在同一个请求的 Tests 标签页中我们编写代码来等待所有并发请求完成// 等待所有并发请求完成 Promise.all(globalThis.concurrentPromiseArray) .then(responses { console.log(所有 ${responses.length} 个并发请求完成); // 1. 检查所有请求是否都成功例如状态码均为200 const allSuccess responses.every(resp resp.code 200); pm.test(“所有并发请求均应成功”, function () { pm.expect(allSuccess).to.be.true; }); // 2. 统计平均响应时间 const totalTime responses.reduce((sum, resp) sum resp.time, 0); const avgTime totalTime / responses.length; console.log(平均响应时间: ${avgTime} ms); pm.test(“平均响应时间应小于500ms”, function () { pm.expect(avgTime).to.be.below(500); }); // 3. 检查每个返回的用户ID是否正确 responses.forEach((resp, index) { const expectedId index 1; pm.test(响应 ${resp.url} 的用户ID应为 ${expectedId}, function () { pm.expect(resp.body.id).to.eql(expectedId); }); }); // 4. 可选将关键结果存入环境变量供后续请求或可视化使用 pm.environment.set(“concurrentTestAvgTime”, avgTime); pm.environment.set(“concurrentTestTotalCount”, responses.length); const failedCount responses.filter(r r.code ! 200).length; pm.environment.set(“concurrentTestFailedCount”, failedCount); }) .catch(errors { console.error(“并发请求执行过程中发生错误:”, errors); pm.test(“并发请求执行失败”, function () { pm.expect.fail(“并发请求中存在失败: ” JSON.stringify(errors)); }); }); // 最后清理全局变量避免影响下一次迭代 setTimeout(() { globalThis.concurrentPromiseArray []; }, 0);第四步配置 Runner将上面编写了脚本的请求单独放在一个文件夹中或者作为集合的第一个请求。打开 Collection Runner选择你的集合。将迭代次数Iterations设置为 1。因为我们的并发逻辑在单次迭代内已经完成了发起了20个请求。如果你需要模拟“多轮”并发冲击比如模拟连续5波并发那么可以将迭代次数设为5但要注意脚本中全局变量的清理和重置。设置好环境变量如baseUrl,authToken。点击运行。你会发现Runner 日志中这个“控制请求”本身很快完成但在后台Tests 脚本正在等待并处理那20个并发请求的结果。通过查看 Postman 控制台View - Show Postman Console你可以清晰地看到所有并发请求的发送和接收日志几乎在同一时间点。3.2 进阶优化使用Promise.all与动态数据上面的基础架构已经实现了并发。我们可以进一步优化1. 动态生成更真实的请求数据不仅仅是递增的ID可以从一个预定义的数据池中随机选取或者读取一个CSV文件、JSON数组来作为参数。// 在Pre-request Script中定义数据池 const userPool [ {id: 101, name: ‘Alice’}, {id: 205, name: ‘Bob’}, {id: 308, name: ‘Charlie’}, // ... 更多数据 ]; const concurrentCount 10; const selectedUsers []; for(let i0; iconcurrentCount; i) { const randomUser userPool[Math.floor(Math.random() * userPool.length)]; selectedUsers.push(randomUser); } // 然后根据 selectedUsers 构造请求2. 控制并发节奏与思考时间真正的用户操作之间有间隔。我们可以使用setTimeout为每个请求设置不同的、随机的延迟来模拟更真实的用户行为分布如指数分布。function sendSingleRequestWithDelay(url, delayMs) { return new Promise((resolve, reject) { setTimeout(() { const req { url: url, method: ‘GET’}; pm.sendRequest(req, (err, res) { /* … */ }); }, delayMs); // 每个请求有不同的延迟 }); } // 为每个请求生成一个随机延迟例如0-3000毫秒 requestUrls.forEach(url { const delay Math.floor(Math.random() * 3000); requestPromises.push(sendSingleRequestWithDelay(url, delay)); }); // 注意这样总的测试时长会拉长但并发峰值可能更平滑。3. 更优雅的全局状态管理使用globalThis可能在某些沙箱环境下不够稳定。一个更健壮的做法是利用 Postman 的环境变量或全局变量来传递一个唯一标识符然后将 Promise 数组存储在一个“模拟全局”的 Map 中用这个标识符作为 Key。// Pre-request Script const batchId Date.now() Math.random(); pm.environment.set(“concurrentBatchId”, batchId); if (!globalThis.concurrencyMap) { globalThis.concurrencyMap new Map(); } globalThis.concurrencyMap.set(batchId, requestPromises); // Tests Script const batchId pm.environment.get(“concurrentBatchId”); const promises globalThis.concurrencyMap.get(batchId); if (promises) { Promise.all(promises).then(/* … */).finally(() { globalThis.concurrencyMap.delete(batchId); // 清理 }); }4. 实战技巧与避坑指南在实际操作中我踩过不少坑也总结了一些让测试更稳定、更有效的技巧。4.1 控制并发规模与资源单次迭代并发数建议从 10-50 开始尝试。Postman 沙箱内存有限一次性发起上千个Promise和sendRequest很容易导致脚本执行超时默认约5分钟或内存不足请求会失败。迭代次数与间隔如果你需要模拟更大规模的并发不要盲目增加单次迭代的并发数。可以利用 Runner 的迭代次数和延迟Delay功能。例如设置迭代次数为 100单次迭代并发 10 个请求迭代延迟为 100ms。这样就能模拟 1000 个请求在较短时间内约10秒发出去且对单次脚本执行的压力较小。监控控制台务必打开 Postman Console (View - Show Postman Console)。这里是查看所有请求包括脚本发起的日志、网络耗时和错误信息的关键窗口。如果看到大量ECONNRESET或socket hang up错误可能是目标服务器或本地网络无法处理你的并发量需要调低。4.2 断言与结果分析的策略聚合断言优于分散断言像示例中那样在Promise.all().then()里进行聚合断言如“所有请求成功”、“平均响应时间达标”更清晰。避免为每个并发请求在sendRequest的回调里单独写pm.test那样会导致测试报告条目爆炸难以阅读。记录关键指标将平均响应时间、95/99分位响应时间、失败率等关键指标通过pm.environment.set记录下来。你甚至可以在所有迭代完成后在 Runner 的界面上看到这些环境变量的最终值进行手工记录。处理部分失败Promise.all有一个特性是“全有或全无”只要一个 Promise 被 reject整个Promise.all就会立即 reject进入.catch分支。这对于需要统计部分成功、部分失败的场景不友好。可以使用Promise.allSettled如果 Postman 的 JS 环境支持或者自己封装一个处理函数等待所有 Promise 完成无论成功失败然后再分析结果。// 模拟 Promise.allSettled 行为 function allSettled(promises) { return Promise.all(promises.map(p p.then(value ({ status: ‘fulfilled’, value })) .catch(reason ({ status: ‘rejected’, reason })) )); } allSettled(requestPromises).then(results { const fulfilled results.filter(r r.status ‘fulfilled’); const rejected results.filter(r r.status ‘rejected’); console.log(成功: ${fulfilled.length}, 失败: ${rejected.length}); // … 进一步处理 });4.3 常见问题排查实录问题1脚本执行超时Runner 提前结束。现象测试运行一段时间后突然停止控制台显示脚本执行错误或超时。排查检查单次迭代并发量是否过大。先降到10试试。检查目标服务器是否响应缓慢或宕机导致每个请求的等待时间过长累积起来超过了 Postman 脚本的超时限制。在 Pre-request Script 中为pm.sendRequest设置明确的超时时间timeout属性避免单个请求挂起太久。const req { url: ‘...‘, method: ‘GET’, timeout: 10000 // 10秒超时 };解决降低并发数增加请求超时设置确保服务器状态正常。问题2控制台看到请求成功但 Tests 脚本里的断言没执行或失败。现象Postman Console 里能看到所有并发请求的响应码是200但测试结果面板显示测试失败或没有测试条目。排查最常见的原因是Tests 脚本执行时机问题。Pre-request Script 中的setTimeout将请求推入异步队列后Pre-request Script 本身很快就执行完毕了。接着 Postman 会立即发送该请求本身的 HTTP 请求如果URL不是空的然后立即执行 Tests 脚本。此时那些异步并发请求可能还没有完成Promise.all等待的数组还是空的或者未完成的状态。检查globalThis.concurrentPromiseArray是否在 Tests 脚本执行时已经被正确赋值。有可能因为脚本执行顺序问题Tests 脚本读取到的是空数组或上一轮迭代的旧数据。解决确保“控制请求”本身没有实质网络请求将其 URL 设置为一个本地或极速响应的端点如https://postman-echo.com/delay/0或者直接设为about:blank让它的网络耗时几乎为0给 Tests 脚本留出更多的“等待”时间。但这并非根本解决。采用更可靠的同步机制这是更根本的解法。我们可以在 Pre-request Script 中不立即将 Promise 推入全局数组而是先收集所有要发送的请求配置。然后在 Tests 脚本中再去真正地、并发地发送这些请求并处理结果。这样Tests 脚本就成为了整个并发测试的“总控中心”时序更清晰。// Pre-request Script - 只准备数据 const requestConfigs […]; // 构造请求配置数组 pm.environment.set(“concurrentRequestsConfig”, JSON.stringify(requestConfigs)); // Tests Script - 发送请求并断言 const configs JSON.parse(pm.environment.get(“concurrentRequestsConfig”)); const promises configs.map(conf new Promise((resolve, reject) { pm.sendRequest(conf, (err, res) { /* … */ }); })); Promise.all(promises).then(/* … */);这种方式逻辑更清晰强烈推荐。问题3如何模拟混合场景读/写并发需求不仅要并发查询还要模拟部分用户并发提交数据如创建订单。实现在构造requestConfigs数组时随机混入不同方法和参数的请求即可。const operations [ { method: ‘GET’, url: ${baseUrl}/user/${id} }, { method: ‘POST’, url: ${baseUrl}/order, body: { mode: ‘raw’, raw: JSON.stringify({userId: id, product: ‘item’}) } } ]; requestConfigs []; for(let i0; iconcurrentCount; i) { const op operations[Math.floor(Math.random() * operations.length)]; // 动态替换id等参数… requestConfigs.push(op); }5. 与专业工具对比及适用场景总结通过上面的拆解我们已经可以用 Postman 脚本实现一个有模有样的并发测试了。最后我们来系统性地对比一下这种方案与 JMeter 的差异明确它的最佳应用场景。优势零成本、快速启动无需安装新软件利用现有的 Postman 环境和熟悉的 JavaScript 语法即可开始。与功能测试无缝集成你的并发测试脚本可以和已有的功能测试集合放在一起共享环境变量、身份认证和请求配置维护起来方便。灵活性高JavaScript 带来了无限的可能性。你可以轻松地实现复杂的动态数据生成、逻辑判断和结果处理这些在 JMeter 中可能需要借助 BeanShell 或 Groovy语法上对测试人员可能更友好。适合验证性并发测试对于“这个接口能不能抗住瞬间 50 个请求”这类问题可以快速给出答案。劣势与局限规模与稳定性不适合大规模、长时间的压测。Postman 本身不是为压测设计的缺乏资源监控服务器 CPU、内存、TPS 等脚本执行有资源限制结果数据的收集和展示也比较原始。缺乏专业压测元素没有 JMeter 那样的线程组模型、精确的定时器如常数吞吐量定时器、丰富的监听器图形化报告、聚合报告和分布式测试能力。结果分析能力弱需要自己写脚本统计和分析结果无法生成开箱即用的专业压测报告。因此我的建议是用 Postman 脚本并发当你需要进行快速验证、探索性测试、小规模并发验证、或者在与 CI/CD 集成的 API 测试流水线中增加一个简单的并发检查点时。用 JMeter当你需要进行正式的负载测试、压力测试、稳定性测试需要模拟复杂的用户行为模型、控制精确的吞吐量、生成专业的测试报告或者测试规模较大数百上千并发以上时。掌握 Postman 的这项隐藏技巧并不是为了取代谁而是为了在你的测试工具箱里多添一件趁手的“瑞士军刀”。它让你在关键时刻多一种选择也让你对“并发”这一概念的理解从工具的使用层面深入到代码实现的层面。下次当你需要快速验证一个接口的并发能力时不妨先别急着打开 JMeter试试在 Postman 里写几行脚本或许会有意想不到的收获。