Spring MVC 測試必備:MockMvc 靜態導入最佳實踐
使用 Spring Test 框架進行 Controller 單元測試時,MockMvc 是核心工具。靜態導入關鍵方法能大幅簡化測試代碼,提升可讀性。本文整理最常用的靜態導入模式與實戰技巧。
核心靜態導入
// 請求構建器 - 核心入口
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.MockMvcBuilders.*;
常用請求構建方法
| 方法 | 說明 | 範例 |
get(url) | GET 請求 | get("/users/1") |
post(url) | POST 請求 | post("/users").content(json) |
put(url) | PUT 請求 | put("/users/1").content(json) |
delete(url) | DELETE 請求 | delete("/users/1") |
patch(url) | PATCH 請求 | patch("/users/1").content(json) |
request(HttpMethod, url) | 通用請求 | request(HttpMethod.OPTIONS, "/") |
fileUpload(url) | 檔案上傳 | fileUpload("/upload").file(mockFile) |
請求修飾鏈式調用
// 完整請求構建範例
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON) // Content-Type
.content(objectMapper.writeValueAsString(user)) // Request Body
.header("Authorization", "Bearer " + token) // Header
.param("key", "value") // Query 參數
.cookie(new Cookie("session", "abc123")) // Cookie
.sessionAttr("userId", 123L) // Session 屬性
.with(csrf()) // CSRF Token (Spring Security)
.with(user("admin").roles("ADMIN")) // 模擬認證用戶
).andExpect(status().isOk())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name").value("張三"))
.andDo(print()); // 列印完整請求/回應,調試神器
核心斷言匹配器
| 分類 | 匹配器 | 說明 |
| 狀態 | status().isOk() | 200 |
status().isCreated() | 201 |
status().isBadRequest() | 400 |
status().isNotFound() | 404 |
| 內容 | content().string("text") | 完整字串匹配 |
content().json("{}") | JSON 結構匹配 |
content().contentType(MediaType.APPLICATION_JSON) | Content-Type |
content().contentTypeCompatibleWith(...) | 兼容類型 |
content().encoding("UTF-8") | 編碼 |
| JSON Path | jsonPath("$.id").exists() | 節點存在 |
jsonPath("$.name").value("張三") | 值精確匹配 |
jsonPath("$.age").value(greaterThan(18)) | Hamcrest 匹配器 |
jsonPath("$.items[*].id").value(hasSize(3)) | 陣列大小 |
jsonPath("$..name").value(contains("張三")) | 遞歸搜尋包含 |
jsonPath("$.timestamp").value(matchesPattern("\\d{13}")) | 正則匹配 |
| Header | header().string("Location", "/users/1") | Header 值 |
header().exists("ETag") | Header 存在 |
header().stringValues("Set-Cookie", contains("session=")) | 多值 Header |
header().longValue("Content-Length", greaterThan(0L)) | 數值 Header |
| Cookie | cookie().value("JSESSIONID", notNullValue()) | Cookie 值 |
cookie().maxAge("remember-me", greaterThan(0)) | Max-Age |
cookie().httpOnly("auth", true) | HttpOnly 標記 |
完整測試類範例
@WebMvcTest(UserController.class) // 只載入 Web 層,快速啟動
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("建立用戶 - 成功")
void createUser_success() throws Exception {
// Given
UserDTO dto = new UserDTO("張三", "zhangsan@example.com");
UserVO vo = new UserVO(1L, "張三", "zhangsan@example.com");
when(userService.create(any())).thenReturn(vo);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("張三"))
.andExpect(jsonPath("$.email").value("zhangsan@example.com"))
.andDo(print());
}
@Test
@DisplayName("查詢用戶 - 不存在返回 404")
void getUser_notFound() throws Exception {
when(userService.getById(999L)).thenThrow(new UserNotFoundException(999L));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("USER_NOT_FOUND"))
.andExpect(jsonPath("$.message").value("用戶不存在: 999"));
}
}
進階技巧
1. 自定義 ResultMatcher 封裝業務斷言
2. 測試異步 Controller (DeferredResult/Callable)
@WithMockUser(username = "admin", roles = "ADMIN") // 方法級
// 或
.with(user("admin").roles("ADMIN")) // 請求級
4. 獨立設置模式 (非 @WebMvcTest)
常見問題排查
| 問題 | 原因 | 解決 |
| 406 Not Acceptable | Accept 標頭不匹配 produces | 檢查 @RequestMapping(produces) 或請求 .accept() |
| 415 Unsupported Media Type | Content-Type 不匹配 consumes | 請求 .contentType(MediaType.APPLICATION_JSON) |
| JSON 斷言失敗 | 字段名/類型不匹配 | 用 .andDo(print()) 列印實際回應對比 |
| MockBean 不生效 | 未掃描到配置類 | 確認 @SpringBootTest/@WebMvcTest 掃描路徑 |
總結
- 靜態導入
MockMvcRequestBuilders.*、MockMvcResultMatchers.*、MockMvcResultHandlers.* 是標準配置
- 鏈式 API 讓測試代碼如文檔般清晰:
perform → andExpect → andDo
- 善用
jsonPath + Hamcrest 匹配器進行精準 JSON 驗證
.andDo(print()) 是調試利器,CI 失效時首選查看輸出
- 優先使用
@WebMvcTest 切片測試,啟動快、聚焦 Web 層
掌握 MockMvc 靜態導入與流暢 API,能讓 Controller 測試從「寫起來痛」變成「讀起來爽」,大幅提升測試覆蓋率與維護性。