【Jmeter學習】JMeter關聯:JMeter正則表示式提取器與JSON提取器
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斷言:
- 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