開發與維運

Spring 5 中文解析測試篇-Spring MVC測試框架

3.6 Spring MVC測試框架

Spring MVC測試框架提供了一流的支持,可使用可與JUnit、TestNG或任何其他測試框架一起使用的流暢API測試Spring MVC代碼。它基於spring-test模塊的Servlet API模擬對象構建,因此不使用運行中的Servlet容器。它使用DispatcherServlet提供完整的Spring MVC運行時行為,並支持通過TestContext框架加載實際的Spring配置以及獨立模式,在獨立模式下,你可以手動實例化控制器並一次對其進行測試。

Spring MVC Test還為使用RestTemplate的代碼提供客戶端支持。客戶端測試模擬服務器響應,並且不使用正在運行的服務器。

Spring Boot提供了一個選項,可以編寫包括運行中的服務器在內的完整的端到端集成測試。如果這是你的目標,請參閱《 Spring Boot參考指南》。有關容器外和端到端集成測試之間的區別的更多信息,請參閱Spring MVC測試與端到端測試

3.6.1 服務端測試

你可以使用JUnit或TestNG為Spring MVC控制器編寫一個普通的單元測試。為此,實例化控制器,向其注入模擬或存根依賴性,然後調用其方法(根據需要傳遞MockHttpServletRequestMockHttpServletResponse等)。但是,在編寫這樣的單元測試時,仍有許多未經測試的內容:例如,請求映射、數據綁定、類型轉換、驗證等等。此外,也可以在請求處理生命週期中調用其他控制器方法,例如@InitBinder@ModelAttribute@ExceptionHandler

Spring MVC Test的目標是通過執行請求並通過實際的DispatcherServlet生成響應來提供一種測試控制器的有效方法。Spring MVC Test基於spring-test模塊中可用的Servlet API的“模擬”實現。這允許執行請求和生成響應,而無需在Servlet容器中運行。在大多數情況下,一切都應像在運行時一樣工作,但有一些值得注意的例外,如Spring MVC測試與端到端測試中所述。以下基於JUnit Jupiter的示例使用Spring MVC Test:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.;

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class ExampleTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void getAccount() throws Exception {
        this.mockMvc.perform(get("/accounts/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$.name").value("Lee"));
    }
}

Kotlin提供了專用的MockMvc DSL

前面的測試依賴於TestContext框架對WebApplicationContext的支持,以從與測試類位於同一包中的XML配置文件加載Spring配置,但是還支持基於Java和基於Groovy的配置。請參閱這些樣本測試

MockMvc實例用於執行對/accounts/1的GET請求,並驗證結果響應的狀態為200,內容類型為application/json,響應主體具有名為name的JSON屬性,其值為LeeJayway JsonPath項目支持jsonPath語法。本文檔後面將討論用於驗證執行請求結果的許多其他選項。

參考代碼:org.liyong.test.annotation.test.spring.WebAppTests

靜態導入

上一節中的示例中的流式API需要一些靜態導入,例如MockMvcRequestBuilders.*MockMvcResultMatchers.*MockMvcBuilders.*。 查找這些類的一種簡單方法是搜索與MockMvc *相匹配的類型。如果你使用Eclipse或Spring Tools for Eclipse,請確保在Java→編輯器→Content Assist→Favorites下的Eclipse首選項中將它們添加為“favorite static members”。這樣,你可以在鍵入靜態方法名稱的第一個字符後使用內容輔助。其他IDE(例如IntelliJ)可能不需要任何其他配置。檢查對靜態成員的代碼完成支持。

設置選項

你可以通過兩個主要選項來創建MockMvc實例。第一種是通過TestContext框架加載Spring MVC配置,該框架加載Spring配置並將WebApplicationContext注入測試中以用於構建MockMvc實例。以下示例顯示瞭如何執行此操作:

@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    // ...

}

你的第二個選擇是在不加載Spring配置的情況下手動創建控制器實例。而是自動創建基本的默認配置,該配置與MVC JavaConfig或MVC命名空間大致相當。你可以在一定程度上對其進行自定義。以下示例顯示瞭如何執行此操作:

class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
    }

    // ...

}

你應該使用哪個設置選項?

