iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Modern Web

Spring Boot API 開發:從 0 到 1系列 第 27

Day 27 MockMvc 測試

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240910/2012194877AAsLKk3b.png

在上一篇文章中,我們已經測試了 Service 的部分

今天要來看 Controller 的部分,探討如何進行測試

在這裡我們就不用和 Service 一樣的單元測試的方式

我們會使用 Spring 框架提供給我們的 MockMVC 這個工具,它專門用來測試 Spring MVC 應用程式的控制器(Controller) 

它允許開發者模擬 HTTP 的動作,而無需實際啟動伺服器

MockMVC Test 的主要用途

  • 驗證控制器的行為:測試各種 HTTP 方法(GET、POST、PUT、DELETE 等)的處理
  • 檢查回應狀態:確認控制器返回正確的 HTTP 狀態碼
  • 驗證回應內容:檢查回應的 body、header 等是否符合預期
  • 測試請求參數和路徑變數:確保控制器正確處理各種輸入
  • 模擬不同的請求情境:包括正常情況錯誤處理

MockMVC 測試中的重要註解

@WebMvcTest

  • 這個註解用於專門測試 Spring MVC 控制器
  • 它會自動配置 Spring MVC 基礎設施,並只實例化被測試的控制器,所以可以大大加快測試速度

@MockBean

  • 用於建立和注入一個 Mockito mock 物件到 Spring 應用程式中
  • 它會替換掉應用程式上下文中任何相同類型的已存在的 bean
  • 在我們的例子中,它用來模擬 TodoService,讓我們能夠控制服務層的行為

@Autowired

  • 在測試中,可以讓你方便的注入 MockMvc 和其他測試所需的組件

TodoController 的 MockMVC 測試

一樣先在 test 的 package 裡面,建立相對應的 controller package,然後建立一個 TodoControllerTest 的測試類別

下面我們測試了主要的 CRUD 相關功能

@WebMvcTest(TodoController.class)
public class TodoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private TodoService todoService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void createTodo() throws Exception {
        Todo todo = new Todo(null, "新待辦事項", false);
        Todo savedTodo = new Todo(1L, "新待辦事項", false);

        when(todoService.save(any(Todo.class))).thenReturn(savedTodo);

        mockMvc.perform(post("/api/todos")
               .contentType(MediaType.APPLICATION_JSON)
               .content(objectMapper.writeValueAsString(todo)))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true))
               .andExpect(jsonPath("$.data.id").value(1))
               .andExpect(jsonPath("$.data.title").value("新待辦事項"))
               .andExpect(jsonPath("$.data.completed").value(false));

        verify(todoService, times(1)).save(any(Todo.class));
    }

    @Test
    public void getAllTodos() throws Exception {
        Todo todo1 = new Todo(1L, "測試待辦事項", false);
        Todo todo2 = new Todo(2L, "測試待辦事項2", true);

        when(todoService.findAll()).thenReturn(Arrays.asList(todo1, todo2));

        mockMvc.perform(get("/api/todos")).andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true))
               .andExpect(jsonPath("$.data[0].id").value(1))
               .andExpect(jsonPath("$.data[0].title").value("測試待辦事項"))
               .andExpect(jsonPath("$.data[0].completed").value(false))
               .andExpect(jsonPath("$.data[1].id").value(2))
               .andExpect(jsonPath("$.data[1].title").value("測試待辦事項2"))
               .andExpect(jsonPath("$.data[1].completed").value(true));

        verify(todoService, times(1)).findAll();
    }

    @Test
    public void getTodo() throws Exception {
        Todo todo = new Todo(1L, "測試待辦事項", false);

        when(todoService.findById(1L)).thenReturn(Optional.of(todo));

        mockMvc.perform(get("/api/todos/1")).andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true))
               .andExpect(jsonPath("$.data.id").value(1))
               .andExpect(jsonPath("$.data.title").value("測試待辦事項"))
               .andExpect(jsonPath("$.data.completed").value(false));

        verify(todoService, times(1)).findById(1L);
    }

    @Test
    public void updateTodo() throws Exception {
        Todo updatedTodo = new Todo(1L, "更新的待辦事項", true);

        when(todoService.updateTodo(eq(1L), any(Todo.class)))
            .thenReturn(Optional.of(updatedTodo));

        mockMvc.perform(put("/api/todos/1")
               .contentType(MediaType.APPLICATION_JSON)
               .content(objectMapper.writeValueAsString(updatedTodo)))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true))
               .andExpect(jsonPath("$.data.id").value(1))
               .andExpect(jsonPath("$.data.title").value("更新的待辦事項"))
               .andExpect(jsonPath("$.data.completed").value(true));

        verify(todoService, times(1)).updateTodo(eq(1L), any(Todo.class));
    }

    @Test
    public void deleteTodo() throws Exception {
        when(todoService.deleteTodo(1L)).thenReturn(true);

        mockMvc.perform(delete("/api/todos/1")).andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true));

        verify(todoService, times(1)).deleteTodo(1L);
    }
}

要注意 import 的 package

  • MockServer 開頭的,主要用於 WebFlux 
  • MockMvc 開頭的,主要用於 Spring MVC - 這才是我們要 import 的

在測試中,@WebMvcTest(TodoController.class) 告訴 Spring Boot 只初始化與 TodoController 相關的 Spring MVC 基礎設施

每個測試方法都使用 MockMvc 來模擬 HTTP 請求,並使用 Mockito 來模擬 TodoService 的行為

我們驗證了回應的狀態碼 和內容,確保控制器正確處理請求並返回預期的結果

已經寫了一篇文章來介紹三種優雅的 JSON 驗證方法,有興趣的可以看看:Day 32.5 進化你的Spring Boot測試:三種優雅的JSON驗證方法

結論

MockMVC 測試是 Spring Boot 應用程式中不可或缺的一部分

它們提供了一種有效的方式來測試控制器層的邏輯,而無需啟動整個應用程式

在實際開發中,建議為每個控制器方法編寫全面的測試案例,包括正向和反向的情境

這樣可以大大提高應用程式的品質,並在進行修改或重構時提供安全網

同步刊登於 Blog 「Spring Boot API 開發:從 0 到 1」Day 27 MockMVC 測試

我的粉絲專頁

圖片來源:AI 產生

參考連結


上一篇
Day 26 單元測試
下一篇
Day 28 DataJpaTest 測試
系列文
Spring Boot API 開發:從 0 到 139
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言