做JMeter接口测试和性能测试这些年,我最大的感触是:函数才是脚本里最容易被低估的东西。很多人写接口脚本,动态参数要么写死,要么依赖CSV文件;跑单接口还行,一旦落到性能压测、多用户并发、跨线程组传参,脚本立刻变得僵硬,错误率飙到让人怀疑人生。实际上,JMeter内置了几十种函数,用好了就是测试脚本里的“活数据引擎”,从时间戳到随机数、从属性传参到动态断言,几乎每个环节都能用函数把脚本从“死水”变成“活水”。
这篇文章我打算把常用函数按使用频率拆开讲清楚:每个函数的语法、参数、典型场景、常见坑,以及函数和断言、Beanshell、逻辑控制器怎么搭配。内容不追求把内置函数全部罗列一遍,而是把“真正常用、值得用”的讲透。无论你是刚接触JMeter,还是已经在写压测脚本但经常被函数绕晕,这篇应该都能帮你省下不少排查时间。
1. 先从一次失败的断言说起:测试脚本为何离不开函数
1.1 接口测试中的变量和函数到底什么分工
先弄明白一个基础问题:变量和函数在JMeter里是什么关系。变量是“存值的地方”,比如你在测试计划里定义一个host=api.example.com,整个测试计划都能引用;函数则是“生产值的手段”,比如${__time(yyyy-MM-dd)},每次调用都会动态生成一个时间字符串。
在实际脚本里,两者经常配合使用。你可以把函数的返回值存进变量,也可以把变量作为函数的参数传进去。比如我要构造一个带时间戳的订单号,可以先用${__time(yyyyMMddHHmmss)}生成当前时间,再和随机数拼接到一起。少数刚入门的朋友喜欢把所有参数写死,接口一旦校验唯一性就立刻失败;另一些人又过度依赖CSV文件做参数化,每加一个字段就要维护一列数据,其实很多动态数据用函数一条表达式就搞定了。
我给出的经验是:能通过函数“算出来”的数据,就不要用文件去维护;只有那些需要业务上预先定义、确实需要一一对应的数据(比如一批测试账号、一组商品ID),才值得走CSV参数化。函数负责“即时生成”,CSV负责“按需读取”,分工清楚了脚本逻辑才会干净。
1.2 性能压测中函数的价值:场景模拟靠它撑起来
接口测试阶段,函数更多是解决“参数不能写死”的问题;到了性能压测阶段,函数的意义就完全不一样了。压测的核心是模拟大量虚拟用户同时操作服务端,如果所有虚拟用户提交的请求参数一模一样,那测出来的结果基本没有参考价值。举个例子,注册接口一般会校验手机号是否重复,你用固定手机号并发200个用户,大概率只能测出“这个手机号到底重不重复”这一个结果,而服务端真实的注册处理能力、数据库写入瓶颈、缓存策略完全体现不出来。
函数在压测中的作用,就是让每个虚拟用户拿到“看起来像是独立真实用户”的数据。随机用户名、随机手机号、随机邮箱、动态时间戳、唯一订单号,这些全部可以靠函数组合实现。还有一个容易忽视的点:压测时服务端往往会根据请求来源做缓存或限流,如果所有并发请求的User-Agent、Token、业务ID都相同,服务端可能直接命中缓存或直接拒绝,最终报告里的高吞吐量其实是“缓存撑起来的假象”,和你想要的真实容量评估完全是两回事。
1.3 必须先弄清的JMeter函数通用语法
使用函数之前,通用语法必须先刻在脑子里:${__funcName(参数1,参数2,...)}。注意这里有几个细节很容易踩坑。
第一,函数名前面是两个下划线,不是变量引用。变量引用是${varName}单层写法,函数则是${__xxx(...)}。很多报错排查到最后,发现只是下划线少打了一个。
第二,参数之间的逗号是英文逗号。如果你要传的值本身包含英文逗号,需要用\转义,或者调整写法避免直接传逗号。
第三,部分函数支持“默认值”参数,写空也要保留逗号位置。比如${__P(token,)},第二个参数留空代表取不到属性时返回空字符串,这个逗号不能省。
JMeter自带的函数助手对话框(Tools > Function Helper Dialog)可以帮你选择函数并生成表达式,但要注意:它的作用只是“复制表达式文本”,并不会自动把表达式塞进你的输入框,生成之后还需要手动粘贴。很多人第一次用的时候点了Generate没反应,以为是功能坏了,其实就是这个原因。
2. 高频函数逐个拆:时间、随机、计数、字符串四类最常用
2.1 时间函数三兄弟:__time、__timeShift、__dateTimeConvert
时间类函数是接口测试里出现频率最高的一类,因为几乎所有业务接口都要和时间打交道:订单创建时间、日志时间、签名过期时间、查询起止时间。
${__time(,)}返回的是当前时间的毫秒时间戳,注意第一个参数留空时返回的是13位毫秒时间戳;如果传格式字符串,则返回格式化时间。比如${__time(yyyy-MM-dd HH:mm:ss)}返回2025-01-08 14:30:00这样的字符串。还有一个常见用法是取秒级时间戳:${__time(/1000,)},把毫秒时间戳直接除以1000,在很多后端签名逻辑里,要求的往往是10位秒级时间戳,这个技巧很实用。
__timeShift是另一个高频函数,用来做时间的加减运算。语法是${__timeShift(格式,日期,偏移量,区域)},日期留空表示基于当前时间。这里必须提醒一个特别容易踩的坑:偏移量用的是ISO 8601格式,PT5M才是5分钟,如果写成P5M,那是5个月。我曾经在压测脚本里想生成“5分钟前”的时间,写成了P5M,结果整批数据的时间字段全部晚5个月,服务端直接拒绝了,排查了很久才发现问题。常用写法参考:PT30S是30秒,PT1H是1小时,P2D是2天,P1W是1周。
__dateTimeConvert做的是格式转换,典型场景是接口返回的时间格式和请求需要的格式不一致。比如查询接口返回了2025-01-08 14:30:00,但下一个接口要求传的是20250108143000,一条${__dateTimeConvert(2025-01-08 14:30:00,yyyy-MM-dd HH:mm:ss,yyyyMMddHHmmss,)}就能解决。注意时区参数和默认时区的问题,日期转换的时区逻辑如果不明确,建议先在某一个取样器中用Debug Sampler打印出来看看结果,再决定要不要加时区参数。
2.2 随机和唯一性函数:__Random、__RandomString、__UUID、__threadNum
随机类函数是构造独立用户数据的核心工具。
${__Random(1,1000,)}生成指定范围内的随机整数,闭区间,包含上下限。注册接口生成手机号的时候,我一般用138${__Random(10000000,99999999,)},前缀加8位随机数,比直接${__Random(10000000000,99999999999,)}更符合手机号段规律,也更容易被服务端校验通过。
${__RandomString(8,abcdefghijklmnopqrstuvwxyz0123456789,)}生成指定长度的随机字符串,第二个参数是字符集。比如生成测试邮箱:user_${__RandomString(6,abcdefghijklmnopqrstuvwxyz,)}@example.com,基本可以保证并发压测时不重复。
${__UUID()}生成标准UUID字符串,几乎没有重复可能,适合做订单号、交易号、文件名等强唯一性字段。不过要注意,UUID是36位含连字符的完整格式,如果你只是需要一个不重复的短标识,用__RandomString会更合适,请求体里塞一个大UUID有时候纯粹增加无谓的带宽。
${__threadNum()}返回当前线程编号,在压测场景里特别有用。排查错误响应时,能在请求参数里看到线程编号,配合日志就能快速定位是哪些虚拟用户报错。比如压测时把${__threadNum()}拼在用户名后缀里:user_${__threadNum()}_${__time(MMddHHmmss)},既能保证部分唯一性,又能在结果树里直接看到是第几个线程发起的请求。
2.3 计数器函数:__counter和迭代次数的使用边界
${__counter(TRUE,)}和${__counter(FALSE,)}都是递增计数器,区别在于作用域。TRUE是进程内全局计数,多个线程公用一个计数序列;FALSE是每个线程独立计数,每个线程自己从1开始。
为了说清楚,假设5个线程、每个线程循环2次。用TRUE计数,序列是1、2、3、4、5、6、7、8、9、10,全局不重复;用FALSE计数,每个线程内部两次循环是1、2,5个线程各自都是1、2。
这里还要和${__iterationNum}做一个区分。__iterationNum反映的是当前线程的当前循环次数,在同一线程内和__counter(FALSE)的值往往一致,但它更偏向“循环次数”的语义。什么时候必须用__counter(TRUE)?需要全局流水号的时候,比如生成一批全局订单编号,要保证压测进程内完全不重复,用TRUE模式最直接。需要注意跨压力机场景下,TRUE只保证单机内不重复,分布式压测时还是要靠随机函数或外部数据保证全局唯一。
2.4 字符串与编码类函数:__char、__unescape、__escapeHtml
这一类函数在新手脚本里出现频率不算高,但遇到边界情况时非常省事。
${__char(72,69,76,76,79)}可以把一组Unicode码点转成对应字符,比如这几个数字转出来就是HELLO。接口测试中有些参数需要输入特殊字符(比如换行符、中文符号),在请求体里直接粘贴可能造成编码混乱,用__char生成更可控。
${__unescape(...)}用来反转义HTML实体。比如接口返回了<script>,你想断言它是否等于<script>,直接写死的断言文本怎么都不匹配,此时把响应值过一遍__unescape再比就对了。
${__escapeHtml(...)}则是反向操作,把HTML特殊字符转义成实体。这类函数整体使用率不高,但值得知道存在,因为你不知道哪一天接口测试中就会蹦出一个编码相关的怪问题。真遇到的时候,知道有现成函数可以用,比临时去写JSR223脚本高效得多。
| 函数 | 核心作用 | 常用示例 | 典型场景 |
|---|---|---|---|
__time | 生成时间戳/格式化时间 | ${__time(yyyy-MM-dd HH:mm:ss,)} | 请求时间字段、订单号拼接 |
__timeShift | 时间加减 | ${__timeShift(yyyy-MM-dd HH:mm:ss,,PT5M,)} | 5分钟前的查询时间 |
__Random | 生成随机整数 | ${__Random(1000,9999,)} | 动态数值参数 |
__RandomString | 生成随机字符串 | ${__RandomString(6,abc123,)} | 随机用户名、验证码 |
__UUID | 生成UUID | ${__UUID()} | 订单号、唯一标识 |
__counter | 递增计数 | ${__counter(TRUE,)} | 全局唯一流水号 |
__threadNum | 当前线程编号 | ${__threadNum()} | 压测日志定位 |
3. 跨线程组传参的三件套:__P、__setProperty与__V的实战
3.1 变量与属性:两个容易混淆的数据域
很多人在JMeter里做跨线程组传参时都会遇到一个现象:在线程组A里成功提取了token,在线程组B里用${token}取值,发现是空的。原因在于JMeter的“变量”是线程级的,每个线程组、每个线程都有自己独立的变量副本,线程组之间互不可见。
和变量对应的是“属性”,属性是JVM级别的,全局共享,理论上任何线程组都能读取。打个比方:变量是每个人工位上的便利贴,随手写随手用,别人看不到;属性是公司公共公告栏,谁写谁都能看,但需要主动去读。
跨线程组传参这件事,本质就是“从便利贴搬到公告栏,再在另一个工位去读公告栏”。
3.2 登录token从线程组A传到线程组B的操作套路
令牌传递是压测脚本里最典型的跨线程组场景:先在一个线程组里登录获取token,然后在另一个线程组里用这个token去调业务接口。
标准做法分三步走。第一步,在登录线程组的HTTP请求上添加JSON提取器或正则提取器,把返回的token存到变量里,比如token_var。第二步,添加一个JSR223后置处理器,里面写一行props.put("token", vars.get("token_var")),把变量转成属性。第三步,在目标线程组的请求头或参数里用${__P(token,)}读取属性。
为什么不直接在第二步用${__setProperty(token,${token_var},)}?函数式写法在简单场景下可行,但一旦token值里包含特殊字符、逗号,或者表达式解析顺序出现问题,取出来的值很容易残缺或报错。JSR223脚本用props.put是最直接的、也是我长期在各种项目中验证过的、最不容易出问题的写法。Groovy脚本里能感知到性能上的差异:JSR223的存取效率比函数解析更高,压测环境里尤其明显。
整个链路里最容易出错的节点是启动顺序。JMeter默认按“线程组在测试计划中的排列顺序”启动线程组,如果业务线程组写在了登录线程组前面,token还没生成就被读了,自然为空。所以日志中如果出现“token取不到”的报错,先检查线程组顺序,再看属性名拼写,最后再查后置处理器是否真的执行了。
3.3 嵌套变量求值:__V和__eval的实用场景
${__V(...)}是一个容易被忽视但非常有用的函数,作用是对变量名做二次求值。什么意思?常规写法${user_1}是直接取user_1这个变量的值;假设我们有一组变量user_1、user_2、user_3,想根据当前循环序号动态取其中一个,直接写${user_${i}}是没法解析的,因为JMeter不支持这种嵌套引用。
用${__V(user_${i})}就可以实现动态拼变量名后取值。我第一次用这个函数是在做多账号轮询:从登录接口提取了10个用户的凭证,分别存为cred_1到cred_10,业务线程里每次循环用${__V(cred_${__counter(FALSE,)})}取出当前凭证。这样每个虚拟用户可以在自己的线程内逐步轮换账号,既复用了测试数据,又避免了多线程抢同一份变量。
__V还有一个常见用法是配合CSV文件实现“数据轮询”。很多人不知道,CSV文件读取后得到的变量可以按命名规则存多组,再用__V动态拼索引,比每次从文件里逐行读取灵活得多。
3.4 属性设置时机:setProperty函数的边界与JSR223的取舍
__setProperty函数本身可以用来把变量写入属性,但它有一个比较明显的限制:函数在取样器执行时才会被求值,而且它的返回值本身也会被处理为字符。如果你在某个取样器的参数里写${__setProperty(token,abc,)},JMeter会先执行属性设置,再用函数的“空值”作为参数填充。这个行为容易让参数值变成空白,甚至引起后续请求失败。
更可靠的策略是:把属性写入操作放到JSR223后置处理器里,用Groovy脚本的props.put("key", "value")完成;读取属性则在需要的地方用${__P(key,默认值)}。这样职责清晰:写入用脚本,读取用函数。大规模并发时,JSR223的性能优势也更明显;函数虽然写起来短,但JMeter每次解析函数表达式都有额外开销,热点路径上能省一点是一点。
另外要记住:属性是存储于当前JVM的,分布式压测时,负载机的属性并不互通,每台机器有各自的属性池。如果你要跨负载机传递数据,属性这条路走不通,得借助外部中间件或参数文件。
4. 函数与断言、逻辑控制、脚本协同:别只会拼参数
4.1 断言里的函数:两个时间戳如何比大小
常规的“响应断言”只能做文本匹配,它没法比较数字大小。可实际接口测试里,经常要断言“接口处理耗时是否小于5000毫秒”“返回的时间戳是否晚于当前时间”“分页的总页数是否大于等于10”。这些场景下,函数是响应断言的得力助手。
比较两个时间戳大小,一种常见做法是把时间值提取到变量,然后用${__jexl3(...)}函数做逻辑判断。比如想判断“响应里的时间戳是否晚于当前时间”,可以用${__jexl3(Long.parseLong(vars.get("respTimestamp")) > ${__time(,)},)}。这个表达式里嵌套了两个函数,外层__jexl3负责按Java语法计算布尔值,内层__time负责生成当前毫秒时间戳。
这类嵌套写在响应断言的“匹配测试”里,勾选“字符串”类型后,函数会返回true或false,和期望值true比对即可通过断言。这个方案能用,但可读性确实不高,表达式一旦超过三层嵌套,后面维护的人基本看天书。所以我的建议是:简单的比较逻辑用函数,稍微复杂一点就上JSR223断言,里面写Java/Groovy原生的条件判断,逻辑清晰、报错定位快。函数不是不能做,而是要看值不值。
4.2 逻辑控制器里的函数:随机分流与动态思考时间
接口测试和性能测试里,函数还可以直接驱动逻辑流程,而不只是提供参数值。
随机分流是一个很典型的玩法。假设压测目标是模拟用户行为:70%的用户走“搜索商品”流程,30%的用户走“下单”流程,可以用一个${__Random(1,100,)}生成随机数,再结合Switch控制器做分支选择。Switch控制器可以通过变量值决定执行哪个子逻辑:给每个分支编号1到100,数值落在1到70走搜索,71到100走下单,就能在压测中模拟出按比例分发请求的效果。
另一个非常实用的场景是动态思考时间。性能测试中“思考时间”如果全部固定,会导致虚拟用户行为过于一致,全部用户在同一时刻发请求,测出的峰值明显偏高、整体曲线失真。正确的做法是让思考时间落在合理范围内,${__Random(2000,5000,)}就是一个直接可用的随机化方式。把它填到“固定吞吐量定时器”或“常数吞吐量定时器”的延迟字段里,每个虚拟用户在两次请求之间会随机等待2到5秒,比固定等待更加贴近真实用户的操作节奏。
4.3 函数与Beanshell的边界:哪些场景别硬用函数
说一句扎心的实话:过度使用函数会让脚本变成一团乱麻。函数的定位是“快速生成一个值”或“做一个简单的表达式判断”,它不适合处理复杂业务逻辑。
举几个典型的例子。带签名的接口,请求体要求sign=MD5(appid+timestamp+secret),用函数写这种签名计算会非常痛苦,你需要在请求参数里嵌套多层表达式,而且中间任何一步出问题都不容易定位;这种场景就应该用JSR223前置处理器写一段Groovy脚本,读参数、拼字符串、算MD5、存变量一步到位。
需要做正则匹配或者从响应JSON中提取多个值再二次加工时,也不建议硬用函数。正则提取器提取出来的结果要再判断、再遍历,这些是脚本的工作,不是函数的工作。判断边界其实很简单:如果一个表达式需要超过两层嵌套,或者需要循环、条件分支、正则匹配,就直接写JSR223;如果只是生成一个随机数、取一个时间戳、读一个属性,那函数是最轻量可靠的选择。
写JSR223脚本还有一个必须注意的点:脚本本身要避免副作用。脚本中定义的临时变量名不要和环境中的变量冲突,vars、props、log这些内置对象用熟了基本就能避开90%的坑。
4.4 正则提取与函数的互补:提取结果再加工
正则表达式提取器和JSON提取器负责“从响应里取数据”,函数负责“对取到的数据做变换”,它们天然就是一对。
最简单的互补用法:提取响应里的某个ID,再用__RandomString给它拼一个后缀,生成新的追踪编号。比如从查询接口提取order_id,然后在下一个接口里构造${order_id}_${__RandomString(4,abcd1234,)}作为新的退款单号。
稍微进阶一点的玩法是用__V配合提取器实现“列表轮询”。JSON提取器可以一次性提取多个匹配值,并生成item_1、item_2、item_3这样的变量组。后续请求里要轮流使用这些值,就可以在循环控制器里用${__V(item_${__counter(FALSE,)})}依次引用。这个组合能应付大多数“从列表中选择一个元素作为下一步请求参数”的需求,很多JMeter封装框架里所谓的数据驱动,本质上也就是这一套。
5. 函数报错排查实录:这五个坑我基本都踩过
5.1 函数写了却不生效:三种典型的根因
函数没有求值、原样出现在请求体里,是最常见的报错样式。比如请求参数变成了mobile=138${__Random(10000000,99999999,)}这种字面文本,而不是真的随机数。
第一种根因是函数名拼写错误或者下划线数量不对。${__time(...)}少了一个下划线变成${_time(...)},JMeter会按普通变量处理,自然原样输出。第二种根因是括号没有闭合或者参数位置错乱,特别是函数嵌套时少写了一个右括号,解析器直接放弃求值。第三种根因是函数返回了空值,而空值恰好被写进了必填参数,接口报错后回看请求,表面看是参数格式问题,实际是函数压根没有生成任何内容。
排查这类问题,推荐在任何取样器之前放一个Debug Sampler,执行之后能在查看结果树里看到所有变量、属性和JMeter内部变量,函数到底解析成了什么值一目了然。强烈建议:批量排查时不要肉眼看请求体,直接用Debug Sampler输出,省时省力。
5.2 跨线程组属性偶发为空:别只盯着setProperty本身
属性在后置处理器里可能执行了、日志里也可能没有报错,但目标线程组里读取时依然是空。这类问题最容易让人在原地打转,因为“写入”和“读取”两边看起来都是对的。
其实有几个隐藏原因值得优先排查。第一个是线程组启动顺序:测试计划里线程组A写在下面、线程组B写在上方,默认启动时B先跑,自然会读到空值。第二个是属性名大小写:props.put("token", ...)和${__P(Token,)}读的不是一个东西。第三个是时间差:线程组B可能在某些场景下和A并行启动,此时登录请求还没执行完,token也还没写入,B就已经读取完了。
解决思路也简单:先在读取端用带默认值的写法${__P(token,__NOT_FOUND__)},如果默认值都输出了,说明属性真的没写进去;接着在写入端加一个日志输出log.info(props.get("token")),验证写入端确实执行了。用这种“逐步缩小范围”的方式,基本几分钟就能定位是哪一段出了问题。
5.3 函数嵌套过深可读性崩了:怎么重构不破坏逻辑
三层甚至四层函数嵌套在一个表达式里,写的时候可能还能理解,过两个星期再看基本等于看加密文本。比如${__jexl3(Long.parseLong(vars.get("respTimestamp")) < ${__time(,)},)}这种,虽然功能没问题,谁都会在维护时头疼。
重构的原则是“把中间结果拆出来”。如果某个函数的结果后面要多次使用,先定义成变量:添加一个“用户自定义变量”,把${__time(yyyyMMddHHmmss)}存为curTime,后续表达式直接用${curTime}。类似的,接口请求体非常长的时候,可以把请求体会话模板单独拆到一个“用户自定义变量”里,再按组成部分拼接。这样虽然多创建了几个变量,但脚本的可读性和可维护性会好非常多。
另外一个实践技巧是:在脚本的关键位置加注释说明。JMeter里每个测试元件都有注释区域,给复杂的函数表达式写一行说明,比如“这里生成的是5分钟前的时间,格式为yyyyMMddHHmmss”,看起来是小事,但团队协作时能省下大量沟通成本。
5.4 压测时函数的性能开销:并发场景下的实测观察
函数虽然方便,但也有代价。JMeter放大压测时,每个虚拟用户每次请求都会解析函数表达式,这个解析过程是纯CPU开销。我自己在1000并发场景下实测过:脚本里大量使用__UUID和__RandomString生成唯一标识时,压力机本身的CPU占用会比采用预置数据文件高出不少,在普通配置的机器上甚至会反过来成为瓶颈。
所以我的建议是分层处理:低并发阶段,函数随意用;高并发压测,能预生成的参数尽量提前在测试数据文件里生成好,用CSV直接读取;只在必须动态生成的地方(比如时间戳)保留函数。另外一个降低开销的实用做法是:对不要求绝对唯一、只要求“足够随机”的字段,缩小随机范围、缩短随机字符串长度,这些操作都能明显减少函数的计算量。压测的本质是测服务端容量,不是测客户端脚本表达式的解析性能,别让脚本自身开销干扰了测试结果。
6. 全链路案例:注册、登录、业务请求里的函数组合
6.1 场景设计:三个线程组怎么编排
接下来用一个贯穿全篇的案例,把上面提到的函数和技巧串起来。目标是构建一条完整的业务链路:注册新用户、登录获取token、携带token调用核心业务接口。
我设计了三个线程组。第一个线程组是“注册新用户”,生成随机账号并提交注册请求;第二个线程组是“登录并提取token”,用注册成功的账号登录;第三个线程组是“核心业务请求”,把token放到请求头里发起真实业务操作。
实际压测时,注册和登录可以用较低并发执行,核心业务线程组才是高并发的重点。这样做的好处是:注册和登录接口的请求量被控制住,不会出现“因为注册接口被限流”导致后续业务全部无法执行的情况;同时核心业务线程组可以独立调整并发数,测试目标更聚焦。
6.2 注册接口:随机手机号和时间戳组合生成
注册接口的请求体核心字段我推荐这么构造:
{ "mobile": "138${__Random(10000000,99999999,)}", "name": "test_${__RandomString(6,abcdefghijklmnopqrstuvwxyz,)}", "timestamp": "${__time(yyyyMMddHHmmss)}", "source": "jmeter" }手机号用138前缀加8位随机数,基本合规且不会重复;用户名用固定前缀加随机字母;时间戳用格式化后的当前时间。这样一个注册请求的参数每次执行都不一样,重复提交不会撞到唯一性约束。
这里要提一个断言设计细节:注册接口返回的内容里可能包含新用户ID,而登录接口正好需要这个ID作为用户名,所以注册请求后面要加JSON提取器,把用户ID存到变量里供后续请求使用。如果注册接口本身返回了用户ID,这一步不能省,否则登录环节还得再改参数,链路就断了。
6.3 登录后token回传:属性跨线程组传递完整配置
登录线程组的第一步是使用前面注册好的用户名密码,发起登录请求。登录响应里通常有一个token字段,用JSON提取器提取为login_token变量。
接着添加JSR223后置处理器:
props.put("token", vars.get("login_token"));然后在第三个线程组的HTTP请求头中添加:
Authorization: Bearer ${__P(token,__TOKEN_MISSING__)}默认参数我特意写成__TOKEN_MISSING__,一旦token没传对,响应结果里能非常明显地看到这个字符串,排查效率比空值高得多。整个传递过程中,线程组顺序一定不能乱:注册、登录、业务,依次排列。执行前可以先用1个线程、1次循环跑通,再放大并发。
6.4 并发压测下的响应断言与错误定位
业务线程组里加上响应断言和错误定位逻辑。假设业务接口要求响应码200,且业务耗时小于5000毫秒,断言可以这么配:先用JSON提取器提取响应里的code和elapsed,然后用__jexl3做复合判断。
不过更实际的做法是直接在JSR223断言里写:
def code = vars.get("code"); def elapsed = Long.parseLong(vars.get("elapsed")); if (!"200".equals(code) || elapsed >= 5000) { AssertionResult.setFailure(true); AssertionResult.setFailureMessage("响应异常,code=" + code + ", elapsed=" + elapsed); }这样断言失败时会在结果树里直接看到失败原因,而不是一个简单的“文本不匹配”。同时,请求参数里如果带了${__threadNum()},失败日志里就能精确追溯到是第几个虚拟用户的请求出了问题,定位效率成倍提升。
6.5 压测结果怎么看:聚合报告里的关键指标
压测跑完之后,聚合报告和汇总报告里的几个指标要会看。样本数确认请求是否全部执行;平均响应时间是最直观的参考,但不要只看平均值,要看90%或95%的响应时间,后者才代表绝大多数用户的真实体验;吞吐量(每秒请求数)是服务端处理能力的核心指标;错误率只要不为零,就要逐条去看失败原因,是断言失败还是连接超时。
还要提醒一点:错误率要区分“业务失败”和“脚本失败”。业务失败是接口返回的业务码不对,比如注册成功但返回了用户已存在;脚本失败是参数写错、token没传上、断言表达式有误。压测报告里如果混着一堆脚本造成的失败,容量评估就不可信。我每次压测完的第一件事,是先抽几条失败样本打开响应体,确认失败原因属于哪一类,再决定这轮结果能不能用。
最后再说一个压测端的小建议:正式压测之前,永远先用低并发跑一遍全流程,确认参数生成、token传递、断言逻辑全部正常,再逐步加大并发。函数表达式写错时,低并发下就能暴露;而高并发下所有问题都会被放大到难以排查。做好这一步,比带多少参数、写多少函数都重要。