電商專案 Jmeter 指令碼實戰開發

語言: CN / TW / HK

「這是我參與11月更文挑戰的第28天,活動詳情檢視:2021最後一次更文挑戰

一、前置工作

1、黃金流程

在做效能指令碼之前,先了解下這本次效能實戰業務,簡要說明本次使用一個電商系統的下單流程做這本次效能業務場景,該流程也叫叫黃金流程,使用者從瀏覽首頁到選擇商品、加入購物車、支付等一系列步驟組合成該流程,下圖是這次效能實戰的業務流程圖。

圖片

2、Jmeter安裝

指令碼開發前置條件是需要在安裝 Jmeter,如果沒有安裝的話請點選下載到官方網站下載 Jmeter; 因為 Jmete r是純 java 開發出來的所以需要安裝 jdk 環境,請大家到網上下載 jdk1.8 以上版本並且安裝,在這裡就不在演示安裝步驟,為了方便大家配置 jdk 與 Jmeter 環境變數,這裡提供 mac 電腦環境中的 jdk 與 Jmeter 環境變數,環境變數參考如下:

Jdk 環境變數參考如下:

```shell $ vim .bash_profile

export JAVA_HOME=/usr/local/java/jdk1.8.0_181 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH

按esc鍵,再輸入如下命令

$ :wq!

終端輸入生效命令

$ source ~.bash_profile Jmeter 環境變數參考如下:shell $ cd ~ $ vi ~.bash_profile

Jmeter:路徑

Jmeter_HOME=/Users/tools/apache-Jmeter-5.3 PATH=$PATH:$HOME/bin:$Jmeter_HOME/bin: $ export PATH 執行生效: $ source ~.bash_profile `` 驗證 Jmeter 環境是否配置成功請輸入【Jmeter -v】命令,如果提示如下表示配置成功,配置環境變數的好處是在終端任何檔案目錄下可以開啟 Jmeter,如果不配置只能在 Jmeter 中的{Jmeter_path}/bin/Jmeter` 下執行:

圖片

輸入命令 Jmeter 命令即可啟動:

plain liwen ~ % Jmeter ================================================================================ Don't use GUI mode for load testing !, only for Test creation and Test debugging. For load testing, use CLI Mode (was NON GUI): Jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] & increase Java Heap to meet your test requirements: Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the Jmeter batch file Check : http://Jmeter.apache.org/usermanual/best-practices.html ================================================================================ 下圖是啟動 Jmeter 後效果圖:

圖片

二、指令碼實戰開發

在開發指令碼之前,先拆分兩部分指令碼來開發,一個部分註冊流程指令碼開發,一個部分是下單流程指令碼開發,如下圖:

圖片

1、使用者註冊鏈路

為什麼先從註冊指令碼開始因為下單流程需要使用者登陸態才可以下單,所以先從註冊流程中的註冊指令碼開始開發;

先從註冊介面文件觀察出來,該系統註冊需要先獲取手機驗證碼,才能呼叫註冊介面註冊到該系統中。

1.1、介面說明

註冊使用者介面圖: 圖片

為了快速開發註冊指令碼,先手動輸入一個手機號,並且點選獲取驗證碼,獲取驗證碼介面如下:

圖片

點選註冊使用者介面輸入驗證碼、密碼、手機號、使用者名稱點擊發送即可註冊成功,介面文件顯示如下:

圖片

通過上面註冊成功的使用者,開啟登陸介面文件,輸入使用者名稱與祕密即可登陸系統,下圖是使用者登陸介面登陸後響應的資訊圖,裡面包含使用者的 token,響應狀態碼等資訊,其他需要登陸態的介面可以通過新增 token 頭資訊就可以獲取該介面響應的資訊,但是註冊後的使用者需新增使用者地址,才能完成下單流程,如果沒有使用者地址,下單 是不會成功,因為下單都不知道貨物送到什麼地方。

圖片

下圖是新增使用者地址資訊介面,介面入參如下圖:

圖片

點選除錯按鈕,介面請求模版資訊如下顯示,做指令碼可以參考介面文件提示而且通過提示資訊就能很快把指令碼開發出來。

