1. 程式人生 > >使用WireMock進行更好的整合測試

使用WireMock進行更好的整合測試

無論您是遵循傳統的測試金字塔還是採用諸如“測試蜂窩”這樣的較新方法,都應該在開發過程中的某個時候開始編寫整合測試用例。
您可以編寫不同型別的整合測試。從永續性測試開始,您可以檢查元件之間的互動,也可以模擬呼叫外部服務。本文將討論後一種情況。
在談論WireMock之前,讓我們從一個典型的例子開始。

ChuckNorrisService

我們有一個簡單的API,用於手動測試。在“業務”類意外是,它可以呼叫外部API。它使用Spring 框架提供功能的。沒什麼特別的。我多次看到的是模擬RestTemplate並返回一些預先確定的答案的測試。該實現可能如下所示:

@Service
public class ChuckNorrisService{
...
  public ChuckNorrisFact retrieveFact() {
    ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
    return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
  }
 ...
 }

在檢查成功案例的常規單元測試旁邊,將至少有一項覆蓋HTTP錯誤碼的測試用例,即4xx或5xx狀態程式碼:

@Test
  public void shouldReturnBackupFactInCaseOfError() {
    String url = "http://localhost:8080";
    RestTemplate mockTemplate = mock(RestTemplate.class);
    ResponseEntity<ChuckNorrisFactResponse> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    when(mockTemplate.getForEntity(url, ChuckNorrisFactResponse.class)).thenReturn(responseEntity);
    ChuckNorrisService service = new ChuckNorrisService(mockTemplate, url);
    ChuckNorrisFact retrieved = service.retrieveFact();
    assertThat(retrieved).isEqualTo(ChuckNorrisService.BACKUP_FACT);
  }

看起來還不錯吧?響應實體返回503錯誤程式碼,我們的服務不會崩潰。所有測試都是綠色通過的,我們可以部署我們的應用程式。
不幸的是,Spring的RestTemplate不能這樣使用。方法簽名getForEntity給了我們很小的提示。它指出throws RestClientException。這就是mock的RestTemplate與實際實現不同的地方。我們將永遠不會收到ResponseEntity帶有4xx或5xx狀態程式碼的。RestTemplate將丟擲的子類RestClientException。通過檢視類的層次結構,我們可以對可能丟擲的結果有一個很好的印象:

因此,讓我們看看如何使這項測試更好。

WireMock進行拯救

WireMock通過啟動模擬伺服器並返回將其配置為返回的答案來模擬Web服務。得益於出色的DSL,它很容易整合到您的測試中,並且模擬請求也很簡單。

對於JUnit 4,有一個WireMockRule有助於啟動停止伺服器的工具。對於JUnit 5,大概需要自己做一個這樣的工具。當您檢查示例專案時,您可以找到ChuckNorrisServiceIntegrationTest。這是基於JUnit 4的SpringBoot測試。讓我們看一下。

最重要的部分是ClassRule:

@ClassRule
  public static WireMockRule wireMockRule = new WireMockRule();

如前所述,這將啟動和停止WireMock伺服器。您也可以像往常一樣使用該規則Rule來啟動和停止每個測試的伺服器。對於我們的測試,這不是必需的。

接下來,您將看到幾種configureWireMockFor...方法。這些包含WireMock何時返回答案的說明。將WireMock配置分為幾種方法並從測試中呼叫它們是我使用WireMock的方法。當然,您可以在一個@Before方法中設定所有可能的請求。對於正確使用的Demo,我們這樣做:

public void configureWireMockForOkResponse(ChuckNorrisFact fact) throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", fact);
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))));
  }

所有方法都是從靜態匯入的com.github.tomakehurst.wiremock.client.WireMock。如您所見,我們將HTTP GET存入路徑/jokes/random並返回JSON物件。該okJson()方法只是帶有JSON內容的200響應的簡寫。對於錯誤情況,程式碼甚至更簡單:

private void configureWireMockForErrorResponse() {
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(serverError()));
  }

