歡迎光臨
每天分享高質量文章

微服務環境下的整合測試探索

現在微服務很流行,企業架構微服務化的確能解決不少問題,但是在微服務環境下,服務之間的依賴以及由此造成的開發、測試和整合的問題,一直都是微服務最大的痛點。
傳統的解決方案是,除了測試、預釋出和生產環境,還會部署多套用於開發和整合的環境。這樣存在的問題是,只要有一組服務出現問題,就會影響其他使用該環境的團隊的日常開發和測試。而且常常出現問題後,需要耗費很多時間定位,結果還常常是因為某個服務的版本沒有同步。並且多套環境維護起來也是一個麻煩重重,即使有了容器。
這次我們一起來探索一下 API 模擬工具以及基於契約的測試,也許會是解決這個問題的一個方案。

WireMock 介紹

我們開發應用也好、服務也好,常常需要依賴後端或者服務的介面。例如開發移動應用 App,可能後端介面還在開發中,這時 App 的開發因為無法呼叫後端,很不方便。又或者程式會依賴第三方的介面,例如微信支付,在本地開發時不能直接呼叫。
這時我們就會需要一個工具來模擬這些服務,WireMock 就是這樣的一個工具,主要針對的是最常見的 HTTP 服務。

WireMock 用於開發除錯

WireMock 首先自身就是一個可以獨立執行的服務。下載 Standalone Jar 檔案後,即可可以直接執行。
java -jar wiremock-standalone-2.11.0.jar
此時可以透過 Json 對映檔案來定義 Stub 服務。例如下麵是一個對映檔案,request 部分設定匹配的 Url 路徑、請求方法及引數,如果匹配到了,則會傳回 response 部分設定的內容。把該檔案放到 WireMock 同路徑下的 mappings目錄下即可。
{
 "request" : {
   "urlPath" : "/api/order/find",
   "method" : "GET",
   "queryParameters" : {
     "orderId" : {
       "matches" : "^[0-9]{16}$"
     }
   }
 },
 "response" : {
   "status" : 200,
   "bodyFileName" : "body-order-find-1.json",
   "essay-headers" : {
     "Content-Type" : "application/json;charset=UTF-8"
   }
 }
}
Response 的內容可以直接在對映檔案裡設定,也可以取用了另一個檔案。這裡是取用了一個名為 body-order-find-1.json 的檔案,該檔案放置在 WireMock 同路徑下的 __files 目錄下。
{
   "success": true,
   "data": {
       "id": 781202,
       "buyerId": -2,
       "status": 0,
       // 略...
   }
}
下麵我們用 curl 測試一下。第一次我們請求的引數 orderId 無法匹配指定的正則,WireMock 會傳回 Request was not matched,而且還會很貼心的告訴你最接近的匹配是什麼。
$ curl http://localhost:8080/api/order/find?orderId=abcdefghijklmnop
          Request was not matched
          =======================
----------------------------------------------
| Closest stub         | Request
----------------------------------------------
GET                    | GET
/api/order/find        |
/api/order/find
----------------------------------------------
第二次我們引數 orderId 匹配的話,WireMock 會直接傳回設定的結果。
$ curl http://localhost:8080/api/order/find?orderId=9999999999999999
{
   "success": true,
   "data": {
       "id": 781202,
       "buyerId": -2,
       "status": 0
   }
}
上面的例子是 WireMock 最基本的用法,除了請求匹配響應,WireMock 也能支援:
  • 透過 RESTFul 的介面提交和管理請求對映和相應。

  • 支援響應模板,傳回內容時會將變數填充到響應模板中。當然,這裡的模板功能是比較簡單的,但對於大部分 Stub 的場景應該是足夠了。

  • 支援模擬異常傳回,例如設定有一定比例的超時傳回等等,這個功能用於測試非常方便。