圖片

通過手動介面文件註冊使用者,不方便未來批量註冊使用者,所以需要給它們之間加上關聯,只這樣才能做到批量註冊使用者,但是如果我們壓測需要大量使用者,可以通過 java 或 python、excl 等工具生成一批使用者並且儲存到檔案中,再通過 Jmeter 中的 CSV Data Set Config 元件讀取檔案方式獲取使用者引數,這樣才能批量註冊使用者,如何批造出手機號,這裡提供兩個程式碼可以幫助大家快速生成使用者手機號。

生成手機號碼需要滿足手機號規則,通過調研需要滿足下面條件:

  • 中國電信:133、149、153、173、177、180、181、189、191、199
  • 中國聯通:130、131、132、145、155、156、166、171、175、176、185、186
  • 中國移動:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、172、178、182、183、184、187、188、198

python生成手機號碼程式碼參考如下:

```python import random

所有的頭資訊新增上來

phon = [133, 149, 153, 173, 177, 180, 181, 189, 191, 199, 130, 131, 132, 145, 155, 156, 166, 171, 175, 176, 185, 186, 135, 136, 137, 138, 139, 147, 150, 151, 152, 157, 158, 159, 172, 178, 182, 183, 184, 187, 188, 198]

開啟檔案,並且存放到一個記憶體地址中

f = open("phone.txt", "w")

迴圈100萬次

for i in range(1, 1000000): # 拼接手機號碼 phone = str(random.choice(phon)) + "".join(random.choice("0123456789") for i in range(8)) # 儲存手機號 f.write(phone) f.write("\n")

關閉檔案

f.close() java 生成手機號碼程式碼參考:java public static void main(String[] args) throws IOException { //儲存到檔案中 FileWriter fileWriter = new FileWriter("./7d/phone.txt"); //儲存1000個手機號 for (int j = 0; j < 100000; j++) { //儲存手機號資料 String[] phone = {"133", "149", "153", "173", "177", "180", "181", "189", "191", "199", "130", "131", "132", "145", "155", "156", "166", "171", "175", "176", "185", "186", "135", "136", "137", "138", "139", "147", "150", "151", "152", "157", "158", "159", "172", "178", "182", "183", "184", "187", "188", "198"}; Random random = new Random(); int first = random.nextInt(phone.length); StringBuilder stringBuilder = new StringBuilder(phone[first]); for (int i = 0; i < 8; i++) { stringBuilder.append(random.nextInt(10)); } fileWriter.write(stringBuilder.toString()); fileWriter.write("\n"); } //關閉檔案流 fileWriter.close(); } ``` 注意:

為什麼需要用隨機數做使用者手機號碼,而不是順序執行生成手機號,因為順序生成手機號碼並且註冊系統中最後存到資料庫中這樣的資料不真實,壓測引數化的資料需要符合真實使用者資料。

1.2、指令碼開發

開啟Jmeter,新建執行緒組,如圖: 圖片

通過手機號碼獲取驗證碼寫法操作步驟如下:

1、新建HTTP Reques並且按如下把引數寫進入即可把指令碼開發出來;

2、中間 ${telephone} 是引用全域性檔案引數檔案,後面會說到;

圖片

因為使用者註冊需要用到手機驗證碼,所以需要通過關聯把引數獲取出來,並且給註冊介面使用,如果大家不瞭解什麼是關聯請參考《效能測試實戰30講》【關聯和斷言:一動一靜,核心都是在取資料】在這裡就不詳細說明,獲取驗證碼寫法參考如圖: 圖片

註冊介面指令碼第一步新建 HTTP Request 輸入註冊地址,並且輸入相關資訊,參考如下:

url:

bash http://cloud-gateway.mall.demo.7d.com/mall-portal/sso/register 具體寫法如下: 圖片

說明:

中間的 value 地方顯示是的 ${username} 是引用 Jmeter 中的全域性變數,這樣寫的好處是一個地方修改全部引用都生效,方便引數維護;

以下是全域性引數檔案:

圖片