如您所見,DSL使閱讀說明變得容易。將WireMock放置在適當的位置,我們可以看到我們先前的實現不起作用,因為RestTemplate引發了異常。因此,我們必須調整程式碼:

public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (HttpStatusCodeException e){
      return BACKUP_FACT;
    }
  }

這已經涵蓋了WireMock的基本用例。配置請求的答案,執行測試,檢查結果,so easy,就這麼簡單。儘管如此,在雲環境中執行測試時通常會遇到一個問題。讓我們看看我們能做什麼。

動態埠上的WireMock

您可能已經注意到,專案中的整合測試包含一個
ApplicationContextInitializer類,並且其@TestPropertySource註釋會覆蓋實際API的URL。那是因為我想在隨機埠上啟動WireMock。當然,您可以為WireMock配置一個固定埠,並在測試中將此埠用作常量來處理。但是,如果您的測試在某些雲提供商的基礎架構上執行,則無法確定該埠是否可用。因此,我認為隨機埠更好。

不過,在Spring應用程式中使用屬性時,我們必須以某種方式將隨機埠傳遞給我們的服務。或者,如您在示例中看到的那樣,覆蓋URL。這就是為什麼我們使用ApplicationContextInitializer。我們將動態分配的埠新增到應用程式上下文中,然後可以使用屬性來引用它${wiremock.port}。這裡唯一的缺點是我們現在必須使用ClassRule。否則,我們無法在初始化Spring應用程式之前訪問埠。

解決了此問題後,讓我們看一下涉及HTTP呼叫的一個常見問題。

超時時間

WireMock提供了更多的響應可能性,而不僅僅是對GET請求的簡單答覆。經常被遺忘的另一個測試案例是測試超時。開發人員往往會忘記在RestTemplate設定超時URLConnections。如果沒有超時,則兩者都將等待無限量的時間來進行響應。在最好的情況下,在最壞的情況下,所有執行緒都將等待永遠不會到達的響應。

因此,我們應該新增一個模擬超時的測試。當然,我們也可以使用Mockito模擬來建立延遲,但是在這種情況下,我們將再次猜測RestTemplate的行為。使用WireMock模擬延遲非常簡單:

private void configureWireMockForSlowResponse() throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, ""));
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(
            okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))
                .withFixedDelay((int) Duration.ofSeconds(10L).toMillis())));
  }

withFixedDelay()期望一個表示毫秒的int值。我更喜歡使用Duration或至少一個表示該引數表示毫秒的常量,而不必每次寫程式碼都需要看一下程式碼註釋。

設定超時RestTemplate並新增響應的測試後,我們可以看到RestTemplate丟擲ResourceAccessException。因此,我們可以調整catch塊以捕獲此異常和,HttpStatusCodeException或者僅捕獲兩者的超類:

public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (RestClientException e){
      return BACKUP_FACT;
    }
  }

現在,我們已經很好地介紹了執行HTTP請求時最常見的情況,並且可以確定我們正在測試接近真實條件的條件。

為什麼不?

HTTP整合測試的另一個選擇是Hoverfly。它的工作原理類似於WireMock,但我更喜歡後者。原因是在執行包含瀏覽器的端到端測試時,WireMock也非常有用。Hoverfly(至少是Java庫)受JVM代理的限制。這可能使它比WireMock更快,但是當例如某些JavaScript程式碼開始起作用時,它根本不起作用。當您的瀏覽器程式碼也直接呼叫其他一些服務時,WireMock啟動Web伺服器這一功能非常有用。然後,您也可以使用WireMock來mock它們,並編寫例如Selenium測試。

結論

本文可以向您展示兩件事:

  • 整合測試的重要性
  • WireMock是個非常不錯的測試框架

當然,這兩個主題都可以寫出非常多的文章。儘管如此,還是分享瞭如何使用WireMock及其功能。在以後的學習路上多去閱讀他們的文件,然後嘗試更多其他功能,例如利用WireMock來進行身份驗證。


  • 鄭重宣告:文章禁止第三方(騰訊雲除外)轉載、發表,事情原委測試窩,首頁抄我七篇原創還拉黑,你們的良心不會痛嗎?