為了方便編寫請求對映檔案,WireMock 還可以執行在代理樣式,只需要執行時新增 –enable-browser-proxying 引數即可。此時 WireMock 匹配到請求後,不是傳回指定的內容,而是把請求 Forword 到指定的 URL,獲得 Response 後再傳回給呼叫方。同時,WireMock 會記錄請求和傳回的內容,生成 Json 對映檔案。使用時只要根據需求對這些對映檔案做一定修改,既可以用來模擬標的服務。

WireMock 用於整合測試

除了獨立執行,WireMock 也可以直接嵌入到程式碼中。最方便的就是在 JUnit 中使用,WireMock 提供了 WireMockRule, 可以很方便的在測試時嵌入一個 Stub 服務。
下麵是一個支付相關的整合測試,被測試方法會呼叫微信的支付服務。stubForUnifiedOrderSuccess 設定了一個很簡單的 Stub,一旦匹配到請求的 URL 為 /pay/unifiedorder,那就傳回指定的 XML 內容。這樣我就可以在整合測試裡測試整個支付流程,而不必依賴真正的微信支付。當然,測試時微信支付介面的 Host 也要改成 WireMockRule 配置的本地埠。並且,透過這種方式也很容易測試一些異常情況,根據需要修改 Stub 傳回的內容即可。
public class OrderTest {
   @Rule
   public WireMockRule wireMockRule = new WireMockRule(9090);
   /**    
    * 統一下單 Stub    
    * 參考 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1    
    *    
    * @param tradeType 交易型別,可以是JSAPI、NATIVE或APP    
    */
   public void stubForUnifiedOrderSuccess(String tradeType) {
       String unifiedOrderResp = "<xml>\n" +
               "    <return_code>return_code>

\n” +
               ”    <return_msg>return_msg>\n” +
               ”    <appid>appid>\n” +
               ”    <mch_id>mch_id>\n” +
               ”    …… \n” +
               ”    <trade_type>trade_type>\n” +
               “xml>”;
       stubFor(post(urlEqualTo(“/pay/unifiedorder”))
               .withHeader(“Content-Type”, equalTo(“text/xml;charset=UTF-8”))
               .willReturn(aResponse()
                       .withStatus(200)
                       .withHeader(“Content-Type”, “text/plain”)
                       .withBody(unifiedOrderResp)));
   }
   @Test
   public void test001_doPay() {
      stubForUnifiedOrderSuccess(“JSAPI”);
      payServices.pay();
       // 測試程式碼…
   }
}

