HTTP Server Mock 從手工到平臺的演變(二)

大家都知道,不管是 Web 系統、還是移動 APP,各自在與內部、外部系統之間進行數據交互時,大多數情況下都是依賴接口。在基於接口約定開發的模式下,依賴接口的產出時間如果延遲,將直接影響了整個研發調試的效率;如果不能對接口進行及早測試,那發現問題的時間就要被推遲了。既然雙方約定了接口格式,爲何不按照這個規範直接測試,何必在乎依賴接口什麼時候產出,優先做到及早自測,後續只要替換接口聯調通過即可。下面主要講解基於 HTTP 協議的 API 接口模擬,從手工 Mock 到平臺的演變過程。

遇到的問題

曾經遇到的困擾:在研發過程中接口調試對接難的問題:

場景一:

【需求階段】Portal 前、後端約定基於接口開發

【開發階段】前端開發完畢,後端接口尚未開發完畢,前端只能硬編碼數據進行測試,造成接口對接調試延後,而且每次進行更多場景的數據調試,需要頻繁重啓服務、本地部署;

研發自測階段無法及早開展,依賴接口約束大。

場景二:

【需求階段】新功能開發,Portal 依賴計費的接口,雙方約定基於接口開發(內部、外部依賴接口場景均通用)

【開發階段】Portal 在開發進行中,計費尚未開發完畢,Portal 遲遲不能與計費對接調試(也有可能版本迭代步伐不一致的情況),測試階段一直被推遲;

另外,即使計費接口開發完畢,Portal 需要修改計費約定的接口數據進行調試,當發現沒有對方接口權限或者計費沒有過多人力資源來配合時,也無法進入更豐富的數據細節調試;

【測試階段】測試人員無法及早介入到調試階段進行接口測試,造成發現缺陷的最佳時期被推遲;

場景三:

【需求階段】移動 APP 項目依賴後端獲取帶寬數據的接口

【開發階段】移動 APP 端通過後端系統 API 獲取帶寬數據,繪製帶寬圖,APP 端繪圖工具開發完畢,後端 API 帶寬接口尚未開發完畢,移動 APP 端只能硬編碼數據進行測試,造成對接延後,每次進行更豐富的數據調試,需要頻繁重啓服務、本地部署;

研發自測階段無法及早開展,依賴接口約束大。

總而言之,如圖所示:

依賴接口開發完畢,才能夠進入到接口聯調測試階段,即使 Portal 的功能開發已經完成,也無法進行自測聯調,消耗的等待時間代價是不可估量的,效率低,。

圖 -1- 傳統的接口對接調試流程

圖 -1- 傳統的接口對接調試流程

手工作坊 -Nginx 反向代理

要解決在研發過程中接口對接調試難的問題,無非是所需即所有,減少等待時間,增加研發自測環節,同時也讓測試及早參與進來,因此需要能夠把依賴接口模擬出來(白盒方面的 Mock 有許多解決方案,這裏主要講的是基於 HTTP 請求的 API Server Mock),以便提高生產效率,改進流程如圖所示:

圖 -2- 改進的接口對接調試流程

圖 -2- 改進的接口對接調試流程

當前最簡單的想法是要解決:基於 HTTP 請求、固定 url、能夠正則匹配,在這個需求的驅動下,通過 Nginx 的反向代理能夠解決問題。

匹配具體路徑下某 html 文件

location ~ ^/live/(.*)\.html$ {
           root /home/htmlfile/ms;
}
location ~ ^/live/([A-Z0-9]+)$ {       
}

定義具體返回碼

location ~ ^/schedule/.*\.(json)$ {                
                error_page  404     /404.html;
 }

定義其它狀態碼也是同樣道理:

error_page 403 /error/403.html;
error_page 500 501 502 503 504 /error/500.html;</pre>

俗話說:術業有專攻,Nginx 並不擅長做 Mock API 的工具,在管理配置文件即使可以通過 svn 進行管理,依然是維護比較困難,對於不熟悉 Nginx 的測試工程師,也有一定的學習成本。

拿來主義:不重複造輪子 - 開源 WireMock

