1. 天气接口为什么适合拿来做 JMeter 自动化练手
1.1 选天气接口做标的的三个现实理由
做接口自动化,标的选得好不好,直接决定你能不能把参数化、断言、关联、正则这几个核心技能串起来。天气接口在这件事上有天然优势:它返回的是结构化 JSON,字段稳定、层级清晰,既有固定的状态码,又有随时间变化的温度、湿度、观测时间,非常适合拿来做字段级断言。更重要的是,天气接口存在"查询城市 → 得到城市编码 → 用编码查天气"这种两段式调用链,天生就是练关联的好材料。
第二个理由是免费和低门槛。和风天气、OpenWeatherMap 这类平台都提供免费额度的开发者接口,注册一个 key 就能用;wttr.in 甚至不需要任何凭证,直接 GET 就能拿到 JSON。练习阶段不用担心费用问题,也不会因为调不通而卡在环境上。
第三个理由是"看起来简单、实际坑多"。中文城市名要 URL 编码、温度单位有开尔文和摄氏度之分、观测时间戳有秒级和毫秒级两种、返回的城市列表可能一次给你十几个匹配项——这些都是真实项目里会遇到的麻烦。把这些坑在天气接口上踩一遍,换到电商、支付、物联网接口时,你会发现套路完全一样。
1.2 一条合格的天气接口脚本应该覆盖什么
很多人写天气接口脚本,就是发一个请求、加个响应断言看有没有 200,跑通就结束了。这种脚本在真实项目里没有任何价值,因为它没验证业务逻辑。一条能拿得出手的脚本,至少要覆盖四件事。
第一是数据驱动。硬编码"北京"只能测一个城市,参数化之后才能用一份 CSV 跑遍几十个城市,并且在并发线程下保证每个线程拿到不同的数据不串号。第二是字段级断言,不能只看 HTTP 状态码 200,还要校验返回体里的code是不是 "200"、温度是不是在合理区间、城市名是不是和请求的一致。
第三是关联取值,城市中文名不能直接传,得先调城市搜索接口拿到 location id。第四是正则提取,从字符串里精准抠出你想要的片段,而且要考虑贪婪匹配带来的意外截断。这四件事我在下面会逐个拆开讲,每一块都会附上我在实际项目里踩过的坑。
2. 环境搭建与接口结构摸底
2.1 JMeter 安装配置的几个硬性前提
JMeter 是基于 Java 的,所以第一件事是装 JDK。5.6.x 系列需要 Java 8 及以上,我一般直接用 JDK 17,兼容性没问题。装完之后java -version能正常输出就行。JMeter 本体去官网下二进制包,zip 解压即用,不需要安装程序。
解压路径有讲究:绝对不要放在带中文或空格的目录,比如D:\我的工具\apache-jmeter或者C:\Program Files\...。前者会导致部分插件读取路径失败,后者会因为空格在某些脚本调用时被截断。我习惯放在D:\tools\jmeter这种纯英文短路径下。
启动方式分两种,Windows 双击bin\jmeter.bat,Linux 或 Mac 执行bin/jmeter.sh。第一次启动会看到命令行窗口和一串日志,这是正常的,别关掉它,关掉就等于退出 JMeter。界面语言可以在bin/jmeter.properties里把language=en改成language=zh_CN,重启后就是中文界面。
还有两个配置建议顺手改掉。一是sampleresult.default.encoding=UTF-8,不改的话中文响应内容在结果树里会显示成乱码,让你误以为接口有问题。二是内存参数,jmeter.bat里默认HEAP=-Xms1g -Xmx1g,跑几百个线程做压测时容易 OOM,我一般调到-Xms2g -Xmx4g,前提是你机器内存够。
注意:JMeter 的配置改动都在
bin目录下的属性文件里,改完必须完整重启 JMeter 才生效,热改不生效是很多人排查半天没结果的原因。
2.2 天气接口的请求结构拆解
拿和风天气举例,它的实时天气接口长这样:https://devapi.qweather.com/v7/weather/now?location=101010100&key=你的KEY。两个查询参数,location是城市编码,key是开发者凭证。返回体是典型的两层结构,外层有code、updateTime,内层now对象里装着temp、feelsLike、text、humidity、windDir这些字段。
OpenWeatherMap 的结构完全不同。请求是https://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=你的KEY&units=metric,参数名是q而不是location,返回体里温度藏在main.temp,天气描述藏在weather[0].main。这里有个细节值得说:OpenWeatherMap 默认返回开尔文温度,26 摄氏度会显示成 299.15。如果你不做单位换算,断言里写"温度应该在 20 到 35 之间"就会全部失败。
参数计算过程很简单:摄氏度 = 开尔文 - 273.15。如果你不想改断言逻辑,就在请求里加units=metric让它直接返回摄氏度;如果接口不支持单位参数,那就在断言脚本里做减法。我建议优先用前者,把转换逻辑交给服务端,测试脚本保持简单。
这里再提一句wttr.in,它的格式更讨喜:https://wttr.in/Beijing?format=j1,无需 key,返回体里current_condition[0].temp_C直接就是摄氏度字符串。做教学演示时它最省事,但字段名不够规范,练关联的话还是推荐用带城市搜索接口的平台。
2.3 测试计划的分层结构设计
结构乱是新手脚本最常见的毛病,所有元件平铺在一层,改一个参数要翻半天。我习惯的层级是这样的:最外层是测试计划,下面挂"HTTP 请求默认值"和"HTTP 信息头管理器"两个配置元件,把域名、端口、协议、Content-Type 这些公共部分抽出来。这样后续所有请求只写路径和参数,换环境时改一处就行。
再往下按业务链路分线程组,比如"城市查询与天气校验"一个线程组,"并发压力测试"另一个线程组。每个线程组内部,用事务控制器把"城市搜索 → 提取 id → 查天气 → 断言"这一整条链路包起来,这样在聚合报告里看到的事务响应时间才是端到端的真实耗时,而不是单个接口的耗时。
元件命名也要花点心思,别用默认的"HTTP请求"、"断言"。改成"步骤1-城市搜索"、"提取器-城市ID"、"断言-温度区间",一个测试计划跑起来有几十个元件,名字清晰能省下大量排查时间。这个习惯在脚本要交接给同事时会体现巨大价值。
3. 参数化实战:让城市列表自动跑起来
3.1 CSV 文件设计与编码避坑
参数化最常见的方式就是 CSV 数据文件。做法是建一个纯文本文件,每行一条测试数据,字段之间用逗号分隔,第一行写表头。比如我要测五个城市,文件内容大致是city,locationId开头,下面依次是北京,101010100、上海,101020100、广州,101280101这样。
文件保存时有一个必须注意的点:编码必须是 UTF-8 无 BOM。Windows 记事本另存为 UTF-8 会自动加 BOM 头,JMeter 读到第一行开头会多出几个不可见字符,导致第一行数据永远匹配不上。用 Notepad++ 或者 VS Code 保存,确认编码格式里没有 "UTF-8 with BOM" 字样。我帮别人排查过三次类似问题,两次都是 BOM 惹的祸。
另一个坑是字段里本身含有逗号或换行。天气接口的场景下,城市名一般不会有逗号,但如果你的参数是"备注"这类自由文本,就得用引号把字段包起来,同时把 CSV Data Set Config 里的 "Allow quoted data" 设为 True。别小看这一项,注释里带个逗号就能让你整个文件列错位。
CSV 文件放哪也有讲究。我建议放在 JMeter 脚本文件(.jmx)同级的data目录下,然后用相对路径引用。绝对路径在换机器或者交接到别人电脑上时必然失效,相对路径则跟着脚本一起迁移。JMeter 的相对路径基准是启动目录,也就是你执行jmeter.bat的那个目录,这点要记住。
3.2 CSV Data Set Config 逐项参数说明
添加路径是:线程组右键 → 添加 → 配置元件 → CSV Data Set Config。这个元件有七八个参数,每个都会影响结果,我逐个说清楚。
Filename填文件路径,支持相对路径。File encoding填UTF-8,别留空,留空会跟随系统默认编码,在中文 Windows 上就是 GBK,必乱码。Variable Names填变量名列表,用逗号分隔,比如city,locationId,顺序必须和文件里的列顺序完全一致。Ignore first line设为 True,跳过表头。Delimiter默认就是逗号,如果你的文件用的是分号或者制表符,记得改。
Allow quoted data前面说过了,字段含逗号时打开。Recycle on EOF和Stop thread on EOF这两个是配套的:前者控制文件读完后是否回到开头重新读,后者控制读完后是否停止线程。常见组合是"Recycle=True + Stop thread=False",让数据循环使用;如果你要严格控制每条数据只跑一次,就设成"Recycle=False + Stop thread=True"。
Sharing mode是最容易被忽略的一项,它的取值有四种:All threads(所有线程共享一份文件指针)、Current thread group、Current thread(每个线程各自独立读)、编辑。默认是 All threads,多个并发线程会争抢同一份数据,导致每个线程拿到的行不固定。这个参数和下一节的"分块取值"直接相关。
3.3 多线程并发下让每个线程各取各的数据
默认的 All threads 共享模式下,5 个线程并发跑、文件里有 10 行数据,你会看到线程 A 拿到第 1 行、线程 B 拿到第 3 行、线程 C 拿到第 2 行,顺序完全打乱,而且不能保证每个线程拿到数量均衡的数据。做功能测试时这没大碍,做数据隔离的并发测试时就是灾难——你没法判断某个请求的失败是接口问题还是数据被别的线程用了。
解决办法有两种。第一种最简单,把 Sharing mode 改成Current thread,每个线程维护自己的文件指针,从第一行开始顺序读,线程 1 读第 1 行、线程 2 也读第 1 行。这种方式适合"每个线程都要完整跑一遍全量数据"的场景。
第二种是真正的分块,需要手动算偏移量。文件里放 100 条城市数据,起 10 个线程,希望线程 1 读第 1 到 10 行、线程 2 读第 11 到 20 行。做法是在 CSV 读取前用 JSR223 元件根据${__threadNum}计算起始位置,然后用${__CSVRead(文件路径, 列号)}函数配合循环索引来取。这个方案代码量偏大,一般在压测需要精准控制数据分布时才用。
还有一种偷懒但有效的办法:干脆给每个线程准备独立的 CSV 文件,文件名里带上线程号,用${__CSVRead(city_${__threadNum}.csv,...)}引用。文件数量少的场景下这个做法最直观,缺点是维护成本高,加一个线程就要加一个文件。
提示:判断当前用的是哪种共享模式,最直接的方法是加一个调试取样器(Debug Sampler)把变量值打印出来,跑 5 个线程看日志里 city 的取值分布,一眼就能确认。
3.4 数据库参数化:把测试数据放进 MySQL
当测试数据量到几百上千条,或者数据需要频繁更新时,CSV 就不好维护了。这时候把数据放进数据库,用 JDBC 直接查出来当参数,是更工程化的做法。
前置条件是数据库驱动包。把mysql-connector-java-x.x.x.jar(8.0 版本之后叫mysql-connector-j-x.x.x.jar)丢进 JMeter 的lib目录,重启生效。少了这一步,你会看到 "Cannot load JDBC driver class" 的报错。
配置分两个元件。先是JDBC Connection Configuration:Variable Name for created pool 填个名字比如mysql_pool,Database URL 填jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,Driver class 填com.mysql.cj.jdbc.Driver,再填用户名密码。URL 里的时区参数别省,不然取到的时间字段可能差 8 小时。
然后是JDBC Request,Connection pool 选上面那个名字,Query Type 选 "Select Statement",SQL 写select city, location_id from weather_city where enabled = 1。关键在Variable Names这一栏,填city,locationId,查询结果的每一行会被存成带下标的变量:第一行的 city 存进${city_1},第二行存进${city_2},以此类推。总行数存在${city_#}里。
引用的时候有个技巧:如果你要循环取多行,用${__V(city_${index})}。因为${city_${index}}这种嵌套写法 JMeter 不会二次解析,必须用__V函数做变量名拼接。我见过不少人卡在这里,明明变量存在却一直取到默认值,就是嵌套没解开。
数据库参数化的额外好处是天然支持条件筛选。加个where city_level = 1只取一线城市,改个条件就能切换测试数据集,比手动改 CSV 高效太多。
4. 关联实战:把上一个接口的返回喂给下一个请求
4.1 关联的本质与天气场景下的典型链路
关联说白了就是把上一个请求响应里的某个值,提取出来当作下一个请求的输入。为什么必须做关联?因为很多接口的参数是服务端动态生成的,你在脚本里没法预先知道。城市编码就是典型例子——你虽然知道"北京"的编码是 101010100,但用户可能搜的是"朝阳区",编码是多少你不可能背下来,必须调接口拿。
天气场景下我常用的关联链路是这样的:第一步请求城市搜索接口https://geoapi.qweather.com/v2/city/lookup?location=北京&key=KEY,返回里会有location数组,每个元素包含name、id、lat、lon。提取出id存成变量cityId。第二步请求实时天气接口https://devapi.qweather.com/v7/weather/now?location=${cityId}&key=KEY,把cityId填进 location 参数。
这个链路里还藏着一个细节:如果搜索"朝阳",返回的 location 数组里可能同时有北京的朝阳区和辽宁的朝阳市,你取第几个?如果没有明确规则,脚本就会随机取到不同城市,断言"返回城市名等于朝阳"时偶尔失败。我的处理方式是加一个adm参数限定省份,比如location=朝阳&adm=北京,或者直接规定取数组的第一个元素并把这个规则写进注释。
还有一种更隐蔽的关联需求:某些平台在请求时需要带一个服务端下发的 token 或者会话标识,先请求首页拿到这个值,再带上它请求业务接口。这就是典型的"防伪令牌"场景,缺少它会直接报未提供必要令牌的错误。原理和城市编码的关联完全一样,只是提取的东西从业务数据变成了认证信息。
4.2 正则表达式提取器的五个参数怎么填
添加路径:请求右键 → 添加 → 后置处理器 → 正则表达式提取器。它只有五个参数,但每一个都能把人卡住。
引用名称(Reference Name)是提取结果的变量名,比如填cityId,后面用${cityId}引用。这里别加${},只写纯名字。正则表达式(Regular Expression)是核心,要从响应文本里匹配出目标值并用括号圈住你要的部分。模板(Template)决定用第几个括号的内容,写$1$表示取第一个括号。匹配数字(Match No.)控制取第几个匹配结果:填 1 取第一个,填 -1 取全部并生成带下标的变量组,填 0 表示随机取一个。缺省值(Default Value)是没匹配到时变量的取值,建议填一个明显异常的字符串比如NOT_FOUND,这样断言失败时你一眼就知道是提取环节出了问题。
举个实际的例子。城市搜索接口的响应片段是:
{"code":"200","location":[{"name":"北京","id":"101010100","lat":"39.90","lon":"116.41"}]}我要提取id。写正则"name":"北京","id":"(\d+)",模板$1$,匹配数字 1,缺省值NOT_FOUND。跑完之后${cityId}就是101010100。
如果你不限定城市名,直接写"id":"(\d+)",匹配数字填 1,拿到的会是数组里第一个元素的 id,可能是北京也可能是别的,取决于接口的排序规则。匹配数字填 -1 的话,你会得到${cityId_1}、${cityId_2}一直到${cityId_N},总数在${cityId_matchNr}里。这个用法适合"搜索结果有多条,我要逐个验证"的场景。
4.3 JSON 提取器与正则提取器的取舍
JMeter 4.0 之后自带了 JSON 提取器(JSON Extractor),它用 JSONPath 语法取值,比正则直观得多。上面那个例子用 JSON 提取器只要写$.location[0].id,一眼就能看懂取的是哪个字段。
那什么时候还用正则?三种情况。一是响应不是 JSON,比如是 XML、HTML 或者纯文本,JSON 提取器直接失效。二是响应体格式不规范,JSON 提取器解析报错,正则反而能强行抠出来。三是你要提取的内容跨字段、跨层级,比如"找到包含某关键词的那一行,再取这一行里的某个数字",JSONPath 做不了这种上下文相关的匹配。
我的习惯是:能用 JSON 提取器就用它,可读性和稳定性都更好;只有遇到非 JSON 响应或者复杂文本模式时才退回正则。顺带说一句,JSON 提取器默认对所有取样器生效,如果你线程组里有多个请求,记得把它挂在正确的请求下面,或者用 "Apply to" 选项限定作用范围,不然可能出现"变量被另一个请求的响应覆盖"这种诡异问题。
4.4 跨线程组传值:变量与属性
JMeter 里变量(Variable)和属性(Property)是两个不同作用域的东西,这个区分非常重要。变量只在当前线程组内有效,线程组 A 里提取的${cityId},在线程组 B 里是取不到的,会直接显示成字面量。属性则是全局的,整个测试计划共享。
所以跨线程组传值的标准做法就是:在线程组 A 里把变量升级为属性,在线程组 B 里读属性。升级用 JSR223 后置处理器的 Groovy 脚本:
props.put("gCityId", vars.get("cityId"));vars是当前线程的变量容器,props是全局属性容器。在线程组 B 里引用时用${__P(gCityId)},__P是读属性的函数,找不到会返回空字符串。
一个常见的误区是用${__setProperty(cityId,${cityId})}这种函数写法。它也能用,但有个致命问题:${cityId}是在脚本执行前就被替换成字面值的,如果 cityId 是在同一个请求的后续处理中才产生,这里拿到的是空值。用 JSR223 里的vars.get()是在运行时取值,顺序才对。
注意:属性是全局的,多线程并发写同一个属性名会互相覆盖。如果每个线程的 cityId 都不一样,别用共享属性名,改成
${__threadNum}拼进属性名,或者干脆把关联逻辑放在同一个线程组内,避免跨组传值。
5. 断言体系:让脚本自己判定结果对错
5.1 响应断言与 JSON 断言的组合打法
没有断言的脚本等于没写测试,因为 JMeter 默认只要收到响应就标记成功,哪怕返回的是 500 错误页。断言就是告诉 JMeter"什么才算通过"。
响应断言(Response Assertion)是最基础的,添加路径是请求右键 → 添加 → 断言 → 响应断言。它有几个可勾选的测试字段:响应文本、响应代码、响应消息、响应头、请求头、URL 样本。最常用的是"响应文本",配合"包含"匹配规则,填"code":"200",只要响应体里有这个片段就算通过。
但只测这一个字符串太弱了。更好的组合是:响应断言校验响应代码是 200,JSON 断言校验业务字段。JSON 断言用 JSONPath 表达式验证,比如$.code期望值填200,或者$.location[0].name期望值填北京。它还能用正则模式,比如$.now.temp填^-?\d+(\.\d+)?$来验证温度确实是个数字格式。
温度和湿度的范围校验是 JSON 断言做不了的,因为它只支持相等或正则匹配,不支持数值比较。这种时候只能上脚本断言,下一节细说。选择哪种断言,判断标准很简单:等值判断用 JSON 断言,格式判断用正则断言,范围或逻辑判断用脚本断言。
5.2 BeanShell 断言与 JSR223 断言处理复杂判断
BeanShell 断言是 JMeter 老版本里做复杂判断的标准工具。它内置了几个变量:Failure是布尔值,设为 true 表示断言失败;FailureMessage是失败原因;prev是上一个取样器结果,用prev.getResponseDataAsString()拿到响应文本。
举个例子,验证温度在 15 到 35 摄氏度之间:
String resp = prev.getResponseDataAsString(); String tempStr = org.json.JSONObject.fromObject(resp).getJSONObject("now").getString("temp"); int temp = Integer.parseInt(tempStr); if (temp < 15 || temp > 35) { Failure = true; FailureMessage = "温度异常:" + temp; }BeanShell 的问题在于性能。它是解释执行的,高并发下会成为瓶颈,一个线程组几百个并发能让 CPU 飙到顶。所以新项目我强烈建议用JSR223 断言 + Groovy,语法更现代,执行速度比 BeanShell 快一个数量级,而且 Groovy 支持编译缓存,重复执行几乎没开销。
JSR223 断言的写法:
def resp = prev.getResponseDataAsString(); def json = new groovy.json.JsonSlurper().parseText(resp); def temp = json.now.temp as int; if (temp < 15 || temp > 35) { AssertionResult.setFailure(true); AssertionResult.setFailureMessage("温度超出范围:" + temp); }注意 JSR223 里不是设Failure变量,而是调AssertionResult.setFailure()。这个差异很多人第一次写会弄错,断言明明该失败却一直显示通过。另外 JSR223 面板里有个"Cache compiled script if available"选项,一定要勾上,不勾的话每次执行都要重新编译,性能比 BeanShell 还差。
5.3 断言规范与常见误区
断言写多了会互相干扰,写少了又失去意义。我总结的规范是这样几条:每个请求至少一个断言,验证响应代码;核心业务请求加字段断言,验证关键数据;不要断言易变字段,比如updateTime、currentTime这类时间戳,每次请求都在变,断言等于给自己找麻烦;断言失败要有可读信息,AssertionResult.setFailureMessage()里写清楚实际值和期望值,排查时能省一半时间。
一个高频误区是断言作用域搞错。JMeter 的断言默认只作用于同级和子级的取样器,如果你把断言加在一个事务控制器上,它会对事务内的所有请求生效,可能出现"第一个请求的响应被拿去匹配第二个请求的断言"这种错乱。解决办法是断言尽量挂到具体请求下面,或者严格用 "Apply to" 选项限定到主样本还是子样本。
另一个误区是断言里用绝对时间。比如断言"数据更新时间距当前不超过 5 分钟",写死一个时间戳,第二天再跑必然失败。这类判断要做成相对计算,用System.currentTimeMillis()和响应里的时间戳做差,而不是写死值。
6. 正则表达式在 JMeter 中的实战细节
6.1 贪婪与非贪婪:一个问号引发的血案
正则表达式在 JMeter 里的最大陷阱是贪婪匹配。默认情况下,.和+是贪婪的,会尽可能多地向右吞字符。假设响应是这样:
{"id":"101","name":"北京","id":"102","name":"上海"}你写"id":"(.+)"想取第一个 id,结果$1$拿到的是101","name":"北京","id":"102,因为它一路匹配到了最后一组引号。正确写法是加个问号变成非贪婪:"id":"(.+?)",这样匹配到第一个引号就停,拿到101。
这个问号在实际项目里能救你无数次。我见过最夸张的例子是提取登录 token,正则写成token=(.+),结果把后面整个 URL 参数全都吞进去了,导致下一个请求携带的 token 完全错误,接口一直返回认证失败。改成token=(.+?)&立刻就好了。
还有一种情况是响应里有换行。.默认不匹配换行符,如果你的目标值跨了两行,匹配就会失败。解决办法是用/s修饰符,或者在 JMeter 里改用[\s\S]+?这种写法。JMeter 的正则引擎对多行模式的支持比较有限,遇到跨行提取时,我更推荐用 JSON 提取器绕过去,比调正则省事。
6.2 模板、匹配数字、缺省值的组合用法
这三个参数组合起来能解决很多实际问题。模板$1$是最常用的,如果正则有多个括号分组,$2$就是取第二个括号。模板还支持$1$$2$这种拼接写法,能把多个分组拼成一个结果,比如把日期和时段拼起来。
匹配数字的用法前面提过,这里补充一个实战技巧:填 -1 之后能拿到${变量名_matchNr}这个总数变量,可以用它做循环次数的控制。比如搜索结果有 8 条,你想逐个验证每条数据的城市名都不为空,就可以用一个循环控制器配${__V(变量名_${index})}来遍历。
缺省值的作用比看起来大。填了缺省值之后,即使正则没匹配到,变量也会有一个确定的值,不会变成字面量${cityId}留在那里。如果不填缺省值,请求参数里就会原样发送${cityId}这个字符串,服务端返回参数错误。你看到的报错是"参数格式不正确",而实际原因是提取失败,排查方向会完全跑偏。所以缺省值我建议永远填一个显眼的标记,比如EXTRACT_FAILED。
6.3 几个高频正则速查
天气接口测试里能用到的正则其实就那么几类,整理成表方便直接抄:
| 提取目标 | 正则写法 | 说明 |
|---|---|---|
| JSON 里的字符串字段 | "temp":"(.+?)" | 非贪婪,取到第一个引号 |
| JSON 里的数字字段 | "humidity":(\d+) | 数字不用引号,直接匹配连续数字 |
| 城市编码 | "id":"(\d{9})" | 国内城市编码固定 9 位,长度限定更精准 |
| 观测时间 | \d{4}-\d{2}-\d{2}T\d{2}:\d{2} | 匹配 2024-05-20T10:30 格式 |
| 开尔文温度 | "temp":(\d+\.\d+) | 带小数点的数值 |
| 表单令牌 | name="__RequestVerificationToken" value="(.+?)" | 页面里的隐藏字段提取 |
时间格式的正则要特别注意时区后缀。2024-05-20T10:30:00+08:00和2024-05-20T02:30:00Z是同一时刻,如果你的正则写死了+08:00,换成 UTC 格式的响应就提取失败。稳妥的写法是\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^"]*,把时区部分放开,最后用[^"]*吃掉剩余字符直到引号。
7. 完整实操流程走一遍
7.1 从零到跑通的操作步骤
先把链路理清,我按顺序说明。新建测试计划,右键添加线程组,线程数设 1、Ramp-Up 设 1、循环次数设 1,先单线程跑通再加并发。
第一步,加 HTTP 请求默认值,协议填 https,服务器名填devapi.qweather.com,端口留空默认 443。再加 HTTP 信息头管理器,加一条Content-Type: application/json。
第二步,加第一个请求"城市搜索"。因为服务器名已经抽到默认值里了,这里只填路径/v2/city/lookup,参数写location=北京、key=你的KEY。注意中文参数在 JMeter 的参数表里会自动做 URL 编码,你直接填中文就行,不用自己转。
第三步,加正则表达式提取器挂在这个请求下。引用名称cityId,正则"location":\[.*?"id":"(\d+)",模板$1$,匹配数字 1,缺省值EXTRACT_FAILED。
第四步,加第二个请求"实时天气",路径/v7/weather/now,参数location=${cityId}、key=你的KEY。这一步验证关联是否成功,跑一次看结果树里第二个请求的 location 参数是不是变成了具体的 9 位数字。
第五步,加断言。第二个请求下加响应断言(响应代码等于 200)和 JSON 断言($.code等于200),再加一个 JSR223 断言做温度范围校验。
第六步,加监听器。察看结果树挂在测试计划下,方便调试;聚合报告也挂上,看响应时间。调通之后把察看结果树禁用,因为它在高并发时会把所有响应体堆在内存里,几百个线程就能让 JMeter 崩掉。
7.2 察看结果树与聚合报告怎么读
察看结果树是调试阶段的主力工具。左侧是请求列表,右侧分"取样器结果"、"请求"、"响应数据"三个标签。取样器结果里能看到响应代码、响应时间、响应大小和加载耗时,如果这里有红色标记说明断言失败或者请求异常。请求标签能看到实际发出去的 URL 和参数,这是验证参数化有没有生效的最直接方式——如果 URL 里还带着${city}字面量,说明变量没取到。
响应数据标签里如果显示乱码,回到jmeter.properties里确认sampleresult.default.encoding=UTF-8改了没,改完要重启。这个标签还支持搜索,响应体几千行的时候用 Ctrl+F 找目标字段比肉眼翻快得多。
聚合报告看几个指标就够:样本数、平均值、中位数、90% 百分位、异常率。做功能测试时主要看异常率是不是 0;做压测时重点看 90% 线和 99% 线,这两个数字才能反映大多数用户的实际体验。平均值好看但 99% 线很难看的接口,就是典型的"平均水平可以,偶尔卡死",这种问题在真实环境里最要命。
7.3 命令行压测与报告导出
界面模式做压测是不专业的,因为 JMeter 的 GUI 本身要消耗大量资源,线程一多就会干扰测试结果。正确做法是用命令行。基本命令是:
jmeter -n -t weather_test.jmx -l result.jtl -e -o report_dir-n表示非 GUI 模式,-t指定脚本文件,-l指定结果文件,-e -o表示测试结束后自动生成 HTML 报告到指定目录。要注意report_dir必须是不存在的空目录,目录里已经有文件的话 JMeter 会直接报错退出。
结果文件.jtl里的每一行是一次请求记录,包含时间戳、响应时间、响应代码、是否成功、线程名。这个文件可以导入 Excel 做二次分析,也可以对比两次压测的差异。我习惯压测跑三轮,取中间一轮的数据,第一轮往往有 JVM 预热的影响,数据偏高。
生成 HTML 报告后,重点看响应时间分布图和每秒事务数曲线。曲线如果呈锯齿状波动,说明有 GC 或者资源争抢;如果一开始很高然后断崖下跌,通常是连接池或者服务端限流导致的。
8. 常见问题排查速查表
8.1 高频报错与解决
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 响应内容显示乱码 | 编码配置未改 | sampleresult.default.encoding=UTF-8,重启 |
| CSV 第一行数据永远取不到 | 文件带 BOM 头 | 另存为 UTF-8 无 BOM 格式 |
变量显示为字面量${xxx} | 提取失败或作用域错误 | 检查缺省值、正则是否匹配、是否跨线程组 |
| 温度断言全部失败 | 单位是开尔文未换算 | 加units=metric参数或脚本里减 273.15 |
| 报错找不到 JDBC 驱动 | 驱动包没放对位置 | 放入lib目录后重启 |
| 高并发下 JMeter 卡死 | 察看结果树占内存 | 压测时禁用所有监听器,只留结果文件 |
| 跨线程组取不到值 | 用了变量而非属性 | 用props.put存属性,${__P()}取 |
| 中文参数发送错误 | 未做 URL 编码 | JMeter 参数表自动编码,特殊字符用__urlencode函数 |
| 请求偶发写入失败 | 连接被服务端重置或超时 | 调大超时时间,检查 Keep-Alive 设置 |
| 断言失败但响应看起来正常 | 断言作用域错乱 | 断言挂到具体请求,限定 Apply to |
8.2 我踩过的几个坑
第一个坑是正则匹配数字填 0。文档说 0 表示随机取一个匹配,看起来挺灵活,实际跑起来每次取到的值都不一样,断言偶尔过偶尔不过,排查了半天才发现是这个参数。功能测试场景下匹配数字永远填 1,只有明确需要遍历全部匹配结果时才填 -1。
第二个坑是断言写在事务控制器上。我一开始图省事,把响应断言加在事务控制器级别,想着能覆盖整条链路。结果事务内的城市搜索请求返回了 A 城市,天气请求返回了 B 城市,断言拿 A 的响应去匹配 B 的规则,一直报错但接口本身没问题。后来把断言拆到每个请求下面才理清。
第三个坑是中文参数没编码。有一次测一个带中文城市名的接口,JMeter 里参数表填了中文直接跑,返回"参数不合法"。查了半天以为是接口问题,最后用__urlencode函数包了一层就好了。原因是某些服务端对请求参数的编码格式解析比较严格,参数表里填的中文经过 JMeter 处理后,个别特殊字符没有正确转义。稳妥做法是中文参数统一用${__urlencode(北京)}处理。
第四个坑是并发下的数据串号。CSV 文件共享模式默认是 All threads,5 个线程跑 10 条数据,日志里看到的城市顺序完全乱掉,我一度以为是提取器出了问题。后来把共享模式改成 Current thread,每个线程独立从第一行开始读,问题立刻消失。这个参数的默认值坑过的人不在少数。
第五个坑是记住一点:JMeter 的所有配置改动都要重启才生效,不管是属性文件、jar 包还是插件。我在这个问题上浪费的时间加起来能有一整天,改完发现没效果,回头一查是没重启。
最后分享一个小技巧:调脚本的时候,在关键位置加调试取样器(Debug Sampler),把变量和属性都打印出来,比在结果树里一个个翻要快得多。调通之后记得把调试取样器删掉或者禁用,不然它会出现在每一次请求结果里,把报告搅得乱七八糟。