天天看點

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控制器編寫一個普通的單元測試。為此,執行個體化控制器,向其注入模拟或存根依賴性,然後調用其方法(根據需要傳遞

MockHttpServletRequest

MockHttpServletResponse

等)。但是,在編寫這樣的單元測試時,仍有許多未經測試的内容:例如,請求映射、資料綁定、類型轉換、驗證等等。此外,也可以在請求處理生命周期中調用其他控制器方法,例如

@InitBinder

@ModelAttribute

@ExceptionHandler

Spring MVC Test的目标是通過執行請求并通過實際的

DispatcherServlet

生成響應來提供一種測試控制器的有效方法。Spring MVC Test基于

spring-test

子產品中可用的Servlet API的“

模拟

”實作。這允許執行請求和生成響應,而無需在Servlet容器中運作。在大多數情況下,一切都應像在運作時一樣工作,但有一些值得注意的例外,如

中所述。以下基于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屬性,其值為

Lee

Jayway 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進行測試,請確定相應地設定

contextPath

servletPath

,以便請求映射起作用,如以下示例所示:

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

在前面的示例中,為每個執行的請求設定

contextPath

servletPath

将很麻煩。相反,你可以設定預設請求屬性,如以下示例所示:

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響應内容包含使用

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中,可以通過以下方法測試異步請求:首先聲明産生的異步值,然後手動執行異步分派,最後驗證響應。以下是針對傳回

DeferredResult

Callable

或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 Test基于

spring-test

子產品的Servlet API模拟實作而建構,并且不依賴于運作中的容器。是以,與使用實際用戶端和實時伺服器運作的完整端到端內建測試相比,存在一些差異。

考慮這一點的最簡單方法是從一個空白的

MockHttpServletRequest

開始。你添加到其中的内容就是請求的内容。可能令你感到驚訝的是,預設情況下沒有上下文路徑。沒有

jsessionid cookie

;沒有轉發、錯誤或異步排程;是以,沒有實際的JSP渲染。而是将“

轉發

”和“重定向” URL儲存在

MockHttpServletResponse

中,并且可以按預期進行聲明。

這意味着,如果你使用JSP,則可以驗證将請求轉發到的JSP頁面,但是不會呈現HTML。換句話說,不調用JSP。但是請注意,不依賴轉發的所有其他渲染技術(例如

Thymeleaf

Freemarker

)都按預期将HTML渲染到響應主體。通過

@ResponseBody

方法呈現

JSON

XML

和其他格式時也是如此。

另外,你可以考慮使用

@SpringBootTest

從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提供了

MockMvc HtmlUnit

之間的內建。使用基于HTML的視圖時,這簡化了端到端測試的執行。通過此內建你可以:

  • 使用 WebDriver Geb 等工具可以輕松測試HTML頁面,而無需将其部署到Servlet容器中。
  • 在頁面中測試JavaScript。
  • (可選)使用模拟服務進行測試以加快測試速度。
  • 在容器内端到端測試和容器外內建測試之間共享邏輯。

MockMvc

使用不依賴Servlet容器的模闆技術(例如

Thymeleaf

FreeMarker

等),但不适用于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內建

那麼,如何在測試頁面的互動性之間保持平衡,并在測試套件中保持良好的性能呢?答案是:通過将

MockMvc

HtmlUnit

內建。

HtmlUnit內建選項

要将

MockMvc

HtmlUnit

內建時,可以有多種選擇:

  • MockMvc和HtmlUnit :如果要使用原始的

    HtmlUnit

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

MockMvc 和 HtmlUnit

本節介紹如何內建

MockMvc

HtmlUnit

。如果要使用原始

HtmlUnit

MockMvc和HtmlUnit設定

首先,請確定你已包含對

net.sourceforge.htmlunit

htmlunit

的測試依賴項。為了将

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");           
預設上下文路徑為

“”

。或者,我們可以指定上下文路徑,如 中所述。

一旦有了對

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

與原始

HtmlUnit

API結合使用。在本節中,我們在Selenium

中使用其他抽象使事情變得更加容易。

為什麼要使用WebDriver和MockMvc?

我們已經可以使用

HtmlUnit

和MockMvc,那麼為什麼要使用

WebDriver

Selenium 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

MockMvc

MockMvc和WebDriver的用法

WebDriver

CreateMessagePage page = CreateMessagePage.to(driver);           

然後,我們可以填寫表格并送出以建立一條消息,如下所示:

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

通過利用頁面對象模式,這可以改善我們的

測試的設計。正如我們在“

為什麼要使用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

    類型。

    WebDriver

    PageFactory

    讓我們删除通過自動解析來自

    HtmlUnit

    版本的

    CreateMessagePage

    的大量代碼每個

    WebElement

    PageFactory#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進階

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

安裝選項

在上一節中,我們了解了如何在

WebDriver

中使用

MockMvc

。在本節中,我們使用

來進行甚至Groovy-er的測試。

為什麼選擇Geb和MockMvc?

Geb得到了

WebDriver

的支援,是以它提供了許多與

WebDriver

[相同的好處]()。但是,Geb通過為我們處理一些樣闆代碼使事情變得更加輕松。

MockMvc和Geb設定

我們可以輕松地使用使用

MockMvc

的Selenium WebDriver來初始化Geb浏覽器,如下所示:

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

MockMvcHtmlUnitDriverBuilder

這樣可以確定在伺服器上引用本地主機的所有URL都定向到我們的

MockMvc

MockMvc和Geb用法

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

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

找不到的所有無法識别的方法調用或屬性通路或引用都将轉發到目前頁面對象。這消除了我們直接使用

WebDriver

時需要的許多樣闆代碼。

與直接使用

WebDriver

一樣,這通過使用

Page Object Pattern

改進了

HtmlUnit

測試的設計。如前所述,我們可以将頁面對象模式與

HtmlUnit

WebDriver

一起使用,但使用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

參數,該參數指定一個計數範圍(例如,

once

manyTimes

,、

max

min

between

之間等等)。以下示例使用

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首選項中,将

MockRestRequestMatchers

。和

MockRestResponseCreators

。添加為“收藏的靜态成員”。這樣可以在鍵入靜态方法名稱的第一個字元後使用内容輔助。其他IDE(例如IntelliJ)可能不需要任何其他配置。檢查是否支援靜态成員上的代碼完成。

用戶端REST測試的更多示例

Spring MVC Test自己的測試包括用戶端REST測試的

示例測試

作者

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

部落格位址:

http://youngitman.tech

CSDN:

https://blog.csdn.net/liyong1028826685

微信公衆号:

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

技術交流群:

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