使用WireMock進行更好的集成測試

不管您是遵循傳統的測試金字塔仍是採用諸如「測試蜂窩」這樣的較新方法,都應該在開發過程當中的某個時候開始編寫集成測試用例。
您能夠編寫不一樣類型的集成測試。從持久性測試開始,您能夠檢查組件之間的交互,也能夠模擬調用外部服務。本文將討論後一種狀況。
在談論WireMock以前,讓咱們從一個典型的例子開始。git

ChuckNorrisService

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

@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測試。讓咱們看一下。dom

最重要的部分是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來進行身份驗證。


相關文章
相關標籤/搜索