webAppContextSetup加載實際的Spring MVC配置,從而進行更完整的集成測試。由於TestContext框架緩存了已加載的Spring配置,因此即使你在測試套件中引入更多測試,它也可以幫助保持測試快速運行。此外,你可以通過Spring配置將模擬服務注入控制器中,以繼續專注於測試Web層。

下面的示例使用Mockito聲明一個模擬服務:

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="org.example.AccountService"/>
</bean>

然後,你可以將模擬服務注入測試中,以設置和驗證你的期望,如以下示例所示:

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    // ...

}

另一方面,standaloneSetup更接近於單元測試。它一次測試一個控制器。你可以手動注入具有模擬依賴項的控制器,並且不涉及加載Spring配置。這樣的測試更多地集中在樣式上,並使得查看正在測試哪個控制器,是否需要任何特定的Spring MVC配置等工作變得更加容易。standaloneSetup還是編寫臨時測試以驗證特定行為或調試問題的一種非常方便的方法。

與大多數“集成與單元測試”辯論一樣,沒有正確或錯誤的答案。但是,使用standaloneSetup確實意味著需要其他webAppContextSetup測試,以驗證你的Spring MVC配置。另外,你可以使用webAppContextSetup編寫所有測試,以便始終針對實際的Spring MVC配置進行測試。

設置功能

無論使用哪種MockMvc構建器,所有MockMvcBuilder實現都提供一些常見且非常有用的功能。例如,你可以為所有請求聲明一個Accept請求頭,並在所有響應中期望狀態為200以及Content-Type響應頭,如下所示:

// static import of MockMvcBuilders.standaloneSetup

MockMvc mockMvc = standaloneSetup(new MusicController())
    .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build();

此外,第三方框架(和應用程序)可以預先打包安裝說明,例如MockMvcConfigurer中的安裝說明。Spring框架具有一個這樣的內置實現,可幫助保存和重用跨請求的HTTP會話。你可以按以下方式使用它:

// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
        .apply(sharedHttpSession())
        .build();

// Use mockMvc to perform requests...

有關所有MockMvc構建器功能的列表,請參閱ConfigurableMockMvcBuilder的javadoc,或使用IDE探索可用選項。

執行請求

你可以使用任何HTTP方法執行請求,如以下示例所示:

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

你還可以執行內部使用MockMultipartHttpServletRequest的文件上載請求,以便不對multipart請求進行實際解析。相反,你必須將其設置為類似於以下示例:

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

你可以使用URI模板樣式指定查詢參數,如以下示例所示:

mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

你還可以添加代表查詢或表單參數的Servlet請求參數,如以下示例所示:

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

如果應用程序代碼依賴Servlet請求參數並且沒有顯式檢查查詢字符串(通常是這種情況),則使用哪個選項都沒有關係。但是請記住,隨URI模板提供的查詢參數已被解碼,而通過param(...)方法提供的請求參數已經被解碼。

在大多數情況下,最好將上下文路徑和Servlet路徑保留在請求URI之外。如果必須使用完整的請求URI進行測試,請確保相應地設置contextPathservletPath,以便請求映射起作用,如以下示例所示:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

在前面的示例中,為每個執行的請求設置contextPathservletPath將很麻煩。相反,你可以設置默認請求屬性,如以下示例所示:

class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}

前述屬性會影響通過MockMvc實例執行的每個請求。如果在給定請求上也指定了相同的屬性,則它將覆蓋默認值。這就是默認請求中的HTTP方法和URI無關緊要的原因,因為必須在每個請求中都指定它們。

定義期望

你可以通過在執行請求後附加一個或多個.andExpect(..)調用來定義期望,如以下示例所示:

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

MockMvcResultMatchers.*提供了許多期望,其中一些期望與更詳細的期望進一步嵌套。

期望分為兩大類。第一類斷言驗證響應的屬性(例如,響應狀態,標頭和內容)。這些是要斷言的最重要的結果。

第二類斷言超出了響應範圍。這些斷言使你可以檢查Spring MVC的特定切面,例如哪種控制器方法處理了請求、是否引發和處理了異常、模型的內容是什麼、選擇了哪種視圖,添加了哪些刷新屬性等等。它們還使你可以檢查Servlet的特定切面,例如請求和會話屬性。

