JMeter使用正則表達式和JSON提取器實現關聯
- 前言
- 1 關聯的釋義與示例
- 2 常用正則表達式詳解
- 3 正則表達式提取器
-
- 3.1 參數詳解
- 3.2 使用示例
- 4 JSON提取器
-
- 4.1 參數詳解
- 4.2 使用示例
- 5 疑難雜症
-
- 5.1 提取多個值
- 5.2 多個值合並
- 5.3 左右邊界不好確定
- 5.4 多個匹配結果
- 5.5 其他特殊用法
前言
本文主要內容是:使用使用正則表達式提取器和JSON提取器實現關聯。
下文中會多次使用到
BeanShell Sampler
和Debug Sampler
,前者其實是起到一個mock server
的作用,返回自定義的響應結果,后者能夠輸出JMeter
的變量情況。
關於JMeter的使用,花費大量精力寫了JMeter的一系列文章,有圖有案例,一方面總結起來作為備忘,一方面希望能給初學者一些幫助。覺得有所幫助的朋友,請點個贊,對於疏漏之處也歡迎指教。
- JMeter邏輯控制器:https://blog.csdn.net/mu_wind/article/details/91879280
- JMeter配置元件:https://blog.csdn.net/mu_wind/article/details/92796646
- JMeter操作Mysql數據庫: https://blog.csdn.net/mu_wind/article/details/93312052
- BeanShell Sampler與BeanShell斷言:https://blog.csdn.net/mu_wind/article/details/93506974
- JMeter Linux下執行測試:https://blog.csdn.net/mu_wind/article/details/95733081
- JMeter自定義日志與日志分析:https://blog.csdn.net/mu_wind/article/details/95752633
1 關聯的釋義與示例
關聯在接口測試中是一個非常重要的概念,它的意思是在兩個或多個接口間建立邏輯上的依賴與聯系。
關聯的使用場景往往要滿足以下條件:
- A接口響應結果中的數據被后續的接口所引用
- A接口響應結果中被后續接口引用的數據是動態變化且無法提前預知的
例如,登錄接口-下訂單接口這樣2個接口組成的流程,就是非常典型的關聯案例。
首先,登錄接口返回包含用戶身份認證信息的token
,后續的下訂單接口需要附帶上這個token
才能被服務器識別身份。
Token是服務端生成的一串字符串,以作客戶端進行請求的一個令牌,當第一次登錄后,服務器生成一個Token便將此Token返回給客戶端,以后客戶端只需帶上這個Token前來請求數據即可,無需再次帶上用戶名和密碼。
要想實現這個場景,我們需要這么做:
- 在登錄接口響應結果中將
token
提取出來並保存在變量中,這里可以使用【正則表達式提取器】和【JSON提取器】。 - 在后續接口中引用已經保存好的
token
,一般通過【HTTP信息頭管理器】
形成的腳本如下。
1、登錄接口的響應結果:
{ "code" : 200, "msg" : "SUCCESS", "data" : { "accessToken" : "PJqx4566Ggf10qJv6firYAFS408p0us", "info" : { "id" : 10000, "level" : 0, "twiceGoogleAuth" : false, "twiceMobileAuth" : true, "twiceEmailAuth" : false, "tradePwdAlways" : false, "tradePwdHours" : false, "lastLoginDate" : null, "lastLoginAddress" : null, "depositFlag" : true, "loginCount" : 0, "emailRegister" : false, "nation" : 211, "webLoginCount" : 0 } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
2、從登錄接口響應結果中提取token
的值,並保存到名稱為myToken
的變量中:
3、引用前面保存的token值(前面保存變量為什么,這里就引用什么)
4、后續接口中,成功引用了到了myToken
的值:
接下來,我們以 WeatherWS 這個網站的兩個接口為示例,使用【正則表達式提取器】完成一個關聯實現。
接下來的測試場景是這樣的:
- 請求
getRegionProvince
接口,得到包含各個省份code
的列表,並在這個列表里提取北京的code
- 將北京的
code
作為getSupportCityDataSet
接口theRegionCode
參數的參數值,請求接口得到北京下轄的行政區域列表。
getRegionProvince的接口說明如下:
GET /WebServices/WeatherWS.asmx/getRegionDataset? HTTP/1.1 Host: ws.webxml.com.cn HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <DataSet xmlns="http://WebXml.com.cn/"> <schema xmlns="http://www.w3.org/2001/XMLSchema">schema</schema>xml</DataSet>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
getSupportCityDataSet的接口說明如下:
GET /WebServices/WeatherWS.asmx/getSupportCityDataset?theRegionCode=string HTTP/1.1 Host: ws.webxml.com.cn HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <DataSet xmlns="http://WebXml.com.cn/"> <schema xmlns="http://www.w3.org/2001/XMLSchema">schema</schema>xml</DataSet>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
根據上面的接口說明,先建立下面的腳本:
整體的腳本結構如上圖所示,下面依次看每個組件的內容和作用。
1、【HTTP請求】getRegionProvince
:
- IP:ws.webxml.com.cn
- 路徑:/WebServices/WeatherWS.asmx/getRegionDataset
- 作用:獲得中國省份、直轄市、地區;國家名稱(國外)和與之對應的ID
- 相應結果(為節省篇幅,刪除了大量無關數據):
<?xml version="1.0" encoding="utf-8"?> <DataSet xmlns="http://WebXml.com.cn/"> <xs:schema id="getRegion" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> </xs:schema> <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> <getRegion xmlns=""> <Province diffgr:id="Province5" msdata:rowOrder="4"> <RegionID>3117</RegionID> <RegionName>河北</RegionName> </Province> <Province diffgr:id="Province29" msdata:rowOrder="28" diffgr:hasChanges="inserted"> <RegionID>311101</RegionID> <RegionName>北京</RegionName> </Province> <Country diffgr:id="Country1" msdata:rowOrder="0"> <RegionID>3320</RegionID> <RegionName>阿爾及利亞</RegionName> </Country> </getRegion> </diffgr:diffgram> </DataSet>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
觀察相應結果,北京的RegionID
是311101,但如何將它提取出來並保存到變量中呢?這就要用到【正則表達式提取器】了。在HTTP請求getRegionDataset
上添加【后置處理器】-【正則表達式提取器】。
2、【正則表達式提取器】:
- 引用名稱:code,后面引用該值時,將使用
${code}
的固定寫法。 - 正則表達式:
<RegionID>(.+?)</RegionID>\r\n\ <RegionName>北京</RegionName>
,注意中間的8個空格,不能多一個也不能少一個。 - 模板:
$1$
,表示取第一列,下文【正則表達式提取器】會有詳細解釋。 - 匹配數字:1,表示取第一行,下文【正則表達式提取器】會有詳細解釋。
3、【HTTP請求】getSupportCityDataSet
:
- IP:ws.webxml.com.cn
- 路徑:/WebServices/WeatherWS.asmx/getSupportCityDataset?theRegionCode=${code}
- 作用:獲得支持的城市/地區名稱和與之對應的ID
- 相應結果:
2 常用正則表達式詳解
正則表達式描述了一種字符串匹配的模式(pattern),可以用來檢查一個串是否含有某種子串、將匹配的子串替換或者從某個串中取出符合某個條件的子串等。
正則表達式是相對繁瑣抽象的,理解和記憶難度較高,因此這里對JMeter中能用到的正則表達式語法(主要是限定符)進行一下講解。
據我個人經驗,(.+?)這個表達式基本就夠用了,何況正則表達式提取遠不如JSON提取器使用頻率高,所以這一節大可以略過,直接看第三節。
字 符 | 描 述 |
---|---|
. | 匹配除換行符 \n 之外的任何單字符 |
* | 貪婪,匹配前面的子表達式零次或多次,等價於{0,} |
+ | 占有,匹配前面的子表達式一次或多次,等價於{1,} |
? | 懶惰,匹配前面的子表達式零次或一次 ,等價於 {0,1} |
{n} | n 是一個非負整數。匹配確定的 n 次。例如a{3}匹配“aaaaa”,能匹配到“aaa” |
{n,m} | 重復n到m次,例如正則 “a{3,4}” 將a重復匹配3次或者4次 |
*? | 重復任意次,但盡可能少重復,如 “acbacb” 正則 “a.*?b” 只會取到第一個"acb" |
+? | 重復1次或更多次,但盡可能少重復,與上面一樣,不同的是至少重復一次 |
?? | 重復0次或1次,但盡可能少重復,如 “aaacb” 正則 “a.??b” 只會取到最后的三個字符"acb" |
{n,m}? | 重復n到m次,但盡可能少重復,如 “aaaaaaaa” 正則 “a{0,m}” 因為最少是0次所以取到結果為空 |
{n,}? | 重復n次以上,但盡可能少重復,如 “aaaaaaa” 正則 “a{1,}” 最少是1次所以取到結果為 “a” |
部分表達式使用【正則表達式測試器】實測結果如下:
*
:0次或多次,因為0個也被能匹配,所以b、c和末尾被匹配成空+
:一個或多個,因為至少要匹配一個,不會有空字符串?
:0個或一個,同*一樣,沒有a的被匹配成空字符串a{n}
:a{n,m}
:a{n,}
:
3 正則表達式提取器
正則表達式提取器一般在取樣器上創建,它的作用是在取樣器(包括HTTP請求和BeanShell Sampler及其他取樣器)的結果中按照一定的規則提取特定的值,並保存到內存中的某一個字段上,正則表達式所在的取樣器之后的組件,都能通過引用方式(格式:${XXX}
)使用該值。
3.1 參數詳解
名稱 | 描述 | 必須 |
---|---|---|
名稱 | 腳本中顯示的這個元件的描述性名稱 | 是 |
Apply to | Main sample only:僅適用於主樣本,默認用這個就可以了 | 是 |
Field to check | 要檢查的響應字段,即在取樣器響應內容的哪個區域進行匹配 | 是 |
Name of created variable | 引用名稱,即匹配到的變量存儲的名稱,一般會有[refname]_g(匹配數量)、[refname]_g0 (整體)、[refname]_gn(某個具體匹配值)等多個變量, | 是 |
Regular Expression | 正則表達式,用於分析響應數據的正則表達式,除非使用$0$組,否則必須至少包含一組括號 | 是 |
Template | 模板,如果在正則表達式中有多列結果,則可以是$2$$3$等等,表示解析到的第幾個值給title,如:$1$表示解析到的第1個值 | 是 |
Match No. (0 for Random) | 匹配數字,取第幾行,0代表隨機取值,-1代表全部取值,1、2、3等表示多行返回值取第幾個值。 | 是 |
Default Value | 缺省值,如果表達式沒有取得到值,就使用這個默認值 | 是 |
Use empty default value | 勾選此項后,如果未提取到值,則給變量賦予空字符串,而不是null | 是 |
3.2 使用示例
先看這么一個場景,假如響應內容ccBBmmAABBAAddBBAA
,想在該響應內容中提取AAddBB
並存儲到參數test
中,該如何處理?
首先,觀察待匹配字符串的左右邊界分別是BB
和AA
,那么正則表達式應寫成BB(.+?)AA
,在【正則表達式測試器】中測試一下:
可以看到,第1列(列從0開始計數)第二行是我們想要的結果,因此【正則表達式提取器】中按下圖填寫:
接下來,我們使用【BeanShell Sampler】模擬服務,來測試一下:
HTTP請求IP中引用正則表達式提取器提取到的test
:
4 JSON提取器
在【后置處理器】中,有一個【JSON提取器】,與【正則表達式提取器】有類似的作用,不同的是,前者專為處理JSON型的響應結果而生。
4.1 參數詳解
名稱 | 描述 | 必須 |
---|---|---|
Name | 名稱,腳本中顯示的這個元件的描述性名稱 | 是 |
Names of chreated variables | 匹配到的數據存儲的變量名稱,后續可以使用${variable name} 引用它 |
是 |
JSON Path Expressions | JSON路徑表達式 | 是 |
Default Values | 默認值,如果JSON 路徑表達式未能匹配到值,將使用該默認值 | 是 |
Match No. (0 for Random) | 如果匹配到多個結果,選擇使用哪個。0代表隨機,-1代表全部,x代表第x個 | 是 |
Compute concatenation var | 勾選此項后,如果匹配到多個結果,JMeter會使用","將他們連接起來,存儲在的變量中 | 是 |
4.2 使用示例
接下來,我們看一個示例:
假如接口返回下面的JSON數據,我們想在其中提取“周芷若”到“name”參數中。
{ "status":200, "data":[{"id":101,"name":"張無忌"},{"id":102,"name":"周芷若"}] }
- 1
- 2
- 3
- 4
首先,構造腳本結果如下圖,【BeanShell Sampler】作為mock server返回上面的數據:
return "{\"status\":200,\"data\":[{\"id\":101,\"name\":\"張無忌\"},{\"id\":102,\"name\":\"周芷若\"}]}";
- 1
在【BeanShell Sampler】下面添加【后置處理器】–【JSON Extractor】
這里解釋一下【JSON Path expression】的寫法,
- . 首先
$.
這部分是固定寫法 data
表示在JSON串以"data"為key獲取value,也就是"[{\"id\":101,\"name\":\"張無忌\"},{\"id\":102,\"name\":\"周芷若\"}]
"。data
所對應的值是一個JSONArray
(JSON數組)格式,里面有兩個JSONObject(JSON對象),第二個JSONObject是我們需要的,因此再按索引值"1"去獲取,寫作data[1]
,寫到這里,我們得到了{\"id\":102,\"name\":\"周芷若\"}
這個JSONObject
,接下來再根據name
這個key去獲取相應的值,就得到"周芷若"了。
運行腳本,查看結果樹中的【Debug Sampler】的響應數據:
后來在自己開發接口自動化框架的過程中,借鑒JMeter的這個功能,做了一個工具類,在響應結果是JSON串的接口中提取數據十分方便。
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author guozhengMu * @version 1.0 * @date 2018/12/20 13:07 * @description 根據自定義的字符串解析提取json中的特定內容 * @modify */ public class JsonPathExpression { public static void main(String[] args) { String str = "{\"data\" : {\"deth\" : {\"bids\" : [[\"3.637\", \"360000\"]],\"asks\" : [[\"4.273\", \"662\"],[[{\"a\":[1,2]}]]]}}}"; // String result = jsonPathExpression("{\"status\" : 200,\"employees\" : [{\"firstName\" : \"Bill\",\"lastName\" : \"Gates\"}, {\"firstName\" : \"George\",\"lastName\" : \"Bush\"}]}", "$.employees[1].firstName"); String result = jsonPathExpression(str, "$.data.deth.asks[1].[0].[0].a[1]"); System.out.println(result); } /** * 根據路徑表達式解析JSON * * @param jsonString 待處理的字符串 * @param matcher 路徑表達式 * @return */ public static String jsonPathExpression(String jsonString, String matcher) { String[] jsons = matcher.split("\\."); JSONObject object = JSON.parseObject(jsonString); JSONArray array = new JSONArray(); String result = ""; int index; for (int i = 1; i < jsons.length; i++) { if (jsons[i].contains("[")) { // 解析數字 index = getIndex(jsons[i]); if (i == jsons.length - 1) { // 最后一層 // 特殊情況處理 if (jsons[i].length() <= 3) { // []必然是從array中取值 result = array.getString(index); } else { array = object.getJSONArray(jsons[i].split("\\[")[0]); result = array.getString(index); } } else { // 不是最后一層 if (jsons[i].length() <= 3) { try { array = array.getJSONArray(index); } catch (Exception e) { object = array.getJSONObject(index); } } else { // 不知道下一層是array還是object try { array = object.getJSONArray(jsons[i].split("\\[")[0]).getJSONArray(index)