Test your first Spring Boot web application

這篇會透過測試上一篇 (Building your first Spring Boot web application) 文章最後的 Example,來快速簡介 Spring MVC Test Framework

測試的流程大概是這樣,透過 MockMvc 去執行一個 RequestBuilder 取得一個 ResultActions,下面會分別針對這三個元件做簡單的介紹。

MockMvc

MockMvc 是 Spring MVC 測試的進入點,可以直接用物件操作的方式來執行對應的 Route。要使用 MockMvc 可以在 Test Class 上標示 @WebMvcTest,然後就可以在 Test Case(Method) 上使用 @Autowired MockMvc mockMvc 來取得一個可以用的 MockMvc

接著就可以用 mockMvc.perform(requestBuilder); 來執行透過 MockMvcRequestBuilders.* 所產生的 RequestBuilder,執行的結果會包在 ResultActions 回傳,提供後續驗證使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest
class WebDemoApplicationTests {
@Test
void route(@Autowired MockMvc mockMvc) throws Exception {
RequestBuilder requestBuilder = get("/first/route");
ResultActions resultActions = mockMvc.perform(requestBuilder);

resultActions
.andExpect(status().isOk())
.andExpect(content().string("Hi"));
}
}

RequestBuilder

要產生一個可以用的 RequestBuilder 可以透過 MockMvcRequestBuilders 這個 Class,裡面包含了 get, post, put… 等 http method 的 Builder,這些 Builder 實際上回傳的是 MockHttpServletRequestBuilder,這個 Class 還可一併設定 Header, Cookie, Parameter… 等 Request 相關的參數。

通常會用 import static ... 來載入 MockMvcRequestBuilders

ResultActions

ResultActions 支援下面三個 Method 來處理或確認 RequestBuilder 的執行結果:

  • ResultActions andExpect(ResultMatcher matcher) throws Exception;

    用來判斷回傳的內容是否跟預期的值一樣,在 MockMvcResultMatchers.* 提供了很多預先定義好的 ResultMatcher,可以透過 import static ... 來使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import static org.springframework.test.web.servlet.ResultMatcher.matchAll;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

    mockMvc.perform(post("/form"))
    .andExpect(status().isOk())
    .andExpect(redirectedUrl("/person/1"))
    .andExpect(model().size(1))
    .andExpect(model().attributeExists("person"))
    .andExpect(flash().attributeCount(1))
    .andExpect(flash().attribute("message", "success!")
    );

    // 多個 andExpect() 可以用 matchAll
    mockMvc.perform(post("/form"))
    .andExpect(matchAll(
    status().isOk(),
    redirectedUrl("/person/1"),
    model().size(1),
    model().attributeExists("person"),
    flash().attributeCount(1),
    flash().attribute("message", "success!")
    )
    );
  • ResultActions andDo(ResultHandler handler) throws Exception;

    ResultHandler 透過實作 void handle(MvcResult result) throws Exception; 來提供直接對 MvcResult 進行處理的能力。 像是 MockMvcResultHandlers 裡,就有定義了幾個 static method,提供記錄或直接印出 MvcResult

  • MvcResult andReturn();

    呼叫這個 Method 可以直接取得 RequestBuilder 的執行結果

Example

下面這個 Example 是用來測試上一篇 (Building your first Spring Boot web application) 文章最後的 Example 的 Test Class。

試著跑看看吧,應該會發生兩個錯誤!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

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

@WebMvcTest
class WebDemoApplicationTests {
@Test
void route1(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/first/route1"))
.andExpect(status().isOk())
.andExpect(content().string("Hi"));
}

@Test
void route2(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/first/route2"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.header").exists())
.andExpect(jsonPath("$.param").exists());
}

@Test
void route3(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/first/route3"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Austin"));
}

@Test
void route4(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/first/route4"))
.andExpect(status().isOk())
.andExpect(xpath("//h2").string("Hello Austin"))
.andExpect(xpath("//p").string("This is test message!"));
}

@Test
void route5(@Autowired MockMvc mockMvc) throws Exception {
String name = "Austin";
String message = "Hello Route5!";
mockMvc.perform(
post("/first/route5?name=" + name).param("message", message)
)
.andExpect(status().isOk())
.andExpect(xpath("//h2").string("Hello " + name))
.andExpect(xpath("//p").string(message));
}

@Test
void route6(@Autowired MockMvc mockMvc) throws Exception {
String name = "Austin";
String message = "Hello Route6!";
mockMvc.perform(
post("/first/route6/" + name).content(message)
)
.andExpect(status().isOk())
.andExpect(xpath("//h2").string("Hello " + name))
.andExpect(xpath("//p").string(message));
}

@Test
void route7(@Autowired MockMvc mockMvc) throws Exception {
String name = "Austin";
String message = "Hello Route7!";
mockMvc.perform(
post("/first/route7")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"" + name + "\", \"message\":\"" + message + "\"}")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(xpath("//h2").string("Hello " + name))
.andExpect(xpath("//p").string(message));
}
}