以下測試斷言綁定或驗證失敗:

mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

很多時候,編寫測試時,轉儲已執行請求的結果很有用。你可以按照以下方式進行操作,其中print()是從MockMvcResultHandlers靜態導入的:

mockMvc.perform(post("/persons"))
    .andDo(print())
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

只要請求處理不會引起未處理的異常,print()方法會將所有有效的結果數據打印到System.out。還有一個log()方法和print()方法的兩個其他變體,一個變體接受OutputStream,另一個變體接受Writer。例如,調用print(System.err)將結果數據打印到System.err,而調用print(myWriter)將結果數據打印到自定義Writer。如果要記錄而不是打印結果數據,則可以調用log()方法,該方法將結果數據作為單個DEBUG消息記錄在org.springframework.test.web.servlet.result記錄類別下。

在某些情況下,你可能希望直接訪問結果並驗證否則無法驗證的內容。可以通過在所有其他期望之後附加.andReturn()來實現,如以下示例所示:

MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

如果所有測試都重複相同的期望,則在構建MockMvc實例時可以一次設置通用期望,如以下示例所示:

standaloneSetup(new SimpleController())
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build()

請注意,通常會應用共同的期望,並且在不創建單獨的MockMvc實例的情況下不能將其覆蓋。

當JSON響應內容包含使用Spring HATEOAS創建的超媒體鏈接時,可以使用JsonPath表達式來驗證結果鏈接,如以下示例所示:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

當XML響應內容包含使用Spring HATEOAS創建的超媒體鏈接時,可以使用XPath表達式來驗證生成的鏈接:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
    .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

異步請求

Spring MVC支持的Servlet 3.0異步請求通過存在Servlet容器線程並允許應用程序異步計算響應來工作,然後進行異步調度以完成對Servlet容器線程的處理。

在Spring MVC Test中,可以通過以下方法測試異步請求:首先聲明產生的異步值,然後手動執行異步分派,最後驗證響應。以下是針對返回DeferredResultCallable或Reactor Mono等反應類型的控制器方法的示例測試:

@Test
void test() throws Exception {
    MvcResult mvcResult = this.mockMvc.perform(get("/path"))
            .andExpect(status().isOk()) //1
            .andExpect(request().asyncStarted()) //2
            .andExpect(request().asyncResult("body")) //3
            .andReturn();

    this.mockMvc.perform(asyncDispatch(mvcResult)) //4
            .andExpect(status().isOk()) //5
            .andExpect(content().string("body"));
}
  1. 檢查響應狀態仍然不變
  2. 異步處理必須已經開始
  3. 等待並聲明異步結果
  4. 手動執行ASYNC調度(因為沒有正在運行的容器)
  5. 驗證最終響應

響應流

Spring MVC Test中沒有內置選項可用於無容器測試流響應。利用Spring MVC流選項的應用程序可以使用WebTestClient對運行中的服務器執行端到端的集成測試。Spring Boot也支持此功能,你可以在其中使用WebTestClient測試正在運行的服務器。另一個優勢是可以使用Reactor項目中的StepVerifier的功能,該功能可以聲明對數據流的期望。

註冊過濾器

設置MockMvc實例時,可以註冊一個或多個Servlet Filter實例,如以下示例所示:

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

spring-test通過MockFilterChain調用已註冊的過濾器,最後一個過濾器委託給DispatcherServlet

Spring MVC測試與端到端測試

Spring MVC Test基於spring-test模塊的Servlet API模擬實現而構建,並且不依賴於運行中的容器。因此,與使用實際客戶端和實時服務器運行的完整端到端集成測試相比,存在一些差異。

考慮這一點的最簡單方法是從一個空白的MockHttpServletRequest開始。你添加到其中的內容就是請求的內容。可能令你感到驚訝的是,默認情況下沒有上下文路徑。沒有jsessionid cookie;沒有轉發、錯誤或異步調度;因此,沒有實際的JSP渲染。而是將“轉發”和“重定向” URL保存在MockHttpServletResponse中,並且可以按預期進行聲明。