有時候在整合測試裡,我們還需要驗證系統的行為,例如是否呼叫了某個 API,呼叫了幾次,呼叫的引數和內容是否符合要求等。區別於前面說的 Stub,其實這就是常說的 Mock 功能。WireMock 對此也有很強大的支援。
verify(postRequestedFor(urlEqualTo("/pay/unifiedorder"))
       .withHeader("Content-Type", equalTo("text/xml;charset=UTF-8"))
       .withQueryParam("param", equalTo("param1"))
       .withRequestBody(containing("success"));
這樣,有了 WireMock,整合測試時處理第三方的依賴就非常方便了。不需要直接呼叫依賴的服務,也不需要專門建立用於整合測試的 Stub 或 Mock,直接程式碼中根據需要設定即可。

WireMock 總結

總結一下, WireMock 可以:
  • 作為代理執行,此時可以錄製請求和傳回的指令碼,用於後繼 Stub 和 Mock 使用。

  • 獨立執行,作為一個 Stub 服務,根據匹配的請求傳回資料。

  • 作為 Stub,透過程式碼嵌入 HTTP 模擬服務,在指定埠監聽,並根據匹配的請求傳回資料。

  • 作為 Mock,在單元測試和整合測試中,驗證請求邏輯。例如是否進行了呼叫、引數是否正確等。

這裡再強調下 Stub 和 Mock 的區別,很多人經常搞混。Stub 就是一個純粹的模擬服務,用於替代真實的服務,收到請求傳回指定結果,不會記錄任何資訊。Mock 則更進一步,還會記錄呼叫行為,可以根據行為來驗證系統的正確性。
我們可以用 WireMock 來最佳化開發和整合的流程。
  • 在外部服務尚未開發完成時,模擬服務,方便開發。

  • 在本地開發時,模擬外部服務避免直接依賴。

  • 在單元測試中模擬外部服務,同時驗證業務邏輯。

契約式測試

本文主要以 WireMock 為例介紹了 API 模擬工具的使用方法。其實除了 WireMock,還有不少類似的工具,例如最早的 MounteBank,以及 MockServer、Moco 等也都是很強大的工具。
不過,在微服務環境下,光有 API 模擬工具還不夠。對於 WireMock,首先必須考慮如何來管理大量的對映檔案。一個方法是開發一個專用的 Stub 平臺,來管理所有的對映檔案,同時作為 Stub 執行。另外一個方法是透過 Git 來管理對映檔案,需要的時候同步下來執行 WireMock 即可。
另外,我們上面提到 WireMock 的兩大作用,呼叫方模擬服務以及服務方整合測試,是否可以統一兩者呢?也就是說,呼叫方和服務方約定好介面,生成對映檔案,這個檔案即可以用於客戶端模擬服務,也可以用於服務方整合測試,這樣雙方開發也好、整合也好都會方便很多。下麵我們來研究一下 Spring Cloud Contract,它就是基於 WireMock 實現了契約式的測試,上文中雙方約定好的介面,其實就是雙方的契約。

微服務的整合

前面已經提到,傳統方式下,微服務的整合以及測試都是一件很頭痛的事情。其實在微服務概念還沒有出現之前,在 SOA 流行的時候,就有人提出了消費者驅動契約(Consumer Driven Contract,CDC)的概念。微服務流行後,服務的整合和整合測試成了不得不解決問題,於是出現了基於消費者驅動契約的測試工具,最流行的應該就是 Pact,還有就是今天我們要說的 Spring Cloud Contract。

消費者驅動契約

熟悉敏捷開發的同學應該知道,敏捷開發提倡測試先行,相應的提出了不少方法和流程,例如測試驅動開發(Test Driven Design,TDD)、驗收測試驅動開發(Acceptance Test Driven Development,ATDD)、行為驅動設計(Behavior Driven Design,BDD )、實體化需求(Specification By Example)等等。它們的共同特點在開發前就約定好了各種形式的契約。如果是單元測試作為契約,就是 TDD;如果是驗收測試作為契約,就是 ATDD;如果是形式化語言甚至圖表定義的業務規則,那就是 BDD 或者實體化需求。
對於基於 HTTP 的微服務來說,它的契約就是指 API 的請求和響應的規則。對於請求,包括請求 URL 及引數,請求頭,請求內容等;對於響應,包括狀態碼,響應頭,響應內容等。
在 Spring Cloud Contract 裡,契約是用一種基於 Groovy 的 DSL 定義的。例如下麵是一個簡訊介面的契約(省略了部分內容,例如 Content-Type 頭等)。
org.springframework.cloud.contract.spec.Contract.make {
   // 如果消費方發送了一個請求
   request {                
       // 請求方法是 POST              
       method 'POST'    
       // 請求 URL 是 `/sendsms`                  
       url '/sendsms'      
       // 請求內容是 Json 文字,包括電話號碼和要傳送的文字                
       body([        
              // 電話號碼必須是13個數字組成                      
              phone: $(regex('[0-9]{13}')),  
              // 傳送文字必須為"您好"
              content: "您好"                
       ])
   }
   response {
       // 那麼服務方應該傳回狀態碼 200
       status 200        
       // 響應內容是 Json 文字,內容為 { "success": true }                  
       body([                              
              success: true
       ])
   }}
使用 CDC 開發服務的大致過程是這樣的。
  1. 業務方和服務方相關人員一起討論。業務方告知服務方介面使用的場景、期望的傳回是什麼,服務方考慮介面方案和實現,雙方一起定下一個或多個契約。

  2. 確定了契約之後,Spring Cloud Contract 會給服務方自動生成驗收測試,用於驗證介面是否符合契約。服務方要確保開發完成後,這些驗收測試都能夠透過。

  3. 業務方也可以基於這個契約開始開發功能。Spring Cloud Contract 會基於契約生成 Stub 服務,這樣業務方就不必等介面開發完成,可以透過 Stub 服務進行整合測試。

所以 CDC 和行為驅動設計(BDD)很類似,都是從使用者的需求出發,雙方訂立契約,測試先行的開發方法。不過一個是針對系統的驗收,一個是針對服務的整合。CDC 的好處有以下幾點:
  • 讓服務方和呼叫方有充分的溝通,確保服務方提供介面都是以呼叫方的需求出發,並且服務方的開發者也可以充分理解呼叫方的使用場景。

  • 解耦和服務方和呼叫方的開發過程,一旦契約訂立,雙方都可以並行開發,透過 Mock 和自動化整合測試確保雙方都遵守契約,最終整合也會更簡單。

  • 透過 Mock 和自動化測試,可以確保雙方在演進過程中,也不會破壞已有的契約。

但是要註意一點是,契約不包括業務邏輯,業務邏輯還是需要服務方和呼叫方透過單元測試、其他整合測試來確保。例如上面的簡訊服務,可能服務方會有一個邏輯是每天一個號碼最多傳送一條簡訊,但這個邏輯並不會包含在契約裡,可能契約只有包含成功和錯誤兩種情況。
Spring Cloud Contract 使用方法

服務方
Spring Cloud Contract 支援 Gradle 和 Maven,詳細的配置檔案就不細述了,請參考檔案。對於服務方,Spring Cloud Contract 提供了一個叫 Contarct Verifier 的東西,用於解析契約檔案生成測試。
如果使用 Gradle 的話,透過以下命令生成測試。
./gradlew generateContractTests
上面傳送簡訊的契約,生成的測試程式碼是這樣的。
public class SmsTest extends ContractBase {
   @Test
   public void validate_sendsms() throws Exception {
       // given:
           MockMvcRequestSpecification request = given()
                   .body("{\"phone\":\"2066260255168\",\"content\":\"\u60A8\u597D\"}");
       // when:
           ResponseOptions response = given().spec(request)
                   .post("/sendsms");
       // then:
           assertThat(response.statusCode()).isEqualTo(200);
       // and:
           DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
           assertThatJson(parsedJson).field("['success']").isEqualTo(true);
   }
}
可以看到是一個很標準的 JUnit 測試,使用了 RestAssured 來測試 API 介面。其中的 ContractBase 是設定的測試基類,裡面可以做一些配置以及 Setup 和 Teardown 操作。例如這裡,我們需要用 RestAssured 來啟動 Spring 的 webApplicationContext,當然我也可以用 standaloneSetup 設定啟動單個 Controller。

@Before
public void setup() {
   RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
}

呼叫方
首先我們需要在服務方透過以下命令生成 Stub 服務的 Jar 包。

./gradlew verifierStubsJar

這個 Jar 包裡麵包含了契約檔案以及生成的 WireMock 對映檔案。我們可以把它釋出到 Maven 私庫裡去,這樣呼叫方可以直接從私庫下載 Stub 的 Jar 包。
對於呼叫方,Spring Cloud Contract 提供了 Stub Runner 來簡化 Stub 的使用。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(repositoryRoot="http://",
       ids = {"com.xingren.service:sms-client-stubs:1.5.0-SNAPSHOT:stubs:6565"})
public class ContractTest {
   @Test
   public void testSendSms() {
       ResponseEntity response =
       restTemplate.exchange("http://localhost:6565/sendsms", HttpMethod.POST,
               new HttpEntity<>(request), SmsServiceResponse.class);
       // do some verification
   }
}
註意註解 AutoConfigureStubRunner,裡面設定了下載 Stub Jar 包的私庫地址以及包的完整 ID,註意最後的 6565 就是指定 Stub 執行的本地埠。測試的時候訪問 Stub 埠,就會根據契約傳回內容。
前端開發
另外一個使用 Mock 的場景就是對於前端開發。以前,前端工程師一般需要自己建立 Mock 資料進行開發,但 Mock 資料很容易和後臺最終提供的資料有不一致的地方。CDC 和 Spring Cloud Contract 也可以幫上忙。
Spring Cloud Contract 生成的 Stub 其實是 WireMock 的對映檔案,因此直接使用 WireMock 也是可以的。不過,它還提供了使用 Spring Cloud Cli 執行 Stub 的方式。
首先需要安裝 SpringBoot Cli 和 Spring Cloud Cli,Mac 下可以使用 Homebrew。
$ brew tap pivotal/tap
$ brew install springboot
$ spring install org.springframework.cloud:spring-cloud-cli:1.4.0.RELEASE
然後在當前目錄建立一個 stubrunner.yml 配置檔案,裡面的配置引數和前面的 AutoConfigureStubRunner 的配置其實是一樣的:
stubrunner:
 workOffline: false
 repositoryRoot: http://
 ids:
   - com.xingren.service:sms-client-stubs:1.5.0-SNAPSHOT:stubs:6565
最後執行 spring cloud stubrunner,即可啟動 Stub 服務。前端同學就可以愉快的使用 Stub 來進行前端開發了。
DSL
Spring Cloud Contract 的契約 DSL,既可以用於生成服務方的測試,也可以用於生成供呼叫方使用的 Stub,但是這兩種方式對資料的驗證方法有一些不同。對於服務方測試,DSL 需要提供請求內容,驗證響應;而對於 Stub,DSL 需要匹配請求,提供響應內容。Spring Cloud Contract 提供了幾種方式來處理。
一種方式是透過 $(consumer(…), producer(…)) 的語法(或者$(stub(…), test(…))、$(client(…), server(…))、$(c(…), p(…)),都是一樣的)。例如:
org.springframework.cloud.contract.spec.Contract.make {
   request {
       method('GET')
       url $(consumer(~/\/[0-9]{2}/), producer('/12'))    
      }  
   response {        
       status 200        
       body(                
           name: $(consumer('Kowalsky'), producer(regex('[a-zA-Z]+')))
       )
   }
}
上面就是指對於呼叫方,url 需要匹配 ~/\/[0-9]{2}/ 這個正則運算式,Stub 就會傳迴響應,其中 name 則為 Kowalsky。而對於服務方,生產的測試用例的請求 url 為 /12,它會驗證響應中的 name 符合正則 ‘[a-zA-Z]+’。另外,Spring Cloud Contract 還提供了 stubMatchers 和 testMatchers 來支援更複雜的請求匹配和測試驗證。
Spring Cloud Contract 現在還在快速發展中,目前對於生成測試用例的規則,還是有不夠靈活的地方。例如,對於某些 Stub 應該傳回,但生成的測試裡不需要驗證的欄位,支援不太完善。還有對於 form-urlencoded 的請求,處理起來不如 Json 的請求那麼方便。相信後繼版本會改善。

總結

透過上面簡單介紹,我們可以看到基於 Spring Cloud Contract 以及契約測試的方法,可以讓微服務之間以及前後端之間的整合更順暢。
另外前面還提到 Pact,它的優勢是支援多種語言,但我們的環境都是基於 JVM 的,而 Spring Cloud Contract 和 SpringBoot 以及 Junit 的整合更簡單方便。而且 Spring Cloud Contract 的另一個優勢是它可以自動生成服務方的自動化測試。
本文轉載自公眾號:杏仁技術站,點選閱讀原文

基於Kubernetes的DevOps實踐培訓

本次培訓包含:Kubernetes核心概念;Kubernetes叢集的安裝配置、運維管理、架構規劃;Kubernetes元件、監控、網路;針對於Kubernetes API介面的二次開發;DevOps基本理念;微服務架構;微服務的容器化等,點選識別下方二維碼加微信好友瞭解具體培訓內容

點選閱讀原文連結即可報名。
贊(0)

分享創造快樂