經歷了 Nginx 的配置繁瑣,決定另尋新路,有開源的 WireMock(http://wiremock.org/):

Ø WireMock 是一個靈活的庫,用於 Web 服務測試,和其他測試工具不同的是:WireMock 創建一個實際的 HTTP 服務器來運行你的 Web 服務以方便測試;

Ø 支持 HTTP 響應存根、請求驗證、代理 / 攔截、記錄和回放;

創建一個基於 WireMock 的 JavaProject(運行在 tomcat 下管理):

圖 -3-ServerMock Project

圖 -3-ServerMock Project

web.xml 配置如下:

<?xml version="1.0" encoding="UTF-8"?> 
<web-app id="WebApp_9"
 version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
 http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 
 <listener> 
    <display-name>wiremock-startup-listener</display-name> 
    <listener-class>com.github.tomakehurst.wiremock.servlet.
                    WireMockWebContextListener</listener-class> 
    <description>Loads WireMock and populates the servlet
                     context with its services</description> 
  </listener> 
  <context-param> 
    <param-name>WireMockFileSourceRoot</param-name> 
    <param-value>/WEB-INF/wiremock</param-value> 
  </context-param> //如果對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。
  <context-param> //感興趣可以175317069,羣內會有不定期的發放免費的資料鏈接,這些資料
    <param-name>verboseLoggingEnabled</param-name> //都是從各個技術網站蒐集、整理出來的
    <param-value>false</param-value> //如果你有好的學習資料可以私聊發我,我會註明出處之後
  </context-param> //分享給大家。
  <servlet> 
    <servlet-name>wiremock-mock-service-handler-servlet</servlet-name> 
    <servlet-class>com.github.tomakehurst.wiremock.jetty6.
                Jetty6HandlerDispatchingServlet</servlet-class> 
    <init-param> 
        <param-name>RequestHandlerClass</param-name> 
                <param-value>com.github.tomakehurst.wiremock.http.
                   StubRequestHandler</param-value> 
    </init-param> 
  </servlet> 
  <servlet-mapping> 
    <servlet-name>wiremock-mock-service-handler-servlet</servlet-name> 
    <url-pattern>/*</url-pattern> 
  </servlet-mapping>   
  <servlet> 
    <servlet-name>wiremock-admin-handler-servlet</servlet-name>     
<servlet-class>com.github.tomakehurst.wiremock.jetty6\. 
          Jetty6HandlerDispatchingServlet</servlet-class> 
    <init-param> 
        <param-name>RequestHandlerClass</param-name> 
                <param-value>com.github.tomakehurst.wiremock.http.
                         AdminRequestHandler</param-value> 
    </init-param> 
  </servlet> 
  <servlet-mapping> 
    <servlet-name>wiremock-admin-handler-servlet</servlet-name> 
    <url-pattern>/__admin/*</url-pattern> 
  </servlet-mapping>    
  <welcome-file-list> 
    <welcome-file>index.json</welcome-file> 
    <welcome-file>index.xml</welcome-file> 
    <welcome-file>index.html</welcome-file> 
    <welcome-file>index.txt</welcome-file> 
  </welcome-file-list> 
  <mime-mapping> 
    <extension>json</extension> 
    <mime-type>application/json</mime-type> 
  </mime-mapping> 
  <mime-mapping> 
    <extension>xml</extension> 
    <mime-type>application/xml</mime-type> 
  </mime-mapping> 
  <mime-mapping> 
    <extension>html</extension> 
    <mime-type>text/html</mime-type> 
  </mime-mapping> 
  <mime-mapping> 
    <extension>txt</extension> 
    <mime-type>text/plain</mime-type> 
  </mime-mapping>  
</web-app> 

web.xml 的這項配置可以改變源文件位置

<context-param> 
    <param-name>WireMockFileSourceRoot</param-name> 
    <param-value>/WEB-INF/wiremock</param-value> 
  </context-param> 

使用 Maven 管理依賴,配置如下:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
    <version>1.53</version>
   <!-- Include everything below here if you have dependency conflicts -->
    <classifier>standalone</classifier>
    <exclusions>
        <exclusion>
          <groupId>org.mortbay.jetty</groupId>
          <artifactId>jetty</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.google.guava</groupId>
          <artifactId>guava</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-core</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-annotations</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.skyscreamer</groupId>
          <artifactId>jsonassert</artifactId>
        </exclusion>
        <exclusion>
          <groupId>xmlunit</groupId>
          <artifactId>xmlunit</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.jayway.jsonpath</groupId>
          <artifactId>json-path</artifactId>
        </exclusion>
        <exclusion>
          <groupId>net.sf.jopt-simple</groupId>
          <artifactId>jopt-simple</artifactId>
        </exclusion>
     </exclusions></dependency>

具體的部署這裏就不介紹了,說說 WireMock 的配置:

Ø WireMock 的文件目錄

如圖所示:

mappings: 存放映射描述的文件

__files: 存放映射匹配結果的文件

圖 -4-WireMock 的文件目錄

圖 -4-WireMock 的文件目錄

WireMock 的匹配規則示例

分兩種:完整 Url 匹配和正則 UrlPattern

Url:完全匹配

mappings:cities-mapping.json

{        
    "request": {                                    
        "method": "GET",                        
        "url": "/cities"
    },                                      
    "response": {                                   
        "status": 200,                          
        "bodyFileName": "/cities.json",
        "headers": {
            "Content-Type": "application/json",
            "Cache-Control": "max-age=86400"
        }
    }                                               
}

__files:cities.json

{
    "cityName": "公司操作間",
    "shortname": "WS",
    "provinceName": "北京",
    "provinceNameEn": "BeiJing City",
    "code": "0001",
    "cityNameEn": "Workshop"
  }

UrlPattern:正則匹配任何 6 位數的,例如:/customer/123456/

mappings:cities-mapping.json

{        
    "request": {                                    
        "method": "GET",                        
        "urlPattern": "/customer/[0-9]{6}/"
    },                                      
    "response": {                                   
        "status": 200,                          
        "bodyFileName": "/customer.json",
        "headers": {
            "Content-Type": "application/json",
            "Cache-Control": "max-age=86400"
        }
    }                                               
}

__files:customer.json

{
  "channels": [],
  "code": "781",
  "companyName": "",
  "enable": true,
  "name": "163",
  "password": "[email protected]",
  "userState": "COMMERCIAL"
}

高效平臺化

使用 WireMock 通過 mappings 和 __files 文件夾可以有效管理映射和返回內容文件,但是所有文件的有部分可抽取未固定模板,而這些部分目前是手動編輯,關注這些部分會分散業務的精力,如果可以做成平臺化管理,所有接口通過創建完成,文件命名規則全部由系統進行管理,將節省的時間更多投入業務關注和及早進行自測,這樣子的收益將會更大。

那怎麼樣的平臺纔算能夠滿足當前需求呢?

  • 基於 HTTP 協議
  • 支持 Url、UrlPattern 匹配
  • 支持數據存儲
  • API 接口規範化管理
  • 提交表單即可生成 mapping 和 __files 所需文件
  • 不同項目接口有不同的前綴
  • 能夠返回指定格式(json|xml|文本)內容

圖 -4-ServerMock-v1.0- 架構圖

圖 -4-ServerMock-v1.0- 架構圖

根據架構圖,做了總體規劃如下:

圖 -5-ServerMock-v1.0 規劃

圖 -5-ServerMock-v1.0 規劃

如果對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣可以175317069,羣內會有不定期的發放免費的資料鏈接,這些資料都是從各個技術網站蒐集、整理出來的,如果你有好的學習資料可以私聊發我,我會註明出處之後分享給大家。

技術選型

由於原來的測試平臺使用 Python 編寫,爲了保持風格一致,從界面錄入到文件生成處理依然採用 Python,後臺工具使用 WireMock 的 standalone 模式,通過 shell 腳本進行一鍵啓停管理,以及實時刷新 url、mapping 映射;

HTTP API Mock 項目管理 Web 前臺

使用 Python+Django+MySQL 進行開發,分爲項目配置和接口配置兩大部分。

項目配置頁

介紹:配置協議、進行 mock 服務器的重啓、重新加載(有新的接口文件生成系統會自動 reset 即可,當然手工 reset 也可以,即時加載無須重啓服務等待)。

圖 -6- 項目配置頁

圖 -6- 項目配置頁

接口列表頁

介紹:展示列表,列出相關 URL、方法、是否正則、返回碼、返回類型。

圖 -7- 接口列表頁

圖 -7- 接口列表頁

接口配置頁

介紹:選擇方法、URL 類型,填寫 URL(如果選擇 URL 類型爲 UrlPattern,則填寫正則表達式),填寫狀態碼、返回接口,以及返回頭,就可以完成一個 mock 接口的創建。

圖 -8- 接口配置頁

圖 -8- 接口配置頁

接口配置有三種輸入形式:

直接輸入返回結果

圖 -9- 手工輸入

圖 -9- 手工輸入

一般場景在返回結果 500k 以內的內容,可以直接輸入,保存進入數據庫;

通過 url 抓取返回結果

圖 -10-url 抓取

圖 -10-url 抓取

一般場景在返回結果超過 500k 以上內容,目標 Mock 接口已經存在,可以直接抓取生成文件;

通過文件上傳返回結果

圖 -11- 上傳文件

圖 -11- 上傳文件

一般場景在返回結果比較大|目標 Mock 接口還未開發完成,手工上傳返回內容的文件即可。

以上三種靈活的保存返回內容方式,最終保存的接口會按照以下格式生成 mapping 和 __files 所需文件:

圖 -12-mapping 和 __files 文件格式

圖 -12-mapping 和 __files 文件格式

Mock 項目管理 Server 後臺

使用 Java-WireMock 進行後臺服務,在項目配置頁通過按鈕:重啓、重新加載,調用後臺腳本:wiremock_controller.sh,僅供參考:

#!/bin/bash
if [ "$#" = 0 ];then
   echo "Usage: $0 (start|stop|restart|reset)"
   exit 1
fi

dirWiremock=`pwd`
getCount=`ps -ef | grep "wiremock-1.53-standalone" | grep -v "grep" |wc -l`
wiremock_jar=${dirWiremock}/wiremock-1.53-standalone.jar
port=9999
wiremock_url=http://localhost:${port}

stop(){
   count=${getCount}
   if [ 1==${count} ];then
   curl -d log=aaa ${wiremock_url}/__admin/shutdown
   echo "Stop success!......"
   else
       echo "Already stop"
   fi
}

start(){
   count=${getCount}
   if [ 0==${count} ];then
      nohup java -jar ${wiremock_jar} --verbose=true --port=${port} &    
      echo "Start success!......"
   else
       echo "Already start"
   fi
}

if [ "$1" = "restart" ];then
   count=${getCount}
   if [ 1==${count} ];then
         echo "Wiremock is running,wait for restarting! ...."
         stop  
         echo "Start wiremock......"
         start  
   else
       start
   fi

elif [ "$1" = "start" ];then
       echo "Start wiremock......"
       start

elif [ "$1" = "stop" ];then
       echo "Stop wiremock......"
       stop

elif [ "$1" = "reset" ];then
   count=${getCount}
   if [ 0==${count} ];then
         echo "Wiremock must be running before reset,wait for starting! ...."
         start
   fi
       curl -d log=aaa  ${wiremock_url}/__admin/mappings/reset
   echo "Reset success!......"
fi

其中:

“nohup java -jar {wiremock_jar} --verbose=true --port={port} &”:在 linux 系統後臺運行 WireMock;

“curl -d log=aaa ${wiremock_url}/__admin/mappings/reset”:是通過發送 POST 請求,重新加載新生成的配置文件,在 WireMock 的源碼中可以看到:reset 的作用:

public interface Admin {
    void addStubMapping(StubMapping stubMapping);
    ListStubMappingsResult listAllStubMappings();
    void saveMappings();
    void resetMappings();
    void resetScenarios();
    void resetToDefaultMappings();
    VerificationResult countRequestsMatching(RequestPattern requestPattern);
    FindRequestsResult findRequestsMatching(RequestPattern requestPattern);
    void updateGlobalSettings(GlobalSettings settings);
    void addSocketAcceptDelay(RequestDelaySpec spec);
    void shutdownServer();
}

通過一系列源碼追溯,可以找到重置:

@Override
    public void reset() {
        mappings.clear();
        scenarioMap.clear();
    }

可以推測映射文件是存放到列表的:

public class SortedConcurrentMappingSet implements Iterable<StubMapping>{

    private AtomicLong insertionCount;
    private ConcurrentSkipListSet<StubMapping> mappingSet;
......
}

當 WireMock 啓動,日誌有以下描述:

2015-02-12 11:38:37.844 Verbose logging enabled
2015-02-12 11:38:38.657:INFO::Logging to STDERR via wiremock.org.mortbay.log.StdErrLog
2015-02-12 11:38:38.664 Verbose logging enabled
 /$      /$ /$                     /$      /$                     /$      
| $  /$ | $|__/                    | $$    /$$                    | $      
| $ /$$| $ /$  /$$$   /$$$ | $$  /$$  /$$$   /$$$$| $   /$
| $/$ $ $| $ /$__  $ /$__  $| $ $/$ $ /$__  $ /$_____/| $  /$/
| $$_  $$| $| $  \__/| $$$$| $  $$| $| $  \ $| $      | $$$/ 
| $$/ \  $$| $| $      | $_____/| $\  $ | $| $  | $| $      | $_  $ 
| $/   \  $| $| $      |  $$$$| $ \/  | $|  $$$/|  $$$$| $ \  $
|__/     \__/|__/|__/       \_______/|__/     |__/ \______/  \_______/|__/  \__/

port:                         9999
enable-browser-proxying:      false
no-request-journal:           false
verbose:                      true

如果對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣可以175317069,羣內會有不定期的發放免費的資料鏈接,這些資料都是從各個技術網站蒐集、整理出來的,如果你有好的學習資料可以私聊發我,我會註明出處之後分享給大家。

圖 -13-WireMock 啓動

圖 -13-WireMock 啓動

成功處理請求的日誌:

2015-02-12 11:41:10.320 Received request: GET /test/today/dkfDF123/1234/ HTTP/1.1
Host: 192.168.32.55:9999
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: csrftoken=alXbvCtMyTBI1wnSnRoljguTaBnTDbPo; sessionid=tvoi9rzs66umnt1a26wsj36eqry2e2lo
Connection: keep-alive

總結

HTTP API 接口測試痛點是什麼?很多公司劃分不同研發組,各組系統之間的數據交互通過接口來實現,那很多時候就是集中在接口開發不同步,測試無法及早參與,對接調試難的問題。或許很多團隊遇到這種問題,就是選擇同步開發或者等待。當你選擇等待的時候,你的產品質量就得不到及時驗證,因爲根本沒有測試過,在當前快速迭代的開發模式中,時間是最致命的要素,如果不能及時交付,交付的質量又得不到保證,那是相當被動的局面,最後返工的成本比你當時願意追加測試的成本會來的更高。

遇到這類問題是想辦法解決,而不是迴避,我們可以使用 Mockito 對依賴進行 Mock,那同樣道理,使用 Mock 技術也可以對 HTTP API 進行 Mock,按照這個思路探索下去,看看有沒有開源解決方案,是否能夠解決當前問題,如果可以就不用重複寫一套解決方案;如果不行,那能否基於開源的做二次開發呢?當團隊經歷過測試痛點,調研收集了一定的數據,這些問題的答案就會浮出水面了。

或許有人要問,使用之後能夠提高多少效率呢?看回《圖 -2- 改進的接口對接調試流程》,根據我們的經驗,要統計當前迭代中有多少 API 需要對接調試,如果對比舊的模式來說,API 接口調試效率提升至少有 10%;可想而知,迭代中全是依賴 API 接口開發的話,那提升的效率就相當可貴了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章