如何對Spring MVC中的Controller進行單元測試

nuccch發表於2022-02-16

對Controller進行單元測試是Spring框架原生就支援的能力,它可以模擬HTTP客戶端發起對服務地址的請求,可以不用藉助於諸如Postman這樣的外部工具就能完成對介面的測試。
具體來講,是由Spring框架中的spring-test模組提供的實現,詳見MockMvc

如下將詳細闡述如何使用MockMvc測試框架實現對“Spring Controller”進行單元測試,基於Spring Boot開發框架進行驗證。
新增測試框架依賴:

<!-- Spring框架 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
!<-- Spring測試框架 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- 檔案操作工具 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

匯入靜態工具方法

為了便於在編寫測試用例時直接呼叫測試框架自帶的靜態方法,首先需要匯入這些靜態工具方法。
需要匯入的靜態方法如下:

import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.*;

初始化MockMvc

初始化MockMvc有2種方式:
方式1:明確指定需要測試的“Controller”類進行配置
方式2:基於Spring容器進行配置,包含了Spring MVC環境和所有“Controller”類,通常使用這種方式。

@SpringBootTest
public class TestControllerTest {

    MockMvc mockMvc;

    // 初始化MockMvc
    @BeforeEach
    void setUp(WebApplicationContext wac) {
        // 方式1:明確指定需要測試的“Controller”類
        this.mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build();

        // 方式2:基於Spring容器進行配置,包含了Spring MVC環境和所有“Controller”類。
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
}

另外,還可以對MockMvc進行全域性配置。

// 全域性配置MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) // 預設請求路徑
        .apply(sharedHttpSession()) // 配置session
        .alwaysExpect(status().isOk()) // 預期響應狀態碼
        .alwaysExpect(content().contentType("application/json;charset=UTF-8")) // 預期內容型別
        .build();

執行測試

MockMvc支援對常見的HTTP方法,如:GET,POST,PUT,DELETE等,甚至還支援檔案上傳請求。

測試GET介面

// 訪問GET介面:不帶引數
@Test
public void testSimpleGet() throws Exception {
    MvcResult result = this.mockMvc.perform(get("/test/simple/get")
            .accept(MediaType.APPLICATION_JSON)) // 接受JSON格式響應訊息
            .andReturn(); // 獲取返回結果
    Assertions.assertEquals("OK", result.getResponse().getContentAsString());
}

// 訪問GET介面:帶URL引數
@Test
public void testParamGet() throws Exception {
    int id = 10;
    // 方式1:在URI模板中指定引數
    //MvcResult result = this.mockMvc.perform(get("/test/param/get?id={id}", id).accept(MediaType.APPLICATION_JSON)).andReturn();

    // 方式2:通過param()方法指定引數
    //MvcResult result = this.mockMvc.perform(get("/test/param/get").param("id", String.valueOf(id)).accept(MediaType.APPLICATION_JSON)).andReturn();

    // 方式3:通過queryParam()方法指定引數
    MvcResult result = this.mockMvc.perform(get("/test/param/get").queryParam("id", String.valueOf(id)).accept(MediaType.APPLICATION_JSON)).andReturn();
    Assertions.assertEquals("OK: " + id, result.getResponse().getContentAsString());
}

測試POST介面

// 傳遞表單引數
@Test
public void testSimplePost() throws Exception {
    int id = 10;

    // 呼叫param()方法傳遞引數
    MvcResult result = this.mockMvc.perform(post("/test/simple/post")
            .param("id", String.valueOf(id))
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .accept(MediaType.APPLICATION_JSON))
            .andReturn();
    Assertions.assertEquals("{\"id\":10}", result.getResponse().getContentAsString());
}

// 傳遞JSON引數
@Test
public void testSimplePostJson() throws Exception {
    // 呼叫content()方法傳遞json字串引數
    Subject subject = new Subject();
    subject.setId(10);
    String content = JSON.toJSONString(subject);
    MvcResult result = this.mockMvc.perform(post("/test/simple/post/json")
            .content(content)
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON))
            .andReturn();
    Assertions.assertEquals("{\"id\":10}", result.getResponse().getContentAsString());
}

測試檔案上傳

@Test
public void testFileUploadSingle() throws Exception {
    File file = new File("C:\\Users\\xxx\\Downloads\\test.jpg");
    String fileName = FilenameUtils.getName(file.getName());
    byte[] bytes = FileUtils.readFileToByteArray(file);
    MockMultipartFile mockMultipartFile = new MockMultipartFile("file", fileName, MediaType.MULTIPART_FORM_DATA_VALUE, bytes);
    this.mockMvc.perform(multipart("/test/upload/single").file(mockMultipartFile))
                .andExpect(status().isOk())
                .andExpect(content().string("OK"))
                .andDo(print());
}

定義預期結果

斷言響應結果時,有2種方式:
1.使用JUnit提供的Assert斷言工具判斷返回結果,這是一種非常普遍和常見的方式
2.在MockMvc框架中可以通過andExpect()方法定義一個或多個預期結果,當其中一個期望結果斷言失敗時,就不會斷言其他期望值了

// 使用Junit斷言工具判斷返回結果是否符合預期
@Test
public void testAssertResult() throws Exception {
    MvcResult result = this.mockMvc.perform(get("/test/simple/get").accept(MediaType.APPLICATION_JSON)).andDo(print()).andReturn();
    Assert.assertEquals("OK", result.getResponse().getContentAsString());
}

// 在MockMvc框架中定義預期結果
@Test
public void testExpectations() throws Exception {
    this.mockMvc.perform(get("/test/simple/get").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())        // 預期響應狀態碼為200
            .andExpect(content().string("OK")) // 預期返回值為字串“OK”
            .andDo(print());
}

相比於使用Junit的斷言工具判斷返回結果,在MockMvc框架中直接定義預期結果進行斷言檢查更加簡潔。

寫在最後

使用Spring提供的測試框架MockMvc可以非常方便地實現對HTTP服務介面進行單元測試,不要把基礎的功能驗證工作都交給測試童鞋,應該通過單元測試來保證程式碼迭代的穩定性。

【參考】
https://blog.csdn.net/coolcoffee168/article/details/88638042 springboot 單元測試 (controller層) 方法 -- MockMvc

相關文章