這意味著,如果你使用JSP,則可以驗證將請求轉發到的JSP頁面,但是不會呈現HTML。換句話說,不調用JSP。但是請注意,不依賴轉發的所有其他渲染技術(例如ThymeleafFreemarker)都按預期將HTML渲染到響應主體。通過@ResponseBody方法呈現JSONXML和其他格式時也是如此。

另外,你可以考慮使用@SpringBootTest從Spring Boot獲得完整的端到端集成測試支持。請參閱《 Spring Boot參考指南》。

每種方法都有優點和缺點。從經典的單元測試到全面的集成測試,Spring MVC Test中提供的選項在規模上是不同的。可以肯定的是,Spring MVC Test中的所有選項都不屬於經典單元測試的類別,但與之接近。例如,你可以通過將模擬服務注入到控制器中來隔離Web層,在這種情況下,你只能通過DispatcherServlet並使用實際的Spring配置來測試Web層,因為你可能會與上一層隔離地測試數據訪問層。此外,你可以使用獨立設置,一次只關注一個控制器,然後手動提供使其工作所需的配置。

使用Spring MVC Test時的另一個重要區別是,從概念上講,此類測試是服務器端的,因此你可以檢查使用了哪個處理程序,如果使用HandlerExceptionResolver處理了異常,則模型的內容是什麼、綁定錯誤是什麼?還有其他細節。這意味著編寫期望值更容易,因為服務器不是黑盒,就像通過實際的HTTP客戶端進行測試時一樣。通常,這是經典單元測試的優點:它更容易編寫、推理和調試,但不能代替完全集成測試的需要。同時,重要的是不要忽略響應是最重要的檢查事實。簡而言之,即使在同一項目中,這裡也存在多種測試樣式和測試策略的空間。

更多例子

框架自己的測試包括許多示例測試,旨在展示如何使用Spring MVC Test。你可以瀏覽這些示例以獲取進一步的想法。另外,spring-mvc-showcase項目具有基於Spring MVC Test的完整測試範圍。

3.6.2 HtmlUnit集成

Spring提供了MockMvcHtmlUnit之間的集成。使用基於HTML的視圖時,這簡化了端到端測試的執行。通過此集成你可以:

  • 使用HtmlUnitWebDriverGeb等工具可以輕鬆測試HTML頁面,而無需將其部署到Servlet容器中。
  • 在頁面中測試JavaScript。
  • (可選)使用模擬服務進行測試以加快測試速度。
  • 在容器內端到端測試和容器外集成測試之間共享邏輯。

MockMvc使用不依賴Servlet容器的模板技術(例如ThymeleafFreeMarker等),但不適用於JSP,因為它們依賴Servlet容器。

為什麼集成HtmlUnit

想到的最明顯的問題是“我為什麼需要這個?”通過探索一個非常基本的示例應用程序,最好找到答案。假設你有一個Spring MVC Web應用程序,它支持對Message對象的CRUD操作。該應用程序還支持所有消息的分頁。你將如何進行測試?

使用Spring MVC Test,我們可以輕鬆地測試是否能夠創建Message,如下所示:

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

如果我們要測試允許我們創建消息的表單視圖怎麼辦?例如,假設我們的表單類似於以下代碼段:

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />

    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>

    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

如何確保表單生成創建新消息的正確請求?一個幼稚的嘗試可能類似於下面:

mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='summary']").exists())
        .andExpect(xpath("//textarea[@name='text']").exists());

此測試有一些明顯的缺點。如果我們更新控制器以使用參數消息而不是文本,則即使HTML表單與控制器不同步,我們的表單測試也會繼續通過。為了解決這個問題,我們可以結合以下兩個測試:

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

這樣可以減少我們的測試錯誤通過的風險,但是仍然存在一些問題:

  • 如果頁面上有多個表單怎麼辦?誠然,我們可以更新XPath表達式,但是由於我們考慮了更多因素,它們變得更加複雜:字段是正確的類型嗎?是否啟用了字段?等等。
  • 另一個問題是我們正在做我們期望的兩倍的工作。我們必須首先驗證視圖,然後使用剛剛驗證的相同參數提交視圖。理想情況下,可以一次完成所有操作。
  • 最後,我們仍然無法解釋某些事情。例如,如果表單也具有我們希望測試的JavaScript驗證,該怎麼辦?