通過上面操作即可把使用者註冊完成,但是完成註冊對於下單流程還是不能下單成功,因為缺少使用者地址,下面再把使用者地址新增到該使用者,新增使用者地址介面實現參考具體如下:

圖片

整個註冊流程 Jmeter 指令碼如下顯示:

圖片

以上指令碼是使用者註冊到登陸再到增加使用者地址資訊在 Jmeter 中實現,上面指令碼中沒有新增斷言是因為這些指令碼主要完成的事情是使用者註冊,使用者註冊資訊是為下單流程做資料準備所以沒新增斷言,不過大家可以新增斷言元件,判斷請求是否正確。

注意:

如果只測下單流程而不想使用登入介面,那麼只需要把使用者資訊與token資訊儲存到本地,通過引數化技術把token與使用者資訊讀取傳給需要登陸態的介面,這樣才能正常壓測業務場景,以下介紹怎麼通過Jmeter 把資料儲存到本地。

第一步新增執行緒組,新增 HTTP Request,輸入登陸系統介面地址與使用者引數資訊如下:

圖片

在全域性引數檔案中新增輸出,儲存檔案資訊路徑,如圖:

圖片

在 Jmeter 中新增 JSR233 Sampler 元件如圖:

圖片

參考程式碼:

java FileOutputStream fps=new FileOutputStream("${outfile_online}",true); OutputStreamWriter osw=new OutputStreamWriter(fps); BufferedWriter bw=new BufferedWriter(osw); bw.append("${Bearer}\t${token}\n"); if(bw!=null){bw.close();} if(osw!=null){osw.close();} if(fps!=null){fps.close();} 通過程式碼編寫,再點選執行驗證輸出結果如下:

圖片

引數檔案寫入成功

圖片

2、使用者下單鏈路

在開發下單指令碼之前,先回顧下,下單流程需要那幾步驟,這樣才能把指令碼開發好,進而為基準測試與容量測試做準備;

圖片

新建全域性變數,全域性變數的目的是為了更好切換壓測環境與變數值,還有更好為引數化做準備,所以採用全域性定義變數,操作步驟如:

1、啟動 Jmeter,選擇右鍵選擇 User Defined Variables 組建即可看到相應的值,建議在每個定義變數後寫上註解,這樣方便記憶這個變數是什麼值。

圖片

下圖 name 這個模組,是需引數化的的變數名,在 Value 這個模組中輸入變數的值,這也是常見的 key-value 輸入樣式模式。

圖片

在做寫介面的時候先了解下介面請求型別這樣方便使用什麼型別做介面指令碼,通常情況下請求分為 GET/POST /DELETE/PUT 等常見請求;一般 GET 請求是或者資源資料,POST 請求是提交資料,DELETE 請求是刪除資料,PUT 請求是修改資料,但這些並不是唯一這樣定義,舉簡單例子如下:

java @GetMapping("/7d/test/{info}") public MsgResponse getRequest(@PathVariable Integer info) { if (info == 1) { //更新資料狀態 return MsgResponse.success().add("data", "更新狀態成功"); } else { //查詢資料 HashMap<String, String> map = new HashMap<>(); map.put("7d", "效能實戰1"); map.put("7dTest", "效能實戰2"); return MsgResponse.success().add("data", map); } } 從上面簡單的 demo 程式碼可以看出雖然是 get 請求,但是後端程式碼是會根據傳的值做相應的動作,如果大家想知道這幾個請求具體定義是什麼可以參考其他資料學習,在這裡就不進一步說明;

2.1、獲取首頁介面

獲取首頁介面文件如下,通過介面文件我們獲取如下資訊:

1、該介面是 get 請求,我們知道一般 get 請求都是獲取資源資料;

2、通過響應資料結構觀察出來它是一個 JSON 資料結構並且套嵌好幾層 list 資料結構;

圖片

3、進一步為了瞭解它為什麼是這樣的資料結構,可以看看它後端程式碼鏈路圖就明白為什麼這個是這樣的資料結構,下圖是獲取首頁後端連路圖與部門程式碼,有人問為什麼需要看鏈路圖與程式碼,這是為了分析問題做知識積累,還有程式碼如果是效能瓶頸,連程式碼怎麼呼叫關係都不懂,那麼更無重下手怎麼定位程式碼問題,並給出合理的調優建議:

(uml類圖與程式碼圖)

圖片

在看上面程式碼呼叫關係之前,先了解 下java EE 專案一般開發模式是什麼,下圖是簡單介紹下

圖片

說明:

  • controller是可接收和返回資料給使用者的web層;
  • service 是業務邏輯層,處理資料,校驗資料
  • dao 全名(data access object)是持久化層,專注於對資料庫的操作的一層;
  • DB 就是資料庫;

有上面知識鋪墊後,再分析首頁介面請求就會清楚明白,以首頁帶著大家瞭解後端程式碼怎麼呼叫怎麼實現;

1、web層能看到請求的資源路徑,如圖:

圖片

2、業務實現層如下圖,從圖可以看出,該介面通過請求獲取首頁廣告、獲取推薦品牌、秒殺資訊、新品推薦、人氣推薦、專題資料一共是 6 個數據結構,通過程式碼就明白前端在請求後,前端展示介面響應為什麼會有好幾層資料結構資料;

圖片

3、在往下檢視Dao層資料,這裡選擇【獲取推薦品牌】舉例,其他都是類似檢視方法,以下是 dao 層方法,注意該方法名【 getRecommendBrandList 】與 xml 中的 id 名稱需要一一對應否則 xml 中的語句就找不 dao 層的方法,也就不能查出資料;

圖片

具體 dao 層結構如下:

java /** * 獲取推薦品牌 */ List<PmsBrand> getRecommendBrandList(@Param("offset") Integer offset,@Param("limit") Integer limit); 4、上面getRecommendBrandList 方法名對應的xml檔案中的sql語句中的 id 關鍵字,通過下面很容易明白該xml檔案就是一些sql語句,注意 Dao 層中的【 @Param("offset" 】需要與xml中的 @Param("offset") 做一一對應,這樣才能傳值正確。

xml <select id="getRecommendBrandList" resultMap="com.dunshan.mall.mapper.PmsBrandMapper.BaseResultMap"> SELECT b.* FROM sms_home_brand hb LEFT JOIN pms_brand b ON hb.brand_id = b.id WHERE hb.recommend_status = 1 AND b.show_status = 1 ORDER BY hb.sort DESC LIMIT #{offset}, #{limit} </select> 通過上面後端程式碼分析,明白後端呼叫關係,未來遇到程式碼效能問題,就知道怎麼分析程式碼問題。

通過程式碼一步一步分析,這時就能看懂上面程式碼與uml類圖。

接下來開始在Jmeter中編寫獲取首頁資訊介面,新建執行緒組,新建HTTP Request 請求,輸入變數,根據響應結果新增斷言,具體如下圖;

圖片

新增登陸態頭資訊,因為把商品加入購物需要登陸後才可以把商品加入購物車,所以指令碼都需要增加標頭檔案資訊,以後指令碼就不說明,指令碼實現如下:

圖片

驗證介面是否成功,成功結果如下:

圖片

使用者登陸介面與用資訊查詢介面在之前的註冊流程中已經講解,這就不再這裡說明Jmeter中實現,需要注意的是為什麼在下單流程中需要使用者登陸與使用者資訊,這是因為後面介面需要使用者登陸態token與使用者地址id號資訊,所以需要使用該介面資訊,如果未來需要做基準測試場景,可以把該資訊儲存起來,下次介面需要該資訊即可通過引數化實現。

2.2、購物車新增商品介面

先看下介面文件,再分析需要什麼引數,再決定怎麼編寫相應的Jmeter指令碼,下圖是商品加入購物車介面,分析該介面需要一個商品skuCode碼還有一個是數量,說明下,實際工作中很少使用者自己新增skuCode碼一般是商品加入購物車,系統自動把商品 skuCode碼新增到購物車中,資料庫中或者快取中自動增加一條記錄。為什麼需要增加到資料庫或者快取中,這是方便使用者下次進入購物車該商品資料還在,如果不新增資料庫或者快取關閉瀏覽器或者手機瀏覽器等資訊商品資料自然會丟失。

如果商品資訊新增到cookie裡面,要知道cookie資料是存放瀏覽器或者其他快取中,該資料是依賴快取,如果快取消逝資料自然消失,所以資料需要存放的服務端,但是服務端依賴使用者登陸態,如果有登陸態資訊,系統會自動保留資料,下次使用者登陸自然就能看到該商品資訊,如果沒有登陸態下次開啟瀏覽器資料自然就不存在。

資料存放到cookie中只是臨時儲存,當用戶離開,商品資訊就會丟失,所以當用戶離開的時候提示是否需要保留商品資訊。

注意,新增商品資訊要想下次能看得到就必須讓使用者登陸才能保留該商品資訊。

圖片

分析商品加入購物車需要商品skuCode與數量,而且該商品需要有庫存才能加入成功,如果商品存在,但是沒庫存也不行,一般這些資料都是業務提供或者自己熟悉業務通過sql查詢出商品資訊再通過引數化做指令碼。

通過表結構分析,庫存表是【 pms_sku_stock 】通過sql語句就可以查出需要引數化的商品skuCode碼;獲取想要的資料後只要把資料另存為文字或者csv等儲存中,下次做指令碼時把引數加進去就行,關於引數化請參考《效能測試實戰30講》中的Jmeter中如何設定引數化資料?在這裡就不詳細說明怎麼引數化。

下圖是商品庫存資訊表:

圖片

2.3、購物車最終新增商品介面

引數檔案參考如下,對於 CSV Data Set Config 元件怎麼使用請參考《效能測試實戰30講》中的如何做引數化,在這裡就不做過多講解怎麼使用。

圖片

最終指令碼如下:

圖片

2.4、查詢購物車資訊介面

通過觀察查詢購物車介面是get請求,與之前做的是一樣,就不再多介紹,但是觀察我們下單流程中的下一步是確認購物車商品接口裡面需要傳一個數組引數,陣列引數就是這次查詢出來的結果購物車資訊中的 ID 值,目前查詢購物車介面是 json 陣列,通過Jmeter中的 JSON Extractor 可以獲取全部購物車資訊的中id號,再通過後置處理器中的【 BeanShell PostProcessor 】處理後可以把結果傳給下一步做引數。

介面文件如下:

圖片

相應結果如下:

json { "code": 200, "message": "操作成功", "data": [ { "id": 63280, "productId": 27, "productSkuId": 98, "memberId": 90, "quantity": 1, "price": 249, "productPic": "http://perfo7d.oss-cn-beijing.aliyuncs.com/mall/images/20200923/web.png", "productName": "", "productSubTitle": "", "productSkuCode": "201808270027001", "memberNickname": "xingneng_test", "createDate": "2020-11-02T15:28:56.000+00:00", "modifyDate": "2020-11-02T15:28:56.000+00:00", "deleteStatus": 0, "productCategoryId": 7, "productBrand": "", "productSn": "No86577", "productAttr": "[{\"key\":\"顏色\",\"value\":\"黑色\"},{\"key\":\"容量\",\"value\":\"32G\"}]" } ] }

查詢購物介面Jmeter指令碼參考如下:

圖片

驗證結果:

圖片

獲取全部商品id號【JSON Extractor 】 寫法參考如下圖,* 是正則表示式,-1表示獲取全部資料。

圖片

BeanShell PreProcessor參考如下圖:

圖片

java // log.info("除錯是否獲取長度id:"+vars.get("cartId_matchNr")); int num=Integer.valueOf("${cartId_matchNr}"); //log.info("資料為:"+num); StringBuilder stringBuilder = new StringBuilder(); for(i = 1;i<=num;i++){ stringBuilder.append(vars.get("cartId_"+i)+","); } String ids = stringBuilder.substring(0, stringBuilder.length() - 1); //log.info("結果:"+ids); vars.put("ides",ids); 解釋說明:程式碼中的${cartIds_matchNr}是通過新增 Debug Sampler 元件執行後在View Results Tree 點選Debug Sampler 請求就能看到該資料,目前通過BeanShell指令碼處理後,購物車確認訂單介面就可以使用該值做引數化。

BeanShell處理驗證結果如下:

圖片

2.5、購物車確定訂單介面

流程圖:

圖片

介面文件:

圖片

根據購物車資訊生成確認單資訊介面指令碼編寫參考如下:

圖片

注意:${ides} 引數是通過購物車查詢獲取的id號並且通過BeanShell 指令碼處理後得到的引數結化,如果不清除請仔細檢視上面指令碼,就明白這個地方的引數怎麼寫。

plain [${ides}] 驗證結果: 圖片

2.6、生成訂單介面

圖片

根據上面介面文件可以觀察出該介面需要傳購物車id號目前上一步已經實現,使用者收貨地址這個值在註冊的時候已經新增,並且也可以通過使用者資訊介面查詢該值,剩下的就是優惠劵、積分這個兩個值,目前這次不牽涉這兩個值,是否可以不傳或者傳什麼值,根據下面程式碼可以觀察處理,可以為null;

圖片

這次指令碼中把 useIntegration 與 couponId 這兩個引數直接去掉,之後Jmeter指令碼參考如下:

圖片

指令碼程式碼參考如下:

json { "cartIds": [${ides}], "memberReceiveAddressId": ${userId}, "payType": 0 } 響應結果如下:

圖片

2.7、分頁查詢訂單資訊介面

為什麼要把分頁訂單查詢這介面做為這次壓測介面,這是因為在實際購買商品後,有時會直接查詢自己之前的訂單資訊或者瀏覽器自己使用者下的全部訂單資訊,這時候就會呼叫分頁查詢訂單資訊介面,這符合真實場景。

介面文件:

圖片

說明:status根據介面文件提示只能輸入【訂單狀態:-1->全部;0->待付款;1->待發貨;2->已發貨;3->已完成;4->已關閉】這幾種狀態值,後pageSize與pageNum很好理解是傳什麼值;我們也可以通過程式碼觀察需要傳什麼值,程式碼如下:

圖片

根據介面資訊提示即可在Jmeter中實現,指令碼參考如下;

圖片

驗證結果:

圖片

2.8、支付訂單介面

介面文件如下:

圖片

說明:

該訂單號是根據生成訂單資訊介面響應結果生成的訂單號,如果需要使用訂單號,就需要通過關聯才能把訂單號取出來,並且傳給該介面使用,如果是基準場景測試是需要先造一批訂單號做引數,再跑訂單介面,

具體介面實現參考如下:

生成訂單資訊介面中獲取訂單號寫法參考如下,需要說明的是用該元件取值需要放到【生成訂單資訊】下,支付接口才能獲取該訂單號做引數。

圖片

支付訂單介面開發參考如下:

圖片

指令碼驗證結果:

圖片

2.9、根據ID獲取訂單詳情介面

通過介面文件看到訂單詳情只要傳訂單號就可以查出訂單詳細資訊;

圖片

在 Jmeter 中實現如下:

圖片

驗證查詢結果如下:

圖片

三、總結

1、通過註冊流程與下單流程指令碼開發,相信大家能掌握 Jmeter 做指令碼開發能在 Jmeter 實現 get 與 post 請求、引數化、關聯、還能通過【 JSR223 Sampler 】元件把響應結果儲存到本地,還能通過後置處理【BeanShell PostProcessor 】處理響應引數,把引數處理自己想要的結果,最後把引數傳給下一個介面使用;

2、通過新增【 Debug Sampler 】元件可以學到怎麼除錯介面請求,還可以通過該元件觀察引數化取值變化;

3、通過使用首頁介面做例子,一步一步帶大家察看後端程式碼是怎麼呼叫與程式碼之間的邏輯關係。如果效能測試中發現方法慢可以在方法前後輸入時間戳,最後輸出到日誌中,在日誌中檢視該方法執行多少時間;

有上面的基礎知識相信接下來遇到需要開發常見的效能指令碼是沒問題的。

四、問題

  1. 什麼情況下會需要用 JSR233 Sampler 來儲存數值到檔案?
  2. 什麼情況下會需要用 Debug Sampler?