LoadRunner12性能测试:关联与断言实战,攻克Token处理难题 1. 项目概述为什么LoadRunner的关联与断言是性能测试的“任督二脉”如果你做过性能测试尤其是用LoadRunner这类老牌工具肯定遇到过这样的场景脚本回放时前一个请求返回的动态数据比如一个订单ID、一个会话Token必须原封不动地塞到下一个请求里否则脚本立马“跑飞”。又或者服务器返回了一堆数据你怎么快速判断这次请求到底是成功还是失败返回的数据结构对不对这两个核心痛点对应的就是LoadRunner脚本开发中的两大基石技术关联Correlation和断言Validation。特别是当现代应用普遍采用Token如JWT进行身份认证和状态保持时处理不好Token的关联你的性能测试脚本连登录都过不去更别提模拟真实用户压力了。很多人觉得LoadRunner12界面老旧学习曲线陡但恰恰是这些“老古董”里的基本功决定了你脚本的健壮性和测试结果的可靠性。一个只会录制-回放的测试工程师和一个能熟练处理动态数据、精准校验响应的工程师产出的脚本价值天差地别。今天我就结合十多年的踩坑经验把LoadRunner12里关联与断言的实战技巧尤其是令人头疼的Token处理掰开揉碎了讲清楚。无论你是刚接触性能测试的新手还是想深化脚本开发的老手这篇都能让你避开我当年走过的弯路。2. LoadRunner12关联功能深度解析从原理到精准捕获关联说白了就是“抓取”和“替换”。在录制脚本时客户端你的浏览器或应用与服务器之间交互的动态值如Session ID、CSRF Token、订单号会被LoadRunner录制下来成为一个固定的“硬编码”。回放时服务器会生成新的动态值如果脚本还用旧的自然就会失败。关联的作用就是在回放时自动从服务器响应中找出这些新生成的动态值并用它们替换脚本中旧的硬编码值。2.1 关联的工作原理与核心类型LoadRunner的关联引擎工作流程可以概括为在录制时标记可能需要关联的数据或回放失败后自动检测在回放时根据预定义的规则左右边界在服务器响应中搜索匹配的数据并将其保存到一个参数中最后在后续请求中用这个参数值替换原来的硬编码。主要关联类型有三种自动关联Auto CorrelationLoadRunner内置了一些常见规则如Web、Oracle NCA等能在录制或回放分析后自动建议关联。对于新手或标准应用这是快速上手的好帮手。但它的“智商”有限对于自定义的、格式复杂的动态数据尤其是JSON或XML深层嵌套的数据经常识别不出来或关联错误。手动关联Manual Correlation这是资深测试工程师必须掌握的技能。通过对比录制和回放的日志找到那些变化的值然后手动编写关联函数主要是web_reg_save_param_ex或更早的web_reg_save_param来捕获它。虽然繁琐但精准可控。基于规则的关联Rule-Based Correlation你可以自定义关联规则告诉LoadRunner“凡是符合这种格式的数据都自动给我关联上”。这在测试具有统一数据格式的API时非常高效。注意不要过度依赖自动关联。对于关键业务流尤其是涉及金钱、订单的状态一定要手动验证关联是否正确。我曾见过一个电商项目自动关联错了一个金额参数导致压测时所有订单都以1分钱成交差点造成重大事故。2.2 手动关联的黄金四步法与实战示例手动关联是核心技能我将其总结为“找、定、写、验”四步法。我们以一个获取用户列表后需要关联用户ID来查看详情的简单场景为例。步骤一找差异Find the Difference首先你需要录制两遍完全相同的业务流脚本。然后使用LoadRunner的“对比脚本”功能或者直接打开两个脚本的Generation Log生成日志进行肉眼比对。寻找那些在两次录制中值不同但位置和格式相似的字符串。在我们的例子中假设第一次录制返回的用户ID是userId: 1001第二次变成了userId: 1005。这个1001和1005就是需要关联的目标。步骤二定边界Define Boundaries找到目标数据后要确定它的“左边界”和“右边界”。边界应该是目标数据前面和后面那些固定不变的文本。原则是边界要尽可能唯一以提高匹配精度但又不能过长或过短。对于userId: 1001左边界可以是userId: 右边界可以是。更精确的左边界可以考虑userId: 右边界为。但要小心响应数据里有转义字符比如userId\: \1001\。步骤三写函数Write the Function在脚本中将关联函数放在获取该数据的请求之前。因为关联函数是“注册型”函数它告诉LoadRunner“下一个请求返回后请按照这个规则去抓数据”。// 使用 web_reg_save_param_ex 函数功能更强大 web_reg_save_param_ex( ParamNameCorr_UserId, // 参数名后续用{Corr_UserId}引用 LB\userId\: \, // 左边界 RB\, // 右边界 Ordinal1, // 取第1个匹配项 SearchBody, // 在响应正文中搜索 RelFrameId1, // 相对帧ID一般主页面为1 LAST); web_url(api/getUserList, URLhttp://example.com/api/users, ...);关键参数解析Ordinal: 如果响应中有多个匹配项此参数指定取第几个。OrdinalALL会保存所有匹配到一个数组中。Search: 指定搜索范围Headers头信息、Body正文常用、Noresource非资源体。RelFrameId: 用于处理HTML框架页现代单页应用SPA或纯API测试通常设为1。步骤四验结果Verify the Result回放脚本并打开回放日志Replay Log设置为“扩展日志”并勾选“参数替换”。查看日志中{Corr_UserId}是否被成功替换为新的值如1005以及后续使用该参数的请求是否发送正确。2.3 处理复杂响应JSON、XML与嵌套结构现代API响应以JSON为主手动解析JSON数据是必备技能。假设服务器返回一个复杂的JSON{ code: 0, data: { items: [ {id: 101, name: Alice}, {id: 102, name: Bob} ], token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... } }如果你想关联token或者关联第二个用户的id102简单的左右边界可能因为结构变化而失效。这里有更高级的技巧使用web_reg_save_param_json函数LoadRunner 12.55这是处理JSON的利器。web_reg_save_param_json(ParamNameJsonToken, QueryString$.data.token, // JSONPath表达式 SelectAllYes, LAST);使用$.data.token这个JSONPath表达式可以直接精准定位到token节点无视其他无关字段的变化。关联数组中的特定元素对于items数组中的第二个id。web_reg_save_param_json(ParamNameSecondUserId, QueryString$.data.items[1].id, // 索引从0开始 SelectAllYes, LAST);回放后{SecondUserId}的值就是102。当没有JSON函数时如果版本较低只能使用传统的web_reg_save_param_ex这时边界定义要非常小心。对于token可以这样写web_reg_save_param_ex(ParamNameManualToken, LB\token\: \, RB\, Ordinal1, LAST);但这种方法很脆弱如果JSON格式缩进改变比如从开发环境到生产环境可能就会失败。因此尽可能升级到支持JSON关联的版本或与开发团队约定响应的标准格式。3. 断言检查点实战确保每一次请求都“名实相符”关联保证了脚本能“跑通”而断言LoadRunner中常称为“检查点”或“验证点”则保证了脚本“跑对”。它的作用是在回放时检查服务器的响应是否包含预期的内容从而判断事务是否成功。没有断言的性能测试就像蒙着眼睛跑步你只知道跑了但不知道方向对不对。3.1 文本检查点与图像检查点的应用场景文本检查点web_reg_find最常用、最灵活的断言方式。它也是一个注册函数需要在请求前定义用于在后续请求的响应中查找特定文本。// 检查登录后页面是否包含“欢迎回来”字样 web_reg_find(FailNotFound, // 如果找不到则失败 SearchBody, Text欢迎回来, LAST); web_submit_data(login.pl, ...);重要参数Fail: 可选NotFound找不到则失败或Found找到则失败用于负面测试。Text: 要查找的文本支持简单通配符。SaveCount: 将匹配次数保存到一个参数中可用于更复杂的逻辑判断。图像检查点web_image_check用于验证页面上的特定图片是否加载。这在测试GUI应用或需要验证验证码当然压测时通常会屏蔽验证码的场景下有用。但注意它比较消耗资源在大型压力测试中谨慎使用。web_image_check(web_image_check, Alt公司Logo, // 通过图片的Alt属性识别 LAST);3.2 断言的最佳实践与常见陷阱断言内容要具有业务代表性不要检查“登录成功”页面上的一个无关紧要的静态文本如版权信息。要检查能唯一标识业务操作成功的内容例如支付成功后的“订单号”、提交表单后的“操作成功”提示、查询结果中的特定数据条目。断言位置要准确web_reg_find是注册函数必须放在它要检查的那个请求之前。一个常见的错误是把它放在请求之后这样是无效的。善用SaveCount进行复杂校验比如检查一个商品列表页要求返回至少10条商品。web_reg_find(SaveCountProductCount, SearchBody, Textdiv class\product-item\, // 假设每个商品项都有这个div LAST); web_url(productList, ...); // 后续可以通过lr_eval_string(“{ProductCount}”)获取计数值并配合if语句判断避免过于脆弱的断言不要断言完全动态、不可预测的文本如精确的时间戳“2023-10-27 14:35:02”。可以断言部分固定文本或使用正则表达式匹配模式。全局断言与局部断言对于所有请求都需要的检查如页面没有“系统错误”字样可以在vuser_init部分设置全局查找。对于特定业务步骤的检查则在对应请求前设置局部断言。4. Token处理专题应对现代认证机制的核心挑战在现代Web和APP性能测试中Token尤其是JWT的处理是关联技术皇冠上的明珠。流程通常是先调用登录接口从响应中获取Token然后在后续所有需要认证的请求的Header通常是Authorization: Bearer Token中带上这个Token。4.1 Token的捕获与关联假设登录接口返回如下JSON{access_token: eyJ...xxx, expires_in: 3600, token_type: Bearer}我们需要关联access_token。方法一使用JSON关联推荐web_reg_save_param_json(ParamNameAccessToken, QueryString$.access_token, SelectAllYes, LAST); web_submit_data(login.api, Actionhttp://api.example.com/login, MethodPOST, Bodyusername{Uname}password{Pwd}, LAST);捕获后Token值就保存在{AccessToken}参数中。方法二使用常规关联如果无法使用JSON函数则需要仔细分析响应体。注意JSON中的字符串值在HTTP响应流中可能被转义或格式化。你需要查看“原始”响应日志来确定精确的边界。// 查看原始日志后发现响应是紧凑型JSON{access_token:eyJ...xxx,...} web_reg_save_param_ex(ParamNameAccessToken, LB\access_token\:\, RB\, Ordinal1, SearchBody, LAST);4.2 在请求中传递Token捕获Token后需要将其添加到后续请求的Header中。这里绝对不能像处理URL参数一样写在Body里。正确做法使用web_add_header函数// 在需要认证的请求前添加Authorization头 char authHeader[1024]; sprintf(authHeader, Bearer %s, lr_eval_string({AccessToken})); web_add_header(Authorization, authHeader); web_url(getUserProfile, URLhttp://api.example.com/profile, ...);关键点web_add_header的作用域是下一个HTTP请求。也就是说它只对其后面紧跟着的第一个web_url或web_submit_data等请求生效。如果后面有多个请求都需要同一个Token你需要在每个请求前都添加一次Header或者将相关请求组织在一起。可以使用lr_save_string将拼接好的Header值先保存到一个参数中方便多次使用。lr_save_string(lr_eval_string(Bearer {AccessToken}), AuthHeaderValue); web_add_header(Authorization, {AuthHeaderValue}); web_url(request1, ...); web_add_header(Authorization, {AuthHeaderValue}); // 需要再次添加 web_url(request2, ...);4.3 处理Token过期与刷新逻辑这是高级场景。很多系统的Token有有效期如expires_in: 3600秒。在长时间运行的稳定性测试或耐力测试中脚本需要处理Token过期并自动刷新。实现思路在登录时同时捕获Token和过期时间。web_reg_save_param_json(ParamNameAccessToken, QueryString$.access_token, ...); web_reg_save_param_json(ParamNameExpiresIn, QueryString$.expires_in, ...); web_submit_data(login, ...); // 将过期时间转换为时间戳保存 lr_save_datetime(%Y-%m-%d %H:%M:%S, DATE_NOW atoi(lr_eval_string({ExpiresIn})), TokenExpireTime);在Action部分开始时检查Token是否即将过期。可以定义一个检查函数在每次使用Token前调用。int checkAndRefreshToken() { // 伪代码逻辑 if (当前时间 TokenExpireTime - 300) { // 提前5分钟刷新 // 调用刷新Token的接口如果有refresh_token机制 // 或者重新登录 // 更新全局的 AccessToken 和 TokenExpireTime 参数 lr_save_string(newToken, AccessToken); lr_save_datetime(..., newExpireTime, TokenExpireTime); return 1; // 已刷新 } return 0; // 未刷新 }在需要认证的请求前调用检查函数。checkAndRefreshToken(); web_add_header(Authorization, lr_eval_string(Bearer {AccessToken})); web_url(someApi, ...);这种逻辑实现起来稍复杂需要用到C语言代码和LoadRunner的日期时间函数但它能让你的脚本真正模拟真实用户的长期会话行为对于发现因Token刷新机制导致的内存泄漏或性能瓶颈至关重要。5. 脚本调试与常见问题排查实录即使你按照最佳实践编写了脚本第一次回放也常常不会一帆风顺。下面是我总结的常见问题排查清单像一本“错题集”供你对照。5.1 关联失败的四大原因及对策问题现象可能原因排查步骤与解决方案错误参数未找到1. 关联函数放错了位置放到了请求之后。2. 左右边界LB/RB定义不准确在回放响应中匹配不到。3. 服务器响应内容与录制时完全不同可能请求就失败了。1.确认位置确保web_reg_save_param函数在目标请求之前。2.检查边界开启“扩展日志”中的“服务器响应”选项查看回放时收到的原始响应数据重新校准LB/RB。特别注意空格、换行符、转义字符如\。3.验证请求先确保发出这个请求本身是成功的检查HTTP状态码。关联到了错误的值1. 边界太宽泛匹配到了多个结果而Ordinal参数设置不对。2. 响应结构变化目标数据位置移动。1.收紧边界使用更独特、更长的左右文作为边界。2.使用Ordinal如果确定要取第N个匹配项设置OrdinalN。用OrdinalALL配合数组查看所有匹配项。3.升级方法对于JSON/XML优先使用web_reg_save_param_json或web_reg_save_param_xpath进行精准定位。关联成功但后续使用报错1. 参数名拼写错误。2. 在参数被赋值之前就尝试使用它。3. 参数值中包含非法字符如引号、换行直接拼接进请求导致语法错误。1.检查拼写确保使用{Param}时名称一致。2.检查顺序确保使用参数的请求在关联该参数的请求之后。3.处理特殊字符使用lr_convert_string_encoding函数对参数值进行URL编码如果需要或使用lr_save_string进行清洗。回放成功但数据不对关联虽然没报错但抓到的值不是业务逻辑需要的那个例如关联到了A记录的ID但业务需要B记录的ID。1.业务验证不要只看脚本是否报错。检查日志中关联出的值在业务上下文中是否合理。例如支付成功后关联的订单号是否真的是一个新生成的、唯一的号码。2.双重检查边界可能是边界匹配到了相似但不正确的文本。5.2 断言不生效的典型场景断言函数位置错误和关联函数一样web_reg_find必须放在被检查的请求之前。文本包含不可见字符要查找的文本可能包含空格、制表符或换行。在定义Text时可以尝试使用Text欢迎回来\n或使用通配符Text欢迎*回来。响应编码问题服务器返回的可能是gzip压缩后的内容或者编码是UTF-8而脚本默认是ANSI。确保运行设置Run-time Settings中的“首选项”-“HTTP”下勾选了“支持字符集”并选择了正确的编码同时可以尝试勾选“将非资源转换为HTML”或“将二进制数据作为资源”。检查点作用域web_reg_find默认只搜索下一个请求的“Body”部分。如果你需要检查所有请求需要在vuser_init中设置或者对每个请求单独设置。5.3 性能测试场景下的脚本优化建议当脚本用于成百上千个虚拟用户并发时一些在单次回放中不明显的问题会被放大。关联和断言的性能开销每一个web_reg_save_param和web_reg_find函数都会增加一些处理时间。在保证必要检查的前提下精简关联和断言的数量。对于不变的静态数据不要做关联对于非关键的成功标识可以不做断言。参数化与关联的结合如果多个用户需要关联不同的值例如每个用户登录后获得自己的Token务必确保关联函数在每次迭代中都能正确执行。通常需要将关联逻辑放在Action部分而不是vuser_init除非Token可以全局共享。Token管理的并发问题在测试需要登录的系统时要模拟真实场景。如果所有用户共用同一个Token不仅不符合实际还可能触发服务器的安全限制如单Token并发数限制。务必为每个虚拟用户或每批用户分配独立的测试账号并让它们各自完成登录、获取Token、使用Token的完整流程。这可以通过参数化用户名密码并将登录和关联Token的步骤放在Action中实现。日志管理在调试阶段开启“扩展日志”是必要的但在正式压测时一定要将其关闭设置为“仅在错误时发送消息”否则巨量的日志会迅速占满磁盘并严重影响LoadRunner Generator自身的性能。6. 超越基础高级技巧与框架性思维当你熟练掌握了单个关联和断言后可以思考如何让脚本更健壮、更易维护。6.1 使用自定义函数封装通用逻辑将重复的、复杂的操作封装成自定义函数放在globals.h中。例如封装一个通用的“登录并获取Token”函数int LoginAndGetToken(char *username, char *password, char *tokenParamName) { int ret 0; // 1. 注册关联函数 web_reg_save_param_json(ParamNameTempToken, QueryString$.access_token, SelectAllYes, LAST); // 2. 发送登录请求这里简化了实际需要参数化URL和Body web_submit_data(login, Action{LoginUrl}, MethodPOST, Bodyusername{username}password{password}, LAST); // 3. 检查登录是否成功简单文本检查 web_reg_find(FailNotFound, Textsuccess, SaveCountLoginCount, LAST); // 注意这个web_reg_find需要放在登录请求前这里仅为示意流程 // 4. 将获取到的Token保存到调用者指定的参数中 if (atoi(lr_eval_string({LoginCount})) 0) { lr_save_string(lr_eval_string({TempToken}), tokenParamName); lr_output_message(登录成功Token已保存至参数 %s, tokenParamName); ret 1; } else { lr_error_message(登录失败); ret 0; } return ret; }在Action中调用LoginAndGetToken(“testUser”, “123456”, “MyAuthToken”);。这样主脚本逻辑会非常清晰。6.2 建立脚本健壮性检查清单在交付一个压测脚本前按照以下清单进行自查[ ]关联检查所有动态数据SessionID, Token, CSRF, 订单号等是否都已正确关联回放日志中是否有“未找到参数”的警告[ ]断言覆盖关键业务步骤登录、提交、查询、支付是否有相应的断言来验证业务成功[ ]参数化用户名、密码、关键查询条件等是否已参数化避免所有用户行为完全一致[ ]思考时间与步调是否设置了合理的思考时间Think Time和步调时间Pacing来模拟真实用户操作间隔[ ]日志级别正式压测运行时设置是否已调整为“仅在错误时”或“无日志”[ ]数据唯一性参数化数据源是否足够能否支撑整个压测时长而不重复[ ]清理与初始化是否有vuser_end部分来清理测试数据如注销、删除测试订单是否有vuser_init部分进行必要的初始化6.3 从工具思维到协议思维最后也是最重要的一点不要被LoadRunner的图形界面和函数束缚。关联的本质是处理HTTP/HTTPS协议层面的动态数据。多使用抓包工具如Fiddler, Wireshark去分析请求和响应的原始报文。当你清晰地看到Authorization: Bearer xxxx这个Header或者看到响应体里token:eyJ...这个字段时你对关联的理解就从“LoadRunner里点哪个按钮”上升到了“网络协议中数据如何传递”的层面。理解了协议即使未来换用JMeter、Gatling或其他任何性能测试工具你都能快速上手因为核心思想是相通的录制或手工构造请求 - 从响应中提取动态数据 - 将数据替换到后续请求中 - 验证响应结果。LoadRunner12的关联与断言是你掌握性能测试脚本开发核心思想的绝佳训练场。把这些基本功打扎实面对更复杂的异步接口、WebSocket、二进制协议时你才会有拆解和解决问题的底气。