APIPOST 8.x 脚本实战:循环调用接口实现OA流程自动化测试
1. 为什么我们需要用脚本循环测试OA流程?
上周,我遇到了一个挺头疼的问题。我们公司用的那个OA系统,时不时会冒出一个诡异的Bug:一个审批流程走到某个节点就卡住了,或者处理人的信息显示不对。这个问题不是每次必现,属于那种“薛定谔的Bug”,你认真去测的时候它不出现,一上线用户用了,它就冒出来了。为了能稳定复现这个问题,我得模拟一个完整的、有多人审批的流程,并且要反复跑很多遍。
最开始,我想到的是用Jmeter。但说实话,Jmeter装起来有点重,配置也复杂,我就想找个更轻便的工具。正好看到国产的APIPOST,界面挺清爽,就打算试试。手动在界面上点点,调用几个单接口,挺顺利的。但我的需求是自动化、循环地模拟多用户依次审批,这就必须用到它的脚本功能了。
结果,一脚就踩进了坑里。我照着网上搜的APIPOST脚本例子写,死活通不过Token校验,一直报301重定向到登录页。后来我才搞明白,网上很多所谓的“APIPOST脚本示例”,其实都是Postman的脚本!更有意思的是,我问APIPOST自带的AI助手,它给我的代码也是Postman风格的。客服直接告诉我:APIPOST的脚本引擎兼容Postman的语法。我把Postman的pm.*那一套写法搬过去,果然就跑通了。
这件事给我的启发是:第一,APIPOST在脚本生态上可能还在成长,直接搜到的中文资料不多,但兼容Postman是个巨大的优势,意味着Postman海量的脚本经验和代码可以直接复用。第二,对于OA流程测试这种强状态依赖、多步骤串联的场景,图形化点击测试效率太低,必须用脚本把逻辑串起来,实现自动化循环。这不仅能用于追查偶发Bug,更是做回归测试、压力测试的必备技能。
所以,这篇文章我就把自己趟出来的路总结一下,重点就是:如何在APIPOST 8.x里,用JavaScript脚本写一个循环,自动完成“查询当前处理人 -> 模拟该用户登录 -> 审批 -> 提交 -> 登出 -> 轮到下一个人”这个完整链条。 即使你之前没怎么写过测试脚本,跟着我的思路和代码,也能一步步实现。
2. 环境准备与核心思路拆解
在动手写代码之前,我们得先把“战场”布置好,并且理解我们要打的这场“仗”具体是怎么个打法。
2.1 APIPOST环境配置要点
首先,你需要安装APIPOST 8.x版本。安装过程很简单,去官网下载就行。打开后,我建议你先熟悉两个核心区域:
-
环境变量管理:在左侧边栏或者顶部能找到“环境管理”的入口。这是我们脚本的“配置中心”。像服务器的基准地址(
base_url)、流程ID(flowId)这些每次运行都可能变的值,一定要放在环境变量里,而不是硬编码在脚本中。比如,我创建了一个叫“OA测试环境”的环境,里面预置了:base_url:http://your-oa-server.comflowId: (可以先留空,运行脚本前手动填入)JSESSIONID和cwbbs.auth: 这两个是登录后的Cookie,脚本会自动更新它们。
-
脚本编辑区:在接口的“预执行脚本”或“后执行脚本”标签页里,就能写我们的JavaScript代码了。对于我们要做的循环测试,更常见的做法是创建一个“测试用例”,或者直接在一个“空白请求”的脚本栏里写主函数。
一个关键技巧:APIPOST兼容Postman的pm.environment对象。这意味着你可以用pm.environment.get("变量名")来读取环境变量,用pm.environment.set("变量名", "值")来设置或更新环境变量。这是我们能在不同用户登录间传递和切换Cookie的关键。
2.2 OA流程自动化测试的核心逻辑
我们的目标脚本,其实是在模拟一个“机器人”,这个机器人非常守规矩:
- 侦察兵:先问系统:“当前这个流程(
flowId)卡在谁手里了?”(调用查询当前处理人接口)。 - 伪装者:拿到处理人信息(比如用户名
userName)后,立刻“变成”这个人。也就是用这个人的账号密码去登录(调用登录接口),并获取到属于这个人的会话Cookie(JSESSIONID等)。 - 办事员:以这个人的身份,去执行审批操作。这通常可能包含两步:先“保存”一下审批表单(
保存流程),再“提交”到下一个节点(提交流程)。 - 清洁工:办完事,立刻“退出登录”(调用
登出接口),清空当前身份,为下一个人的操作做准备。 - 循环判断:提交后,流程就到了下一个人手里。于是“侦察兵”再次出动,查询新的当前处理人。如果还能查到,就回到第2步,继续循环。如果查不到了(
ret不为1),说明流程已经走完,循环结束。
你看,整个流程就像一场接力赛,脚本扮演的机器人不断从当前跑者手中接过接力棒(流程),跑完自己那段,再交给下一个跑者。而循环的终止条件,就是“侦察兵”发现没有下一个接棒的人了。这个逻辑完美解决了“流程节点数动态变化”的测试难题——我们不需要事先知道要循环几次,让脚本自己判断就好。
3. 从零开始编写主循环控制函数
理解了逻辑,我们开始写最核心的“大脑”——主循环函数。我会把代码拆开,一段一段解释。
3.1 搭建主循环骨架
我们先定义一个异步函数,因为网络请求都是异步操作。函数里先把必要的环境变量都取出来。
// 定义一个异步函数来处理循环
async function loopRequests() {
try {
// 1. 从环境变量获取基础配置
let baseUrl = pm.environment.get("base_url");
if (!baseUrl) {
throw new Error('base_url 环境变量未设置');
}
let jsessionId = pm.environment.get("JSESSIONID");
let cwbbsAuth = pm.environment.get("cwbbs.auth");
let flowId = pm.environment.get('flowId');
// 简单的日志输出,调试时非常有用
console.log('初始JSESSIONID:', jsessionId);
if (!flowId) {
throw new Error('flowId 环境变量未设置');
} else {
console.info('开始处理流程flowId:' + flowId);
}
// 2. 初始化循环控制变量
let hasMore = true; // 是否还有下一个处理人
let count = 0; // 循环计数器,防止死循环
const MAX_LOOP = 50; // 最大循环次数,安全阀
// 3. 主循环:当还有处理人且未超限时继续
while (hasMore && count < MAX_LOOP) {
console.log(`\n=== 正在发起第 ${count + 1} 次循环 ===`);
// 这里将插入具体的步骤代码
}
console.log('流程自动化测试执行完毕。');
} catch (outerError) {
console.error('loopRequests 函数发生错误:', outerError.message);
// 可以把错误信息也设置到环境变量,方便在界面上查看
pm.environment.set("LAST_ERROR", outerError.message);
throw outerError; // 重新抛出,让外层知道失败了
}
}
这段代码搭建了一个安全的循环框架。hasMore是核心开关,count和MAX_LOOP是安全措施,防止因为逻辑错误或接口异常导致脚本无限循环。在实际的OA系统中,一个流程的节点数一般不会超过50个,这个值可以按需调整。
3.2 填充循环体:查询与条件判断
现在,我们在while循环体里,实现最关键的“侦察兵”步骤——查询当前处理人,并根据结果决定下一步。
while (hasMore && count < MAX_LOOP) {
console.log(`\n=== 正在发起第 ${count + 1} 次循环 ===`);
try {
// 步骤1: 查询当前流程最后处理人员
const queryUrl = baseUrl + '/flow/getLastHandlingPersonnel';
const headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Cookie': 'JSESSIONID=' + jsessionId + ';cwbbs.auth=' + cwbbsAuth
};
const body = `flowId=${flowId}`;
const queryResponse = await pm.sendRequest({
url: queryUrl,
method: 'POST',
header: headers,
body: { mode: 'raw', raw: body }
});
const responseText = String(queryResponse.stream);
const responseData = JSON.parse(responseText); // 假设返回标准JSON
console.log(`第 ${count + 1} 次查询结果:`, JSON.stringify(responseData));
// 步骤2: 判断查询结果
if (responseData.ret == 1 && responseData.data) {
// ret为1且有数据,表示有处理人,需要继续审批
console.log(`当前处理人: ${responseData.data.userRealName} (${responseData.data.userName}),开始模拟处理...`);
// 这里依次调用登录、保存、提交、登出等子函数
await login(responseData.data);
await saveFlow(responseData.data);
let nextPerson = await moveNextPerson(responseData.data);
await finish(responseData.data, nextPerson);
await logout();
console.log(`第 ${count + 1} 次处理完成。`);
count++; // 成功完成一次,计数器+1
} else if (responseData.ret == 0) {
// ret为0,通常表示流程已结束或没有待办
console.log('查询返回ret=0,流程可能已结束。响应:', responseData);
hasMore = false; // 终止循环
} else {
// 其他未知的ret状态,也终止循环,避免异常
console.warn('收到未知的查询状态码,停止循环。响应:', responseData);
hasMore = false;
}
} catch (innerError) {
// 任何一步出错,都记录日志并停止循环,方便排查
console.error(`第 ${count + 1} 次循环失败:`, innerError.message);
console.error('错误详情:', innerError.stack);
hasMore = false;
// 可以选择是否抛出错误,让整个脚本停止
// throw innerError;
}
}
这里有几个实战细节:
- Cookie的传递:查询接口本身也需要携带Cookie(初始的或上一轮登录留下的),否则系统不知道你在查哪个流程。
- 响应解析:我用
JSON.parse,但原代码用了JSON5,后者能解析不那么标准的JSON(比如带注释的)。如果你的接口返回非常规范,用JSON就行;如果不确定,用JSON5更保险,记得在预执行脚本里用require引入(APIPOST支持)。 - 错误处理:每个循环步骤都用
try...catch包住。一旦出错,立刻记录并停止循环,而不是让脚本在错误状态下继续跑,这样能快速定位问题点。
4. 关键子函数详解:登录、保存与提交
主循环控制着流程,而具体的“体力活”则由各个子函数完成。这几个函数是OA流程测试中最具挑战性的部分,因为涉及会话管理和复杂的表单提交。
4.1 登录函数:如何捕获并更新Cookie
登录是整个脚本的钥匙。不仅要成功登录,更重要的是从登录响应中提取新的Cookie,并更新到环境变量中,供后续接口使用。
async function login(userData) {
try {
console.log(`开始模拟用户 [${userData.userRealName}] 登录...`);
const baseUrl = pm.environment.get("base_url");
const loginUrl = baseUrl + '/doLogin.do';
// 构建登录请求体。注意:密码(wdmm)通常是加密的,你需要从浏览器抓包获取,或找开发确认加密方式。
// 这里是一个示例,实际值必须替换。
const loginBody = `name=${userData.userName}&wdmm=ENCRYPTED_PASSWORD_HERE&signature=`;
const loginResponse = await pm.sendRequest({
url: loginUrl,
method: 'POST',
header: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: { mode: 'raw', raw: loginBody }
});
const loginResult = JSON.parse(String(loginResponse.stream));
if (loginResult.ret != '1') {
throw new Error(`登录失败: ${loginResult.msg}`);
}
console.log('登录接口调用成功。');
// **关键步骤:提取响应头中的Set-Cookie**
const responseHeaders = loginResponse.headers;
let newJsessionId = '';
let newCwbbsAuth = '';
responseHeaders.each(header => {
if (header.key.toLowerCase() === 'set-cookie') {
const cookieStr = header.value;
// 使用正则表达式匹配Cookie值
const jsessionIdMatch = cookieStr.match(/JSESSIONID=([^;]+)/);
if (jsessionIdMatch) {
newJsessionId = jsessionIdMatch[1];
pm.environment.set("JSESSIONID", newJsessionId);
console.log('✅ 已更新环境变量 JSESSIONID');
}
const authMatch = cookieStr.match(/cwbbs\.auth=([^;]*)/);
if (authMatch) {
newCwbbsAuth = authMatch[1] || '';
pm.environment.set("cwbbs.auth", newCwbbsAuth);
console.log('✅ 已更新环境变量 cwbbs.auth');
}
}
});
if (!newJsessionId) {
console.warn('警告:未从响应中解析到新的JSESSIONID,后续接口可能失败。');
}
return { jsessionId: newJsessionId, cwbbsAuth: newCwbbsAuth };
} catch (error) {
console.error('登录函数出错:', error.message);
throw error; // 将错误抛给主循环处理
}
}
踩坑提醒:密码加密是登录最大的坑。wdmm参数的值很少是明文,通常是前端用某种算法(如RSA、AES或自定义算法)加密后的字符串。你有两个选择:1) 让开发提供一个测试账号的固定加密密码;2) 从浏览器开发者工具的网络请求中,复制一次成功登录的请求参数。我强烈推荐第一种,因为第二种抓取的密码可能有过期时间。
4.2 保存与提交函数:处理Multipart表单
OA系统的审批接口常常使用multipart/form-data格式来提交表单数据,这种格式比普通的x-www-form-urlencoded复杂,因为它包含边界符。手动拼接这个格式很痛苦,但理解了就很简单。
async function saveFlow(flowData) {
const baseUrl = pm.environment.get("base_url");
const jsessionId = pm.environment.get("JSESSIONID");
const cwbbsAuth = pm.environment.get("cwbbs.auth");
const flowId = pm.environment.get('flowId');
const url = baseUrl + '/flow/finishAction.do';
// 1. 生成一个随机的边界字符串
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
const crlf = '\r\n'; // 换行符必须是\r\n
let body = '';
// 2. 辅助函数:添加一个表单字段
const addField = (name, value) => {
body += `--${boundary}${crlf}`;
body += `Content-Disposition: form-data; name="${name}"${crlf}${crlf}`;
body += `${value}${crlf}`;
};
// 3. 添加所有需要的字段
addField('isUseMsg', 'true');
addField('flowAction', '');
addField('cwsWorkflowTitle', '自动化测试流程标题');
addField('flowId', flowId);
addField('actionId', flowData.actionId);
addField('myActionId', flowData.myActionId);
addField('op', 'saveformvalueBeforeXorCondSelect');
// ... 这里省略了几十个其他字段,你需要根据实际接口文档或抓包数据补全
addField('bmjlyj', '同意'); // 例如,部门经理意见填“同意”
// 4. 添加结束边界
body += `--${boundary}--${crlf}`;
// 5. 设置正确的Content-Type头,必须包含boundary
const headers = {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Cookie': `JSESSIONID=${jsessionId}; cwbbs.auth=${cwbbsAuth}`
};
const response = await pm.sendRequest({
url,
method: 'POST',
header: headers,
body: { mode: 'raw', raw: body }
});
const result = JSON.parse(String(response.stream));
if (result.ret == '1') {
console.log('✅ 保存成功');
return result;
} else {
throw new Error(`保存失败: ${result.msg}`);
}
}
关键点:
boundary是分隔表单不同部分的唯一字符串,开头和结尾的--不能少。- 每个字段的格式是固定的:以
--boundary开头,接着是描述头,两个换行后是字段值。 - 整个请求体的最后一行必须是
--boundary--。 Content-Type头必须明确指定boundary的值,且要和请求体里用的一致。
finish(提交流程)函数的构造逻辑和saveFlow几乎一模一样,只是op等少数参数的值不同。在实际操作中,最笨但最有效的方法就是:用浏览器正常操作一次审批,用开发者工具的网络面板(Network)把“保存”和“提交”两个请求抓下来,直接复制它们的请求体(Payload),然后替换其中动态的部分(如flowId, actionId, userName等)。这样可以确保字段一个不差。
4.3 登出与移动函数
这两个函数相对简单。
logout函数通常是一个GET请求,调用后服务端会使当前会话失效。执行它是个好习惯,确保每个用户的会话是独立的。
moveNextPerson函数在有些OA系统中用于在提交前,确定或选择下一个处理节点。它可能返回下一个节点的标识(如nextInternalName),这个标识需要传递给finish函数。它的实现就是一个普通的POST请求,构造方式类似登录接口。
5. 脚本调试技巧与Postman兼容性实战
写这么长的脚本,一次跑通是小概率事件。调试是必修课。
5.1 APIPOST脚本调试三板斧
- 善用console.log:这是你最好的朋友。在每个关键步骤、函数入口、获取到重要变量后,都
console.log一下。APIPOST的“控制台”标签会输出所有日志,你能清晰地看到执行流和当时的变量值。比如,在登录后打印一下新获取的Cookie,在发送复杂表单前打印一下请求体前100个字符。 - 分步执行与断点:不要一上来就跑整个循环。先单独测试
login函数。在APIPOST里,你可以创建一个临时请求,在“预执行脚本”里调用login函数,并手动设置好userData对象,然后运行这个请求,看控制台和响应。确保登录成功了,再测试保存,最后再把它们串起来。 - 环境变量监视器:APIPOST界面可以实时显示当前环境变量的值。在脚本运行过程中,观察
JSESSIONID和cwbbs.auth的变化,能直观地判断登录、登出是否生效。
5.2 深入理解Postman兼容性
我最初遇到的“301重定向”问题,根源就在于没理解APIPOST的兼容策略。APIPOST的脚本引擎,在语法层面几乎完全兼容Postman的pm.* API。这意味着:
pm.sendRequest: 用于发送请求,用法和Postman一致。pm.environment.get/set: 环境变量操作,完全一致。pm.response.*: 在“后执行脚本”中处理响应,完全一致。pm.*其他对象如pm.variables,pm.iterationData等,在APIPOST中也可能有对应实现或类似物。
所以,当你搜索资料时,直接搜“Postman脚本如何做XXX”,找到的代码大概率可以直接或稍作修改就在APIPOST里运行。 这是一个巨大的优势,因为Postman的社区资源和解决方案非常丰富。例如,处理Cookie、处理多种响应格式、编写复杂的断言测试,都可以参考Postman的最佳实践。
但是,也要注意细微差别。比如,APIPOST可能有自己独有的内置函数或对象,或者对某些JavaScript高级语法的支持度不同。当遇到问题时,除了查APIPOST官方文档,一个很实用的思路就是:“如果这是在Postman里,代码该怎么写?” 然后把它移植过来试试。
6. 进阶:让脚本更健壮、更通用
写完能跑的脚本只是第一步,要让它在实际项目中可靠地运行,还需要做一些加固和优化。
6.1 错误处理与重试机制
网络抖动、服务短暂不可用都可能导致单次请求失败。对于自动化测试脚本,我们不应该因为一次偶然失败就全盘停止。可以给关键请求(如查询、登录、提交)增加简单的重试逻辑。
async function sendRequestWithRetry(requestOptions, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await pm.sendRequest(requestOptions);
// 可以在这里检查HTTP状态码或业务码,决定是否重试
if (response.code === 200) {
return response;
} else {
// 如果是业务逻辑错误(如密码错),通常不应重试
console.warn(`请求业务失败,不重试。状态码: ${response.code}`);
return response;
}
} catch (error) {
lastError = error;
console.warn(`第 ${i + 1} 次请求失败,${i < maxRetries - 1 ? '准备重试' : '已达重试上限'}:`, error.message);
if (i < maxRetries - 1) {
// 等待一段时间再重试,例如1秒、2秒、4秒的指数退避
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
throw lastError; // 重试多次后仍失败,抛出最后的错误
}
然后在主循环中,用sendRequestWithRetry替代直接的pm.sendRequest。对于登录失败这类错误,重试可能没用,但对于网络超时,重试能显著提高脚本的稳定性。
6.2 参数化与数据驱动
我们的脚本现在硬编码了“同意”这样的审批意见。如果想测试“拒绝”、“转交”等其他场景怎么办?每次都改代码太麻烦。我们可以利用APIPOST的环境变量或全局变量来实现参数化。
- 审批意见参数化:在环境变量里添加
approval_comment和approval_action(值可以是“agree”、“reject”等)。在saveFlow和finish函数里,不再写死“同意”,而是用pm.environment.get("approval_comment")来获取。 - 多流程测试:创建一个
flowIds的数组,放在“预执行脚本”里,然后写一个外层循环,遍历每个flowId来执行主测试逻辑。这样就能用一份脚本批量测试多个流程实例。 - 使用数据文件:APIPOST也支持类似Postman的CSV或JSON数据文件驱动。你可以创建一个CSV文件,每一行是一个测试用例,包含
flowId、expectedFinalStatus等字段。在“集合运行”或“测试用例”中导入这个文件,脚本里通过pm.iterationData来获取当前行的数据,实现真正的数据驱动测试。
6.3 性能考量与优化
当你要循环测试一个很长的流程(比如20个节点),或者想模拟压力测试时,性能就需要注意了。
- 减少不必要的等待:脚本中的
await是顺序执行的。检查一下,logout(登出)后是否需要立即login(下一个用户登录)?有些系统登出后会话立即失效,没问题。但有些系统可能有短暂延迟,可能需要加一个小的sleep(用setTimeout实现),但这会拖慢整体速度。需要根据系统实际情况调整。 - 控制并发:目前的脚本是严格串行的,一个用户办完,下一个用户才能开始。这符合真实审批场景。但如果你要模拟的是“多个独立流程同时被发起”的压力场景,就需要编写更复杂的脚本,可能用到
Promise.all来并发发送请求。不过要注意,这可能会对测试服务器造成较大压力,也可能因为会话冲突而失败。 - 日志输出优化:在循环次数很多时,
console.log输出大量信息可能会影响APIPOST客户端的性能。可以考虑在脚本开头设置一个DEBUG = true的开关,只有调试时才输出详细日志,正式运行时只输出关键节点信息。
写完这个完整的OA流程自动化测试脚本,我感觉APIPOST在轻量级API测试和自动化方面确实有它的优势。图形化界面足够直观,脚本能力又借助Postman兼容性站在了巨人的肩膀上。对于测试同学或者后端开发自测来说,用它来构建这种中等复杂度的业务流程自动化测试,学习成本比Jmeter低,灵活性又比纯界面操作高。当然,最大的成就感还是来自于看到脚本自动跑通一个十几节点的长流程,那种“机器替我把脏活累活干了”的感觉,确实很爽。下次你们遇到需要反复测试的复杂业务流程,不妨也试试用APIPOST脚本把它自动化掉,真的能省下大把时间。
更多推荐



所有评论(0)