尧图精选

JMeter天气接口自动化:参数化、关联、断言、正则

🕒 发布时间:2026/10/1 9:05:28 📁 来源:尧图网络
接口脚本最容易骗人的地方就是它明明挂了却给你一片绿色的 200。之前帮一个团队看他们的天气接口自动化脚本聚合报告里错误率 0%结果我一翻察看结果树返回体里全是{code:1002,msg:invalid city code}——状态码 200业务全错。后来复盘问题出在三个地方城市编码是硬编码的、上一接口返回的城市 ID 没有传给下一个接口、断言只校验了响应里包含 200 这个字符。这三件事对应的恰好就是 JMeter 做接口自动化绕不开的四个考点参数化、关联、断言、正则。这篇就把这四个考点全部落在一个具体场景上——天气查询接口。它天然包含一条两段式链路先按城市名搜出城市 ID再拿 ID 查实时天气中间必须做关联城市列表天然适合做参数化返回体是 JSON字段多、有取值范围正好练断言和正则。不管你是刚装完 JMeter 想找个能跑通的练手项目还是已经写了半年脚本但一直靠肉眼点结果树这篇里的步骤和踩坑记录都能直接用上。1. 天气接口这条链路为什么恰好卡在四个考点上1.1 一条查询请求里藏着的依赖关系先看这条链路长什么样。天气服务一般不会让你直接用城市名查天气因为重名城市太多所以标准设计是两段式第一段/api/city/search?name杭州返回一个列表里面有cityId、cityName、province第二段/api/weather/now?cityId101210101keyxxx返回温度、湿度、风力、更新时间。第三段还可能有一个/api/weather/forecast?cityId...查未来几天。这个设计本身没问题但放到自动化脚本里就出现了硬依赖第二个请求的参数值来自第一个请求的响应体。如果我把101210101写死在脚本里那这个脚本就只能测杭州换个城市就得改脚本——这就引出了参数化。而要把响应体里的cityId抠出来塞进第二个请求——这就是关联抠出来这个动作靠什么工具完成最常用的就是正则表达式提取器。再看断言。天气接口的返回体里code字段是业务状态码data.temp是温度值data.updateTime是时间字符串data.city是城市信息。HTTP 200 只说明你的请求到达了服务端服务端也回了话至于回的是成功还是你 key 过期了只有看code才知道。我见过太多团队的脚本断言只有一句响应代码等于 200这种脚本的价值约等于零。所以天气接口不是一个随便挑的练手项目它的结构刚好把参数化、关联、断言、正则四件事全部串在一条主线上而且链路短出错了容易定位。换成电商下单链路十几个接口串起来新手还没跑到断言那一步就已经放弃了。1.2 先把测试计划的分层想清楚再动手拖元件JMeter 最大的坑不是元件不会用是元件放错位置。同一个响应断言放在线程组下面和放在某个 HTTP 请求下面作用范围完全不同——前者对所有取样器生效后者只对当前请求生效。所以动手之前先在纸上把分层画出来比在 GUI 里拖半小时有用。我在实际项目里固定用这套分层逻辑测试计划级放用户定义的变量、HTTP 请求默认值、HTTP 信息头管理器。这三样是所有请求共用的底座改了以后全局生效不用一个个请求去改。线程组级定义并发数、循环次数、调度器。参数化配置文件CSV Data Set Config我也习惯放在线程组下面因为它的共享模式是跟线程组绑定的。取样器级HTTP 请求本身加上只属于它的后置处理器正则提取器、断言、定时器。监听器调试阶段挂察看结果树正式跑的时候全部关掉只留聚合报告或者干脆用命令行生成 HTML 报告。关于 JMeter 版本我目前用的是 5.6.x配套 JDK 175.6 要求 Java 8 以上官方更推荐 17。别用 JDK 8 去跑 5.6某些 JSON 相关的元件会出奇怪的兼容问题。至于环境变量把%JMETER_HOME%\bin加到 PATH 里后面命令行模式会方便很多。1.3 中文乱码这件事开工前就该按死乱码是新手最容易被绊住的地方而且它的表现很迷惑请求发出去了响应也有内容但中文全是问号或者方块。三个地方要一起改缺一个都不行。第一jmeter.properties里找到sampleresult.default.encoding取消注释并改成UTF-8。这一行决定 JMeter 用什么编码去解析响应流不改的话默认是 ISO-8859-1。第二CSV 数据文件本身的编码要和 CSV Data Set Config 里填的编码一致。Windows 上用记事本另存的 CSV很容易变成带 BOM 的 UTF-8这时候第一个变量名的前面会多出一个不可见字符你会看到变量名是cityCode但引用${cityCode}取不到值——这种问题能查一下午。第三如果是从命令行启动加上编码参数jmeter -n -t test.jmx -Dfile.encodingUTF-8 -l result.jtl。GUI 模式下一般不用管但如果你的机器默认区域设置是非中文环境加上更保险。提示CSV 文件优先用 VS Code 或 Notepad 另存为UTF-8 无 BOM不要用 Excel 直接另存Excel 默认会带上 BOM 和平台相关的换行符。还有一点GUI 模式只用来调试和录制真正的压测一定要走命令行。GUI 本身要渲染结果树、要刷新界面一台普通办公机跑 100 并发光是 GUI 渲染就能吃掉大半 CPU压出来的数据根本不能看。2. 参数化把城市列表喂给线程组的几种姿势2.1 CSV Data Set Config 的八个字段每个都值得停一秒CSV Data Set Config 是参数化里用得最多的元件但它那八个输入框我见过至少一半的测试人员从来没把共享模式那一项点开看过。逐个拆一遍字段含义实际踩过的坑Filename数据文件路径相对路径以 JVM 启动目录为基准。GUI 下双击 jmeter.bat 启动基准是 bin 目录命令行下从项目目录启动基准就变了。建议直接写绝对路径File encoding文件编码留空等于用系统默认编码跨平台迁移必炸。明确写 UTF-8Variable Names变量名列表留空则用文件首行当变量名此时必须勾选 Ignore first line填了变量名首行就可以是纯数据Delimiter分隔符默认英文逗号。要填制表符就写\t反斜杠加 t不是真的按 Tab 键Allow quoted data是否允许引号包裹数据里本身含逗号时必须打开比如杭州,西湖区Recycle on EOF读完后是否回头重读默认 True。线程循环次数乘以线程数大于总行数时关掉它线程会提前结束报告里线程数对不上Stop thread on EOF读完后是否停止线程和上一项配合使用一般保持 FalseSharing mode共享模式决定文件指针怎么分配最容易理解错的一项共享模式三选一的实际效果说白了就是文件指针有几个。选All threads默认整个测试计划里只有一个文件指针所有线程按顺序抢着读下一行。线程 1 读第 1 行线程 2 读第 2 行互不重复。这是最常用的模式数据不会被重复消费。选Current thread group每个线程组各自持有一个文件指针。如果你有两个线程组共用同一个文件它们会各自从第一行开始读数据就被重复用了。选Current thread每个线程各自持有一个独立的文件指针都从第一行开始读。结果就是线程 1 读第 1 行、线程 2 也读第 1 行。这个模式适合每个线程都要完整遍历一遍数据集的场景比如每个虚拟用户都要把所有城市查一遍。2.2 线程数、循环次数、数据行数三者之间的关系假设你有 20 个城市50 个线程每个线程循环 3 次Recycle on EOF 保持默认的 True共享模式选 All threads。那么实际执行下来每一轮循环会把 20 行数据跑完然后回头重来总共 150 次请求里每个城市会被请求 7 到 8 次具体哪个城市多一次取决于时序。如果你把 Recycle on EOF 设成 False那第 21 次请求开始就取不到值了变量会变成${cityCode}这个字面量或者变成默认值。很多人在这里翻车看到请求还在发就以为是正常的实际上后面 130 次请求发的都是垃圾数据。所以算数据量的时候要记住这个公式需要的行数 ≥ 线程数 × 循环次数All threads 模式下否则一定要打开 Recycle。而 Current thread 模式下每个线程都要独立跑完需要的行数是线程数 × 循环次数但取值会大量重复。2.3 想按线程分块取值CSV 本身做不到这是被问得最多的一个问题同一个 CSV 文件怎么让线程 1 只取第 1 到 5 行线程 2 只取第 6 到 10 行各自管各自的一块互不干扰答案很直接CSV Data Set Config 做不到。它只能按顺序发号或者每个线程从头读没有按块切分这个概念。要实现分块得自己算行号。我的做法是在线程组下挂一个 JSR223 PreProcessor语言选 Groovy比 BeanShell 快也是官方现在推荐的把整个文件读进内存用线程编号和迭代次数算出目标行// JSR223 PreProcessorLanguage 选 groovy def lines new File(D:/testdata/cities.csv).readLines() // 如果首行是表头去掉它 if (lines.size() 0 lines[0].startsWith(cityCode)) { lines lines.tail() } int blockSize 5 int threadNo ctx.getThreadNum() // 线程编号从 1 开始 int iterNo vars.getIteration() // 当前线程的第几次循环从 1 开始 int row (threadNo - 1) * blockSize (iterNo - 1) if (row lines.size()) { row row % lines.size() // 越界回绕防止取到空值 } def cols lines[row].split(,) vars.put(cityCode, cols[0]) vars.put(cityName, cols[1]) log.info(线程 {} 第 {} 次循环取到第 {} 行{}, threadNo, iterNo, row, cols[0])这段代码里有几个细节值得说。ctx是 JMeterContext 实例vars是当前线程的变量表这两个在 JSR223 元件里是内置的不用自己 new。vars.getIteration()返回的是 int从 1 开始计数这个计数是当前线程的第几次循环不是全局计数所以在分块算法里正好合适。row % lines.size()这一句是兜底。如果不做回绕一旦行号越界lines[row]会抛 IndexOutOfBoundsException整个线程直接报错报告里会出现一堆莫名其妙的失败。回绕之后数据会从头再来一遍虽然有点重复但至少不会把脚本跑挂。这个方案的代价是数据全量加载到内存里。城市列表这种几百行的文件完全无所谓如果是百万级的号码池就不该用这个方法应该走数据库或者专业的测试数据服务。2.4 JDBC Request 参数化什么情况下值得上数据库当测试数据超过几千行、或者需要多个脚本共用同一份数据、或者数据本身要经常由业务系统维护的时候把数据放进数据库比放文件合理得多。JMeter 里的做法是加一个 JDBC Connection Configuration 配置元件放在线程组下填好 JDBC URL、驱动类名、用户名密码然后加 JDBC Request 取样器。关键在三个地方Query Type 要选Select StatementResult variable name 随便起个名比如cityResult如果想按行取值可以在 SQL 里用占位符配合参数化SELECT city_code, city_name FROM dim_city WHERE province ? ORDER BY city_code LIMIT ?然后在 JDBC Request 的 Parameter values 里填${province},${pageSize}Parameter types 里填对应的类型字符串用 VARCHAR数字用 INTEGER。如果不想用占位符也可以直接在 SQL 里写${province}但那样就失去了预编译的意义而且字符串要自己加引号容易出错。取出来的结果集怎么用如果是单行单列直接用${cityResult_1}。多行的话是${cityResult_1}、${cityResult_2}……同时有一个${cityResult_#}告诉你一共返回了几行。想随机取一行可以用${__Random(1,${cityResult_#},)}生成下标再拼成${cityResult_${__Random(1,${cityResult_#},)},}。这个嵌套看起来有点绕但是能跑。注意JDBC 驱动 jar 要放到lib/目录下重启才生效。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver写错了会报 ClassNotFound。另外连接池的 Max Number of Connections 要大于等于线程数否则高并发下会出现线程排队等连接压出来的响应时间全是假的。2.5 随机值、时间戳、UUID有些参数化不该用文件不是所有参数都适合从文件里读。比如请求里的时间戳、随机数、防重放的 UUID这些每次请求都该变写文件里反而累赘。JMeter 内置函数就能搞定${__time(yyyy-MM-dd HH:mm:ss,)}生成格式化时间天气接口的预报查询经常需要传日期。${__timeShift(yyyy-MM-dd,P1D,,)}在指定日期基础上偏移P1D是加一天查明天天气正好用。${__Random(1,100000,)}生成随机整数最后那个参数是变量名留空就不存变量。${__RandomString(16,abcdef0123456789,)}生成随机字符串适合做流水号。${__UUID()}生成通用唯一标识。还有一个坑要单独提很多天气服务要求把参数按字典序拼接后加密钥做摘要MD5 或 SHA256这个摘要没法用普通参数化搞定。我的做法是在线程组下加一个 JSR223 PreProcessor把所有参数取出来排序拼接算完哈希再存回变量import java.security.MessageDigest def params [cityId: vars.get(cityId), key : vars.get(apiKey), ts : vars.get(ts), nonce : vars.get(nonce)] def raw params.sort().collect { k, v - ${k}${v} }.join() def digest MessageDigest.getInstance(SHA-256) .digest(raw.getBytes(UTF-8)) .encodeHex().toString() vars.put(sign, digest)这样第二个请求里直接写${sign}就行。注意vars.get()取到的都是字符串如果服务端要求数字参与签名别自己加引号去拼。3. 关联把上一个响应里的钥匙递给下一个请求3.1 关联失败的九成原因是边界没卡住关联这个词听着高级本质就一句话从上一个响应里抠出一段内容存成变量下一个请求引用它。抠的动作是正则表达式提取器或者 JSON 提取器干的用的动作是变量引用干的。新手关联失败绝大多数不是正则写错了而是边界卡错了。举个真实例子响应体里同时有cityId:101210101和parentCityId:101210100你用cityId:(\d)去提第一个匹配到的可能是parentCityId那一行因为cityId:这个片段在parentCityId:里也被包含。结果你提出来的城市 ID 是父级城市的接口能返回数据但返回的是错的城市脚本全绿数据全错。这种 bug 最要命。解决办法是在正则前面加上不会歧义的边界字符比如cityId\s*:\s*(\d)把双引号带上。如果响应体里有换行和空格\s*就是用来兜住cityId : 101210101这种带空格的格式。3.2 正则提取器的五个输入框一次说透正则表达式提取器的界面上有五个需要动的地方逐个说。Name of created variable存进变量的名字比如写cityId后面就用${cityId}引用。Regular Expression正则本体。这里要记住 JMeter 用的是 Java 正则不是 JavaScript 正则两者有细微差别。三个高频技巧(?s)加在正则最前面可以让.匹配换行符抓跨行内容时必须加.*?是非贪婪匹配比.*安全但在大响应体上性能略差[^]*表示非引号字符重复任意次抓 JSON 里的字符串值最可靠因为它天然停在引号边界上。Template模板也就是最终存到什么。$1$表示第一个捕获组$1$-$2$表示把前两个捕获组拼起来。这里最容易错的是写成\1或者$1少了后面的$JMeter 只认$1$这种写法写错了提出来的是空字符串而且不报错。Match No.匹配编号。填1表示取第一个匹配填0表示随机取一个匹配填-1表示取全部匹配。填-1是个非常有用的技巧假设变量名是cityId那么取到全部之后${cityId_1}是第一个、${cityId_2}是第二个同时${cityId_matchNr}会告诉你一共匹配到几个。这在搜索结果列表里随机挑一个城市的场景下很好用——用${__Random(1,${cityId_matchNr},)}生成下标再拼成${cityId_${__Random(1,${cityId_matchNr},)},}。Default Value默认值。这一项千万别留空。正则没匹配到的时候如果默认值是空的变量就变成空字符串下一个请求发出去是?cityId服务端可能返回一个模糊的错误你查半天不知道怎么肥事。我的习惯是填一个明显不可能的值比如NOT_FOUND这样在察看结果树里一眼就能看出来是关联失败了。还有两个界的面的选项Use empty default value勾上等于默认值强制为空一般不勾Apply to决定从哪个范围里找默认是 Main sample and sub-samples如果响应里有重定向或者嵌入资源这个范围会扩大抓到意外内容的风险就高了建议改成 Main sample only。3.3 返回体是 JSON 的时候优先用 JSON Extractor正则虽然万能但在 JSON 面前确实有点苦。{data:{city:{id:123,name:杭州},temp:18}}这种嵌套结构用正则抓temp你得写temp\s*:\s*([\d.-])能抓到但是如果响应里还有别的地方也叫 temp 呢嵌套层级一深正则的可维护性会断崖式下降。JMeter 从 4.0 开始自带 JSON Extractor用 JSONPath 语法用法比重正好写$.data.city.id就能拿到123写$.data.temp拿到18写$.data.list[*].name配合 Match No. 为 -1 就能把所有名字抓出来。JMeter 5.6 里还有一个 JSON JMESPath Extractor语法略有不同功能类似。但正则不要急着丢。有两个场景我依然用正则一是响应体的结构不规则或者埋在一堆 HTML 里这里顺带一提用 JMeter 测老式 MVC 项目时__RequestVerificationToken这个防伪标记就藏在表单的 hidden input 里标准做法就是用正则从页面里把 value 抠出来再作为参数回传和 JSON 提取是同一个道理二是要提的内容和前后文有强依赖关系比如订单号([A-Z0-9]{12})用正则反而更直观。判断标准很简单数据结构化就用结构化工具数据埋在一堆噪声里就用正则。别拿正则硬啃深层 JSON也别拿 JSONPath 去啃 HTML。3.4 跨线程组的参数怎么传同一个线程组里提取出来的变量在下个请求里直接${cityId}就完事了。但如果链接被拆到了两个线程组——比如线程组 A 做登录拿凭证线程组 B 做业务查询——变量就不通了因为每个线程组的变量作用域是隔离的。这时候要用 JMeter 属性Property做中转。线程组 A 里用__setProperty把值写进全局属性线程组 B 里用__P读出来线程组 A 的 JSR223 PostProcessor ${__setProperty(globalCityId,${cityId},)} 线程组 B 的 HTTP 请求 /api/weather/now?cityId${__P(globalCityId)}__setProperty的第二个参数是要写入的值第三个参数是是否覆盖已有值留空表示覆盖。用${__P(名称,默认值)}读取时第二个参数是拿不到时的兜底值强烈建议填上比如${__P(globalCityId,NONE)}不然属性不存在时引用会原样输出字符串${__P(globalCityId)}很难排查。还有一个前提条件必须勾选测试计划上的Run Thread Groups consecutively顺序执行线程组否则两个线程组可能同时跑线程组 B 在读属性的时候线程组 A 可能还没写完。属性是测试计划级的全局对象多个线程同时读写会有竞争这一点在并发场景下尤其要注意。4. 断言让脚本自己说清楚哪一条挂了4.1 Response Assertion 的三个维度响应断言Response Assertion是使用频率最高的断言元件它有门口三个维度要选对选错了一个就全错。Field to Test要测哪个字段常用的是 Response Code响应码、Response Message响应消息、Response Data响应体、Response Headers响应头、Request Headers请求头、URL sampled请求的 URL。新手最容易犯的错是想校验响应体内容但选了 Response Code结果断言永远通过。Pattern Matching Rules匹配规则Contains包行、Matches完全匹配这里用的是整个字符串匹配正则要写完整、Equals相等、Substring子串、Not取反、以及可以组合的 Or。最容易被误用的是 Matches——它要求整个字段与模式完全一致写Matches加200去比响应码是能过的但写Matches加success去比一大坨响应体永远不会过。Patterns to Test要匹配的内容一行一个模式多行之间默认是或关系要改成并且就点下面的 And 按钮。一个可以直接抄的配置组合用三个断言覆盖三层断言目标Field to Test规则模式接口通了Response CodeEquals200业务成功Response DataSubstringcode:0不是错误页Response DataNot Containserror注意第三个用了 Not Contains 的组合意思就是响应体里不能包含 error 这个词。这类反向断言在回归测试里非常有用能把那些返回了 200 但回的是错误页面的情况拦住。4.2 200 不等于成功JSON 断言要校验到字段级JSON 断言JSON Assertion是 JMeter 4.0 之后新增的用起来很简单写一个 JSONPath勾上 Additionally assert value再填上期望值就成了。比如 JSONPath 写$.code期望值写0这一条就同时完成了字段存在和值正确两件事。但它有个限制只能做相等判断。温度是 18 还是 18.2它管不了也做不了范围判断。所以我的分层做法是协议层响应码 200加上响应头里的Content-Type包含application/json。这一层保证通信正常。业务层用 JSON 断言校验$.code等于 0校验$.data.city.id等于请求里发出去的那个城市 ID。这一条很重要它能拦住参数没生效服务端用了默认值这种沉默的错误。做关联的时候我习惯把请求参数和响应内容做一个交叉验证两边对得上才认为关联是成功的。数据层用 JSR223 断言做范围和格式校验。数据层为什么不能也用 JSON 断言因为要判断的东西太活了。温度得在合理区间内更新时间得是合法的日期格式风力等级得是整数且在 0 到 12 之间湿度得是 0 到 100。这些判断逻辑用相等断言写不出来只能上脚本。4.3 JSR223 断言范围判断、枚举判断与容差JSR223 断言支持 Groovy、JavaScript 等语言我固定用 Groovy。它的核心 API 就三个AssertionResult.setFailure(true)标记失败、AssertionResult.setFailureMessage(...)写失败原因、prev拿到上一个取样器的结果。一段可以覆盖大部分天气校验场景的代码import groovy.json.JsonSlurper def respCode prev.getResponseCode() def body prev.getResponseDataAsString() // 第一层协议层校验 if (respCode ! 200) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(HTTP 状态码异常 respCode) return } // 第二层能解析成 JSON 吗 def json try { json new JsonSlurper().parseText(body) } catch (Exception e) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(响应不是合法 JSON body.take(200)) return } // 第三层字段级校验 if (json.code ! 0) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(业务码异常 json.code msg json.msg) return } def temp json.data?.temp if (temp null || !(temp instanceof Number)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(temp 字段缺失或类型不对 temp) return } // 范围校验零下 60 到零上 60 度超出就是数据异常 if (temp -60 || temp 60) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(温度值超出合理区间 temp) return } // 格式校验时间字符串必须是 yyyy-MM-dd HH:mm:ss def updateTime json.data?.updateTime def pattern /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/ if (updateTime null || !(updateTime ~ pattern)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(updateTime 格式不符合预期 updateTime) }这段代码里有三个技巧值得展开。第一return用得很密。一旦某个前置条件不满足立刻标记失败并返回不要继需往下跑。不返回的后果是响应不是 JSON 的时候json.code那行会抛异常异常信息会把真正的失败原因埋掉你看到的是一堆堆栈而不是响应不是合法 JSON。第二失败信息里一定要带上具体的值。温度值超出合理区间 temp和干巴巴的一句温度校验失败排障效率差十倍。前者在聚合报告里一眼就能看到出问题的具体数值。第三~是 Groovy 的完整匹配运算符等价于 Java 里的Pattern.matches。它要求整个字符串完全匹配这个模式。如果只想判断包含用~。这两个符号长得很像用错了不会报错但会得到完全相反的结论实测的时候一定要用一条明知会失败的用例去验证断言真的能挂。顺便说一个容差问题。浮点数的比较永远不要用去比。服务端返回 18.300000000000001 和你期望的 18.3直接比是不相等的。做数值校验的时候用Math.abs(actual - expected) 0.01这种形式别嫌麻烦这是踩过坑的人才会写的一行代码。4.4 断言不是越多越好作用域放大是全链路灾难这一点我被坑过一次印象特别深。当时我在线程组下面放了一个响应码等于 200的断言本意是想让所有请求都校验响应码。结果测试计划里有一个 JDBC Request 取样器它没有 HTTP 响应码这个概念这个断言就对着它一顿判直接把所有数据库查询都标成失败。排查的时候看错误信息完全摸不着头脑最后才发现是断言的继承关系在作怪。规则很简单断言和定时器、后置处理器一样都遵循就近原则放在哪一层就管哪一层以及它下面的所有取样器。所以我现在的习惯是公共断言尽量不放在线程组级而是放在每个 HTTP 请求下面。听起来有点琐碎但出问题的时候定位成本低得多。性能上也有代价。JSR223 断言里如果拿整个响应体去做正则匹配大响应体上单次开销可能到几十毫秒。一百个并发乘上去就很可观了。所以断言里的正则要尽量精确不要用.*去扫全文本能不解析 JSON 就不解析能只取必要字段就不要反复getResponseDataAsString()。5. 四个考点串成一条链脚本落地与实测5.1 一份可以直接照抄的测试计划结构前面讲了四个独立的点现在把它们组装起来。下面这套结构我用了很久稍微改参数就能套到别的接口上测试计划 weather-auto-test ├─ 用户定义的变量 │ ├─ apiHost https://your-weather-host │ ├─ apiKey YOUR_KEY │ └─ timeout 5000 ├─ HTTP 请求默认值服务器、端口、协议、编码、超时 ├─ HTTP 信息头管理器Content-Type: application/json;charsetUTF-8 ├─ 线程组查询天气线程数 5循环 5调度器关闭 │ ├─ JSR223 PreProcessor分块取值计算 cityCode │ ├─ 事务控制器 完整查询链路 │ │ ├─ HTTP 请求城市搜索 /api/city/search │ │ │ ├─ JSON 提取器 cityId ← $.data[0].id │ │ │ ─ 响应断言状态码 200 JSON 提取器默认值检查 │ │ ├─ HTTP 请求实时天气 /api/weather/now │ │ │ ├─ JSON 断言 $.code 0 │ │ │ ├─ JSON 断言 $.data.cityId ${cityId} │ │ │ └─ JSR223 断言范围与格式校验 │ │ └─ 统一随机定时器延迟 300ms模拟真实间隔 │ ├─ 聚合报告 │ └─ 察看结果树仅调试时启用压测前禁用有几个地方要解释一下为什么这么放。事务控制器用在这儿的价值在于它把搜索城市和查询天气两个请求合并成一条事务聚合报告里看到的就是这条完整链路的响应时间而不是两个割裂的数字。如果只看单个请求你永远不知道用户在完成一次查天气这个动作时到底等了多久。事务控制器上有一个 Generate parent sample 的复选框勾上之后聚合报告里只显示事务这一行两个子请求的数字被隐藏报告会干净很多。定时器的位置也很讲究。JMeter 里的定时器作用域同样遵循就近原则放在事务控制器下面它就会在每个取样器前都加一次延迟相当于两个请求都延迟了 300ms。如果你只想在事务之间加间隔应该把定时器放到事务控制器外面或者用 Flow Control Action 取样器。定时器的时间设置上我一般不追求模拟得特别真300ms 到 1s 之间随便取目的是避免本地压测把服务端的连接数瞬间打满那属于压测范畴不是功能自动化该关心的事。JSON 提取器给默认值这一条是我现在写脚本的硬性规范。城市搜索如果没找到结果$.data[0].id提取不到变量就是空的第二个请求会带着空参数发出去。给默认值NONE之后第二个请求发的是cityIdNONE服务端的响应会明确告诉你参数非法你也一眼就能看出是关联断了。这比对着一个空请求发呆强太多。5.2 跑一遍看看输出从 200 到业务通过脚本搭好之后第一次跑建议把线程数设成 1、循环设成 1把察看结果树打开请求和响应的内容全看一遍。重点看四个地方。看请求参数是否正确。在察看结果树的请求标签页里确认cityId101210101而不是cityId或者cityId${cityId}。如果是后者说明变量名写错了或者提取器的变量名和引用名对不上大小写敏感cityId和cityid是两个变量。看提取器是否命中。JMeter 在调试阶段很有用的一个技巧是把提取器的默认值设成NONE之后如果请求里出现了NONE就说明正则没匹配上。这时候回到响应体里把正则贴进去重新数一遍引号和空格。九成的失败都是空格问题响应里是id: 101210101带一个空格你的正则是id:(\d)没写\s*自然匹配不上。写正则的时候宁可写松一点加上\s*兜住格式差异也不要不写因为服务端升级一次序列化格式就可能变。看断言有没有真的执行。断言失败之后察看结果树的取样器会标红右侧的断言结果面板里会显示失败原因。这里有一个必做的验证动作故意把期望值改错比如把$.code的期望值从 0 改成 999跑一次确认脚本真的变红了再改回来。没验证过会失败的断言等于没有断言。我见过太多脚本里的断言是花的从来没红过因为断言写错了永远能过。看响应时间的大致分布。单线程单循环的时候响应时间一般在几十到几百毫秒之间如果第一条就是五六秒那大概率是网络问题或者服务端限流先把超时设置调大确认能通再谈后面的并发。HTTP 请求默认值里的超时我一般设连接 5000ms、响应 10000ms。5.3 那些绕不过去的报错怎么解java.io.IOException: error writing to server这个报错在压测和功能测试里都会遇到它不是 JMeter 的问题而是连接被对端断掉了。常见原因有四个请求体太大超过了服务端的限制调大 JMeter 的堆内存并检查请求体大小服务端处理时间太长在返回前就把连接关了调大超时HTTP 信息头里带了Expect: 100-continue服务端不买账在信息头管理器里把它删掉用了 keep-alive 复用连接但服务端短连接在 HTTP 请求的 Implementation 里改成 HttpClient4或者加一个Connection: close请求头。排查这类问题时先用 curl 发一遍同样的请求curl 能通就说明是 JMeter 的配置问题curl 也不通就说明是对端的问题这一步能把排查范围砍一半。证书相关测 HTTPS 接口时如果服务端用的是自签证书JMeter 会直接报 SSL 握手失败。两种处理方式一是把服务端证书导入到 JMeter 用的信任库里keytool -importcert打进bin/下的 cacerts或者自己指定一个信任库文件并配上密码二是调试阶段干脆在 HTTP 请求的配置里关掉证书校验但正式回归绝对不能关那等于把测试的有效性扔了。另外如果你想用 JMeter 自带的录制功能录 HTTPS 脚本需要先装 JMeter 生成的根证书这个证书在第一次启动录制时会在bin/目录下自动生成按提示导入系统信任库就行。防伪标记未提供的报错测一些传统 MVC 项目的时候会碰到类似__RequestVerificationToken 未提供必要的防伪标记的返回。这本质上就是一个关联问题页面里有一个 hidden 字段带着一次性令牌表单提交时必须带上它。做法就是在访问页面的请求下挂一个正则提取器从 HTML 里把 value 抓出来再作为 POST 参数传到提交请求里。正则大概长这样name__RequestVerificationToken[^]*value([^])模板填$1$变量名填token。这种场景特别能体现关联不等于只抓 JSON——数据藏在 HTML 属性里的时候正则就是唯一的选择。中文乱码前面提过了sampleresult.default.encodingUTF-8CSV 用 UTF-8 无 BOM。区分一下乱码的来源请求里乱码是编码问题响应里乱码也是编码问题只有两者的编码配置都对了才能都正常。5.4 模拟 100 用户并发之前的三个前提功能脚本跑通了不代表能直接拿去压测。100 并发之前有三个前提要先解决否则压出来的数据毫无参考价值。第一合到非 GUI 模式。命令大致是这样jmeter -n -t weather.jmx -l result.jtl -e -o ./report -Jjmeter.save.saveservice.output_formatcsv-n非 GUI-t指定脚本-l指定结果文件-e -o生成 HTML 报告。注意-o指向的目录必须是不存在的或者空的否则会报错退出。如果结果文件已经存在也会报错我通常会在命令前面加一个删除旧文件的动作。第二调整堆内存。默认的 HEAP 是 1G100 线程加上结果收集很容易 OOM。改的方式是编辑bin/jmeterLinux或bin/jmeter.batWindows找到 HEAP 那一行改成-Xms1g -Xmx4g。同时建议把结果收集里的保存响应数据关掉压测时保存每一条响应体会让内存增速快得离谱。第三想清楚这次压测的目的是什么。如果目的是找上限那线程数应该阶梯上升先 50 再 100 再 200看在哪一档响应时间开始指数上升如果目的是验证100 并发下能不能扛住那就要保证 100 并发能真实打出去别用 10 个线程循环 10 次去假装 100 并发那两件事在服务端眼里完全不同。顺带说一个本地机器的限制。单机 JMeter 跑 100 个 HTTP 线程CPU 和网络一般是够的但如果响应体很大、要解析 JSON、还要跑 JSR223 断言瓶颈可能在 JMeter 自己身上。判断方法是看聚合报告的吞吐量是不是先上升后平稳如果线程数加了吞吐量不动说明 JMeter 已经到极限了这时候要上分布式压测一台控制机加多台执行机参数和脚本保持一致用-R指定远程节点。6. 脚本从能跑到有人敢用报告、规范与维护6.1 察看结果树导出与聚合报告里真正该看的列察看结果树的定位是调试工具不是报告工具。但它在调试期确实需要导出数据做法有两种一是用工具栏的配置按钮勾选需要的字段比如只勾请求头、响应数据、断言结果然后通过监听器的文件名输入框和保存动作把当前结果落到 JTL 文件里二是更推荐的做法——另外加一个 Simple Data Writer 监听器专门负责把结果写到文件察看结果树只管显示不管存储。这样两个职责分开了压测的时候把察看结果树一关数据照样有。注意 JTL 文件的格式。JMeter 5.6 默认写的是 XML 格式的 JTL体积很大100 万条采样能写到几个 G。改成 CSV 格式体积能小一个数量级改的方式是编辑jmeter.properties里的jmeter.save.saveservice.output_formatcsv或者在启动命令里加-Jjmeter.save.saveservice.output_formatcsv。另外还要想清楚要不要保存响应数据jmeter.save.saveservice.response_datafalse是压测时的标配。聚合报告里最容易被忽略的是分位数。Average 会被极端值拉高看平均响应时间很容被骗。真正有参考价值的是 90% Line 和 95% Line——90% Line 是 300ms意思是 90% 的请求在 300ms 内完成剩下一成慢得多。要看长尾就看 99% Line。还有一个容易看错的列是 Throughput它的单位是每秒完成的请求数注意它统计的是取样器数量如果你的脚本里一个事务包含两个请求那吞吐量看起来会是并发数的两倍这是正常的。6.2 断言的三层设计协议层、业务层、数据层前面在讲断言的时候已经提到这三层这里把它单独拎出来说因为这是脚本质量的分水岭。只有协议层断言的脚本能拦住网络问题和 5xx但拦不住业务异常价值很低。加上业务层断言能拦住参数错误、权限不足、数据为空这已经能覆盖大部分回归场景。再加数据层断言才能拦住数据算错了这种最难发现的问题——温度值是 180 度、更新时间是去年、城市 ID 和请求的对不上这三类错误在业务层断言下全都是绿的。这三层的成本递增收益也递增。我的经验是核心链路的断言必须三层齐全边缘接口至少要有协议层加业务层。判断一个接口是不是核心链路看它挂了之后用户能不能感知到——天气查询挂了用户立刻知道那就是核心链路。写断言的时候还有两个规范值得坚持。一是失败信息要包括具体值业务码异常1002msginvalid key比断言失败有用一百倍尤其是在 CI 里跑的时候你只能看到报告看不到响应体。二是每条断言都要能独立判断出问题在哪一层别把三层逻辑写在一个 JSR223 断言里不加区分那样失败信息只能告诉你挂不能告诉你哪一层挂了。6.3 让脚本活过三个月把易变的东西抽出去接口自动化脚本真正的敌人不是技术难点是维护成本。一个脚本写完两个月后服务端改个字段名如果你的脚本里这个字段名散落在二十个地方那就等着加班吧。我的做法是三个抽出来。环境地址抽出来用用户定义的变量加${__P()}本地、测试、预发三套地址通过启动参数切换脚本本身一行不改。测试数据抽出来城市列表、账号密码放 CSV 或数据库脚本里只留引用。断言规则抽出来比如温度合理区间是 -60 到 60把这个数字写到用户定义的变量里断言脚本去读变量而不是把数字硬编码在 Groovy 代码里。业务规则变了改一个变量就行。还有一个很小的习惯收益特别大给每个 HTTP 请求起一个人看得懂的名字。JMeter 默认的名字是HTTP 请求十个请求排下来全是HTTP 请求报告里出了问题你根本定位不到是哪个接口。命名规范我用的是动作 - 接口路径比如查询天气 - /api/weather/now一眼能看出来在干什么。最后说一个我用了很多年的自检清单每次脚本要交付之前过一遍断言是否真的验证过会失败故意改错期望值跑一遍确认能变红。参数化的默认值是否都填了空默认值是最隐蔽的坑。提取器的默认值是否填了一个明显不可能的值这样关联断了能一眼看出来。线程数乘以循环次数是否大于数据行数不满足就要确认 Recycle on EOF 的设置。察看结果树是否在正式跑的时候关掉了它的内存开销比你想象的大得多。脚本里是否还有写死的 IP、端口、密钥有就抽成变量为下一步做准备。这套东西没有什么高深的技术含量都是被坑出来的。我个人的体会是JMeter 这个工具的门槛不在元件怎么拖而在你知不知道自己每一个配置项为什么这么填。参数化、关联、断言、正则这四件事学一遍可能只要一天但把这四件事在每一个接口上都做对需要的是对接口本身的理解——知道哪个字段是依赖的、哪个值是有范围的、哪个响应是成功的样子。工具是手理解才是脑。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →