1. 程式人生 > >如何使用 JMeter 呼叫你的 Restful Web Service?進行簡單的壓力測試和自動化測試

如何使用 JMeter 呼叫你的 Restful Web Service?進行簡單的壓力測試和自動化測試

表述性狀態傳輸(REST)作為對基於 SOAP 和 Web 服務描述語言(WSDL)的 Web 服務的簡單替代,在 Web 開發上得到了廣泛的接受。
能夠充分證明這點的是主流 Web 2.0 服務提供商在介面設計中對 REST 的普遍採用 - 包括雅虎、谷歌以及臉譜 - 出於簡單易用、以面向資源的模型釋出自己的服務的偏好他們都已經拋棄了 SOAP 和基於 WSDL 的介面。在你必須要對你的 RESTFul web service 進行測試的時候,你可能會有這兩個選擇:
  • 使用 URL 對你的 WebService 進行簡單訪問測試
  • 使用 JMeter 來對 WebService 進行迴圈訪問測試。這種測試也能起到簡單效能測試的效果。
本文示例中將會演示如何建立簡單 Hello World WebService,並使用 JMeter 訪問測試該 WebService。
如果你碰到了以下問題之一,那麼恭喜你,你來對地方了:
  • 使用 JMeter 對 SOAP/REST WebService 進行功能測試
  • 使用 JMeter 對一個 RESTFul API 執行效能測試
  • 使用 JMeter 對 Rest API 進行自動化測試 - 也就是效能測試
  • 如何使用 JMeter 測試 REST API
  • 使用 JMeter 來測試一個 RESTful web service(Jersey)

步驟概述:

1. 先決條件:RESTFul web service 的完整實現。
2. 建立簡單 Java 類:CrunchifyJMeterTest.java(我們將使用 JMeter 訪問的服務)。
3. 在 Tomcat 上對該應用進行重新部署。
4. 執行 JMeter 並開啟下文提供的 .jmx 檔案。
5. 執行並對你的測試結果進行分析。

步驟 1

先決條件:RESTFul web service 的完整實現。部署並執行應用程式。

步驟 2

建立 CrunchifyJMeterTest.java 檔案
package com.crunchify.restjersey;
 
import java.io.FileNotFoundException;
import java.io.IOException;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
 
/**
 * @author Crunchify
 * 
 */
 
@Path("/index")
public class CrunchifyJMeterTest {
	@GET
	@Produces("text/html")
	public String checkECV() throws InterruptedException, FileNotFoundException, IOException {
		String result = "<br><div align='center'><h2>Hey This is Crunchify's JMeter Test...</h2></div>";
 
		System.out.println(result);
		Thread.sleep(1000);
		return result;
	}
}

之後應該看到下面這種 Eclipse 目錄結構:
Crunchify-REST-Jersey-Example.png

步驟 3

將 CrunchifyRESTJerseyExample 在 Tomcat 上重新部署,使用這個 URL 測試你的 REST 服務:
URL:http://localhost:8080/CrunchifyRESTJerseyExample/crunchify/index/
Crunchify-REST-Jersey-Example-URL-test.png

步驟 4

將以下指令碼拷貝到文字檔案並另存為 Crunchify-JMeter-Test.jmx:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.4" jmeter="2.9 r1437961">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="App Shah Desktop Test" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Crunchify's REST Service JMeter Test" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">5</stringProp>
        <stringProp name="ThreadGroup.ramp_time">2</stringProp>
        <longProp name="ThreadGroup.start_time">1367432020000</longProp>
        <longProp name="ThreadGroup.end_time">1367432020000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value"></stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">localhost</stringProp>
          <stringProp name="HTTPSampler.port">8080</stringProp>
          <stringProp name="HTTPSampler.connect_timeout">10000</stringProp>
          <stringProp name="HTTPSampler.response_timeout">10000</stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/CrunchifyRESTJerseyExample/crunchify/index</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
        <ResultCollector guiclass="StatGraphVisualizer" testclass="ResultCollector" testname="Aggregate Graph" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>true</xml>
              <fieldNames>false</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="Aggregate Report" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>false</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

步驟 5

  • 下載 Apache JMeter
  • 使用 jmeter.bat 或 jmeter.sh 檔案啟動 JMeter
  • 點選 File -> Open
  • 切換到你儲存 Crunchify-JMeter-Test.jmx 檔案的目錄並選擇該檔案

步驟 6

  • 將 Crunchify 測試案例擴充套件開你會看到下圖所示介面
  • 點選 HTTP Request
  • 確認各項引數是否正確
Crunchify-JMeter-and-RESTful-service-Load-test.png

步驟 7 

分析你的測試結果:
Crunchify-Tutorial-JMeter-and-RESTService-Result.png
原文連結:How to Call Your Restful Web Service Using JMeter? Perform a Simple Load Testing and Automation

譯者續:《對一個基於 Jersey 框架實現的 RESTful web service 進行壓力測試》

Jersey 專案

一個基於 Jersey 框架實現的 RESTful web service 專案如下:
一個Jersey實現的RESTful web service專案.png

wadl地址.png

Web Service 方法

我們要對其這個方法進行壓測:
    /**
     * 資料初始化
     * @param request
     * @return
     */
    @Override
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("push")
    public UserPushResponse push( UserPushRequest request) {
        logger.info(">>>>使用者" + request.getToken() + "資訊初始化!");
        logger.info(">>>>資料初始化請求報文:{}",
                new Object[]{JsonUtils.object2jsonString(request)});
        //返回報文
        UserPushResponse response;
        try {
            //請求報文校驗
            userPushService.validateRequest(request);
            //資料庫操作
            redisDao.set(request.getToken(), request.getValue(),
                         new Long(request.getTimeOut()));
            //組織成功報文
            response = userPushService.buildSucessResponse(request);
        } catch (Exception e) {
            logger.info(e.getMessage(), e);
            response = userPushService.buildFailResposne(e);
        }
        logger.info("使用者資訊初始化響應報文:{}"
                , new Object[]{JsonUtils.object2jsonString(response)});
        return response;
    }

該方法單元測試相關程式碼:
    UserResource userResource = RESTfulJsonClientFactory.createClient(UserResource.class, VIP);

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Test
    public void testResource() {
        UserPushRequest pushRequest = new UserPushRequest();
        pushRequest.setToken("mytest004");
        UserInfoAndMenu userInfoAndMenu = new UserInfoAndMenu();
        UserInfo userInfo = new UserInfo();
        userInfo.setMerCode("102239");
        userInfo.setAccountList(getAccountList());
        userInfoAndMenu.setUserInfo(userInfo);
        pushRequest.setValue(JsonUtils.object2jsonString(userInfoAndMenu));
        pushRequest.setTimeOut(10000);
        System.out.println("請求報文:{}"+JsonUtils.object2jsonString(pushRequest));
        UserPushResponse response = userResource.push(pushRequest);
        System.out.println("響應報文:{}"+JsonUtils.object2jsonString(response));
    }

RESTfulJsonClientFactory 是一個 WebService 的遠端客戶端建立類。執行該單元測試,輸出:
請求報文:{}{"msgType":null,"tranCode":null,"sysCode":"1001","brcCode":"1000","srcCode":"01","frontNo":"dfs-02040668","frontTime":"2017-01-20 15:27:19","repCode":null,"repMsg":null,"remark":null,"version":"1.0","signature":null,"cerVersion":null,"channelNo":"01","token":"mytest004","value":"{\"csrfToken\":null,\"userInfo\":{\"loginName\":null,\"merCode\":\"102239\",\"merName\":null,\"accountList\":[{\"acccountCode\":\"1022380016\",\"accountName\":\"redis測試賬戶名稱\"}],\"operator\":null,\"role\":null},\"menuTree\":null}","timeOut":"10000","reqDate":null}
響應報文:{}{"msgType":null,"tranCode":null,"sysCode":"1001","brcCode":"1000","srcCode":"01","frontNo":"channel.web.dfs.local","frontTime":"2017-01-20 15:26:24","repCode":"000000","repMsg":"成功","remark":null,"version":"1.0","signature":null,"cerVersion":null,"channelNo":"01","token":"mytest004","result":"1","reqDate":null}

http 報文擷取

SmartSniff 為我們擷取到了這次 Web Service 呼叫,包括一個完整的 http 請求報文(藍字部分)和返回報文(紅字部分):
SmartSniff幫我們擷取到了這次WebService呼叫.png
根據 SmartSniff 的結果我們拿到了這次 Web Service 呼叫的完整 URL:http:192.168.23.204/uas/resource/com.dfs.uas.biz.resource.UserResource/push,以及完整報文:
POST /uas/resource/com.dfs.uas.biz.resource.UserResource/push HTTP/1.1
Content-Type: application/json
User-Agent: Java/1.6.0_29
Host: 192.168.23.204
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 429

{"sysCode":"1001","brcCode":"1000","srcCode":"01","frontNo":"dfs-02040668","frontTime":"2017-01-20 17:12:29","version":"1.0","channelNo":"01","token":"mytest004","value":"{\"csrfToken\":null,\"userInfo\":{\"loginName\":null,\"merCode\":\"102239\",\"merName\":null,\"accountList\":[{\"acccountCode\":\"1022380016\",\"accountName\":\"redis..................\"}],\"operator\":null,\"role\":null},\"menuTree\":null}","timeOut":10000}
HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Fri, 20 Jan 2017 09:11:28 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive

db
{"sysCode":"1001","brcCode":"1000","srcCode":"01","frontNo":"channel.web.dfs.local","frontTime":"2017-01-20 17:11:28","repCode":"000000","repMsg":"......","version":"1.0","channelNo":"01","token":"mytest004","result":1}
0

這些都是很有用的資訊,URL 在我們寫 HTTP 取樣器配置路徑的時候用,請求報文在我們組織請求報文體時用,返回報文可以用來參考寫斷言。

HTTP 資訊頭管理器

JMeter 新建測試執行緒組、新建 HTTP 資訊頭,資訊頭加入名稱為 Content-Type 值為 application/json 項:
JMeter新建測試執行緒組、新建HTTP資訊頭.png

HTTP 請求取樣器

根據擷取資訊編輯 HTTP 請求取樣器,注意標框部分:
根據擷取資訊編輯 HTTP 請求取樣器.png

HTTP 響應斷言

根據擷取資訊編輯 HTTP 響應斷言:
HTTP 響應斷言.png

執行壓力測試

使用察看結果樹對指令碼除錯成功之後,再配置執行緒組併發數、迴圈次數等,開始壓測,一個小時後的 TRT 結果:
TRT.png

TPS 資料:

TPS.png

譯者後記

Web Service 的壓力測試指令碼其實很好寫,但是如果從建立 Web Service 的客戶端比如寫 BeanShell 實現本地執行遠端服務呼叫的角度出發往往會落入複雜繁瑣而且遙遙無期的指令碼開發除錯之中(用《神探狄仁傑》裡狄仁傑經常說的一句經典臺詞說:落入彀中)。換個思路,比如從協議的角度出發,Web Service 再複雜也脫離不了 HTTP 吧?然後在藉助合理的協議工具的幫助下,看似複雜的指令碼編寫就會變得非常簡單了。