總體問題是,測試網頁不涉及單個交互。相反,它是用戶如何與網頁交互以及該網頁與其他資源交互的組合。例如,表單視圖的結果用作用戶創建消息的輸入。另外,我們的表單視圖可以潛在地使用影響頁面行為的其他資源,例如JavaScript驗證。

集成測試可以起到補救作用?

為了解決前面提到的問題,我們可以執行端到端集成測試,但這有一些缺點。考慮測試允許我們翻閱消息的視圖。我們可能需要以下測試:

  • 我們的頁面是否向用戶顯示通知,以指示消息為空時沒有可用結果?
  • 我們的頁面是否正確顯示一條消息?
  • 我們的頁面是否正確支持分頁?

要設置這些測試,我們需要確保我們的數據庫包含正確的消息。這帶來了許多其他挑戰:

  • 確保數據庫中包含正確的消息可能很繁瑣。 (考慮外鍵約束。)
  • 測試可能會變慢,因為每次測試都需要確保數據庫處於正確的狀態。
  • 由於我們的數據庫需要處於特定狀態,因此我們無法並行運行測試。
  • 對諸如自動生成的ID,時間戳等項目進行斷言可能很困難。

這些挑戰並不意味著我們應該完全放棄端到端集成測試。相反,我們可以通過重構詳細的測試以使用運行速度更快,更可靠且沒有副作用的模擬服務來減少端到端集成測試的數量。然後,我們可以實施少量真正的端到端集成測試,以驗證簡單的工作流程,以確保一切正常工作。

進入HtmlUnit集成

那麼,如何在測試頁面的交互性之間保持平衡,並在測試套件中保持良好的性能呢?答案是:通過將MockMvcHtmlUnit集成。

HtmlUnit集成選項

要將MockMvcHtmlUnit集成時,可以有多種選擇:

  • MockMvc和HtmlUnit:如果要使用原始的HtmlUnit庫,請使用此選項。
  • MockMvc和WebDriver:使用此選項可以簡化集成和端到端測試之間的開發和重用代碼。
  • MockMvc和Geb:如果要使用Groovy進行測試,簡化開發並在集成和端到端測試之間重用代碼,請使用此選項。

MockMvc 和 HtmlUnit

本節介紹如何集成MockMvcHtmlUnit。如果要使用原始HtmlUnit庫,請使用此選項。

MockMvc和HtmlUnit設置

首先,請確保你已包含對net.sourceforge.htmlunithtmlunit的測試依賴項。為了將HtmlUnit與Apache HttpComponents 4.5+一起使用,你需要使用HtmlUnit 2.18或更高版本。

我們可以使用MockMvcWebClientBuilder輕鬆創建一個與MockMvc集成的HtmlUnit WebClient,如下所示:

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

這是使用MockMvcWebClientBuilder的簡單示例。有關高級用法,請參閱Advanced MockMvcWebClientBuilder

這樣可以確保將引用localhost作為服務器的所有URL定向到我們的MockMvc實例,而無需真正的HTTP連接。通常,通過使用網絡連接來請求其他任何URL。這使我們可以輕鬆測試CDN的使用。

MockMvc和HtmlUnit用法

現在,我們可以像往常一樣使用HtmlUnit,而無需將應用程序部署到Servlet容器。例如,我們可以請求視圖創建以下消息:

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

默認上下文路徑為“”。或者,我們可以指定上下文路徑,如Advanced MockMvcWebClientBuilder中所述。

一旦有了對HtmlPage的引用,我們就可以填寫表格並提交以創建一條消息,如以下示例所示:

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

最後,我們可以驗證是否已成功創建新消息。以下斷言使用AssertJ庫:

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

前面的代碼以多種方式改進了我們的MockMvc測試。首先,我們不再需要顯式驗證表單,然後創建類似於表單的請求。相反,我們要求表單,將其填寫並提交,從而大大減少了開銷。

另一個重要因素是HtmlUnit使用Mozilla Rhino引擎來評估JavaScript。這意味著我們還可以在頁面內測試JavaScript的行為。

有關使用HtmlUnit的其他信息,請參見HtmlUnit文檔

MockMvcWebClientBuilder進階

在到目前為止的示例中,我們通過基於Spring TestContext 框架為我們加載的WebApplicationContext構建一個WebClient,以最簡單的方式使用了MockMvcWebClientBuilder。在以下示例中重複此方法:

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

我們還可以指定其他配置選項,如以下示例所示:

WebClient webClient;

@BeforeEach
void setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
}

或者,我們可以通過分別配置MockMvc實例並將其提供給MockMvcWebClientBuilder來執行完全相同的設置,如下所示:

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();

這比較冗長,但是,通過使用MockMvc實例構建WebClient,我們可以輕而易舉地擁有MockMvc的全部功能。

有關創建MockMvc實例的其他信息,請參見安裝程序選項

MockMvc和WebDriver

在前面的部分中,我們已經瞭解瞭如何將MockMvc與原始HtmlUnit API結合使用。在本節中,我們在Selenium WebDriver中使用其他抽象使事情變得更加容易。

為什麼要使用WebDriver和MockMvc?

我們已經可以使用HtmlUnit和MockMvc,那麼為什麼要使用WebDriverSelenium WebDriver提供了一個非常優雅的API,使我們可以輕鬆地組織代碼。為了更好地說明它是如何工作的,我們在本節中探索一個示例。

儘管是Selenium的一部分,WebDriver並不需要Selenium Server來運行測試。

假設我們需要確保正確創建一條消息。測試涉及找到HTML表單輸入元素,將其填寫並做出各種斷言。

這種方法會導致大量單獨的測試,因為我們也想測試錯誤情況。例如,如果只填寫表格的一部分,我們要確保得到一個錯誤。如果我們填寫整個表格,那麼新創建的消息應在之後顯示。

如果將其中一個字段命名為“ summary”,則我們可能會在測試中的多個位置重複以下內容:

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

那麼,如果我們將id更改為smmry,會發生什麼?這樣做將迫使我們更新所有測試以納入此更改。這違反了DRY原理,因此理想情況下,我們應將此代碼提取到其自己的方法中,如下所示:

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}

這樣做可以確保在更改UI時不必更新所有測試。

我們甚至可以更進一步,將此邏輯放在代表我們當前所在的HtmlPage的Object中,如以下示例所示:

public class CreateMessagePage {

    final HtmlPage currentPage;

    final HtmlTextInput summaryInput;

    final HtmlSubmitInput submit;

    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }

    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);

        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);

        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }

    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }

    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}

以前,此模式稱為頁面對象模式。雖然我們當然可以使用HtmlUnit做到這一點,但WebDriver提供了一些我們在以下各節中探討的工具,以使該模式的實現更加容易。

MockMvc和WebDriver設置

要將Selenium WebDriver與Spring MVC Test框架一起使用,請確保你的項目包含對org.seleniumhq.selenium:selenium-htmlunit-driver的測試依賴項。

我們可以使用MockMvcHtmlUnitDriverBuilder輕鬆創建一個與MockMvc集成的Selenium WebDriver,如以下示例所示:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}

這是使用MockMvcHtmlUnitDriverBuilder的簡單示例。有關更多高級用法,請參見Advanced MockMvcHtmlUnitDriverBuilder

前面的示例確保將引用localhost作為服務器的所有URL定向到我們的MockMvc實例,而無需真正的HTTP連接。通常,通過使用網絡連接來請求其他任何URL。這使我們可以輕鬆測試CDN的使用。

MockMvc和WebDriver的用法

現在,我們可以像往常一樣使用WebDriver,而無需將應用程序部署到Servlet容器。例如,我們可以請求視圖創建以下消息:

CreateMessagePage page = CreateMessagePage.to(driver);

然後,我們可以填寫表格並提交以創建一條消息,如下所示:

ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

通過利用頁面對象模式,這可以改善我們的HtmlUnit測試的設計。正如我們在“為什麼要使用WebDriver和MockMvc?”中提到的那樣,我們可以將頁面對象模式與HtmlUnit一起使用,但使用WebDriver則要容易得多。考慮以下CreateMessagePage實現:

public class CreateMessagePage
        extends AbstractPage { //1

    //2
    private WebElement summary;
    private WebElement text;

    //3
    @FindBy(css = "input[type=submit]")
    private WebElement submit;

    public CreateMessagePage(WebDriver driver) {
        super(driver);
    }

    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }

    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}
  1. CreateMessagePage擴展AbstractPage。我們不詳細介紹AbstractPage,但總而言之,它包含我們所有頁面的通用功能。例如,如果我們的應用程序具有導航欄,全局錯誤消息以及其他功能,我們可以將此邏輯放置在共享位置。
  2. 對於HTML頁面的每個部分,我們都有一個成員變量有興趣。這些是WebElement類型。 WebDriverPageFactory讓我們刪除通過自動解析來自HtmlUnit版本的CreateMessagePage的大量代碼每個WebElementPageFactory#initElements(WebDriver,Class <T>)方法通過使用字段名稱並查找來自動解析每個WebElement按HTML頁面中元素的ID或名稱。
  3. 我們可以使用@FindBy註解覆蓋默認的查找行為。我們的示例顯示瞭如何使用@FindBy
    註釋以使用CSS選擇器(input [type = submit])查找提交按鈕。

最後,我們可以驗證是否已成功創建新消息。以下斷言使用AssertJ斷言庫:

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

我們可以看到ViewMessagePage允許我們與自定義域模型進行交互。例如,它公開了一個返回Message對象的方法:

public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}

然後,我們可以在聲明中使用富域對象。

最後,我們一定不要忘記在測試完成後關閉WebDriver實例,如下所示:

@AfterEach
void destroy() {
    if (driver != null) {
        driver.close();
    }
}

有關使用WebDriver的其他信息,請參閱Selenium WebDriver文檔

MockMvcHtmlUnitDriverBuilder進階

在到目前為止的示例中,我們通過基於Spring TestContext 框架為我們加載的WebApplicationContext構建一個WebDriver,以最簡單的方式使用了MockMvcHtmlUnitDriverBuilder。在此重複此方法,如下所示:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}

我們還可以指定其他配置選項,如下所示:

WebDriver driver;

@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build();
}

或者,我們可以通過分別配置MockMvc實例並將其提供給MockMvcHtmlUnitDriverBuilder來執行完全相同的設置,如下所示:

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();

這更為冗長,但是通過使用MockMvc實例構建WebDriver,我們可以輕而易舉地擁有MockMvc的全部功能。

有關創建MockMvc實例的其他信息,請參見安裝選項

MockMvc和Geb

在上一節中,我們瞭解瞭如何在WebDriver中使用MockMvc。在本節中,我們使用Geb來進行甚至Groovy-er的測試。

為什麼選擇Geb和MockMvc?

Geb得到了WebDriver的支持,因此它提供了許多與WebDriver[相同的好處]()。但是,Geb通過為我們處理一些樣板代碼使事情變得更加輕鬆。

MockMvc和Geb設置

我們可以輕鬆地使用使用MockMvc的Selenium WebDriver來初始化Geb瀏覽器,如下所示:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}

這是使用MockMvcHtmlUnitDriverBuilder的簡單示例。有關更多高級用法,請參見Advanced MockMvcHtmlUnitDriverBuilder

這樣可以確保在服務器上引用本地主機的所有URL都定向到我們的MockMvc實例,而無需真正的HTTP連接。通常,通過使用網絡連接來請求其他任何URL。這使我們可以輕鬆測試CDN的使用。

MockMvc和Geb用法

現在,我們可以像往常一樣使用Geb了,而無需將應用程序部署到Servlet容器中。例如,我們可以請求視圖創建以下消息:

to CreateMessagePage

然後,我們可以填寫表格並提交以創建一條消息,如下所示:

when: form.summary = expectedSummary form.text = expectedMessage submit.click(ViewMessagePage)

找不到的所有無法識別的方法調用或屬性訪問或引用都將轉發到當前頁面對象。這消除了我們直接使用WebDriver時需要的許多樣板代碼。

與直接使用WebDriver一樣,這通過使用Page Object Pattern改進了HtmlUnit測試的設計。如前所述,我們可以將頁面對象模式與HtmlUnitWebDriver一起使用,但使用Geb則更加容易。考慮我們新的基於Groovy的CreateMessagePage實現:

class CreateMessagePage extends Page {
    static url = 'messages/form'
    static at = { assert title == 'Messages : Create'; true }
    static content =  {
        submit { $('input[type=submit]') }
        form { $('form') }
        errors(required:false) { $('label.error, .alert-error')?.text() }
    }
}

我們的CreateMessagePage擴展了Page。我們不會詳細介紹Page,但是總而言之,它包含了我們所有頁面的通用功能。我們定義一個可在其中找到此頁面的URL。這使我們可以導航到頁面,如下所示:

to CreateMessagePage

我們還有一個at閉包,它確定我們是否在指定頁面上。如果我們在正確的頁面上,它應該返回true。這就是為什麼我們可以斷言我們在正確的頁面上的原因,如下所示:

then:
at CreateMessagePage
errors.contains('This field is required.')

我們在閉包中使用一個斷言,以便我們可以確定在錯誤的頁面上哪裡出錯了。

接下來,我們創建一個內容閉合,該閉合指定頁面內所有感興趣的區域。我們可以使用jQuery-ish Navigator API來選擇我們感興趣的內容。

最後,我們可以驗證是否已成功創建新消息,如下所示:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有關如何充分利用Geb的更多詳細信息,請參見The Geb Book用戶手冊。

3.6.3 客戶端REST測試

你可以使用客戶端測試來測試內部使用RestTemplate的代碼。這個想法是聲明預期的請求並提供“存根”響應,以便你可以專注於隔離測試代碼(即,不運行服務器)。以下示例顯示瞭如何執行此操作:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();

在前面的示例中,MockRestServiceServer(客戶端REST測試的中心類)使用自定義的ClientHttpRequestFactory配置RestTemplate,該ClientHttpRequestFactory根據期望斷言實際的請求並返回“存根”響應。在這種情況下,我們希望有一個請求/greeting,並希望返回200個帶有text/plain內容的響應。我們可以根據需要定義其他預期的請求和存根響應。當我們定義期望的請求和存根響應時,RestTemplate可以照常在客戶端代碼中使用。在測試結束時,可以使用mockServer.verify()來驗證是否滿足所有期望。

默認情況下,請求應按聲明的期望順序進行。你可以在構建服務器時設置ignoreExpectOrder選項,在這種情況下,將檢查所有期望值(以便)以找到給定請求的匹配項。這意味著允許請求以任何順序出現。以下示例使用ignoreExpectOrder

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

即使默認情況下無順序請求,每個請求也只能執行一次。Expect方法提供了一個重載的變量,該變量接受一個ExpectedCount參數,該參數指定一個計數範圍(例如,oncemanyTimes,、maxminbetween之間等等)。以下示例使用times

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();

請注意,如果未設置ignoreExpectOrder(默認設置),並且因此要求按聲明順序進行請求,則該順序僅適用於任何預期請求中的第一個。例如,如果期望“/something”兩次,然後是“/somewhere”三次,那麼在請求“/somewhere”之前應該先請求“/something”,但是除了隨後的“/something”和“/somewhere”,請求可以隨時發出。

作為上述所有方法的替代,客戶端測試支持還提供了ClientHttpRequestFactory實現,你可以將其配置為RestTemplate以將其綁定到MockMvc實例。這樣就可以使用實際的服務器端邏輯來處理請求,而無需運行服務器。以下示例顯示瞭如何執行此操作:

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...

靜態導入

與服務器端測試一樣,用於客戶端測試的流利API需要進行一些靜態導入。通過搜索MockRest 可以輕鬆找到這些內容。 Eclipse用戶應在Java→編輯器→內容輔助→收藏夾下的Eclipse首選項中,將MockRestRequestMatchersMockRestResponseCreators。添加為“收藏的靜態成員”。這樣可以在鍵入靜態方法名稱的第一個字符後使用內容輔助。其他IDE(例如IntelliJ)可能不需要任何其他配置。檢查是否支持靜態成員上的代碼完成。

客戶端REST測試的更多示例

Spring MVC Test自己的測試包括客戶端REST測試的示例測試

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。關注公眾號:青年IT男 獲取最新技術文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *