測試Spring MVC應用

由David發表在天碼營

Spring的依賴注入使得我們的代碼非常容易進行單元測試——@Controller, @Service,@Entity等註解標註的類基本都是POJO(plain old Java object),也就是說很少依賴於Spring容器本身的API。我們可以非常容易地使用JUnit或TestNG編寫測試代碼。另一方面,對於三層架構的Spring Web應用(Controller, Service, DAO),使用Mock活Stub方法也能夠更好的來測試我們的代碼邏輯。例如Service層代碼的單元測試中,依賴的DAO(或Repository)對象都是根據應用測試需求Mock出來的,而不需要真正去訪問資料庫。

Spring Web測試

在對Spring Web應用中的@Controller代碼進行單元測試的過程中,一般的方法是創建@Controller對象,同時將它依賴的一些Mock對象——例如MockHttpServletRequest, MockHttpServletResponse(都由spring-test模塊提供,無需自己編寫)作為@Controller方法的參數。但是對於處理Web請求的@Controller代碼來說,僅僅測試Handler方法里的代碼是遠遠不夠的,對於一個處理HTTP請求的@Controller`,我們還需要測試:

  • @RequestMapping路由是否正確
  • 數據綁定、類型轉換、校驗邏輯是否正確——數據包括URL參數、表單、@PathVariable等
  • @InitBinder, @ModelAttribute, @ExceptionHandler等註解的方法或屬性計算過程

上述過程貫穿於HTTP請求處理的生命周期中,所以對於Spring Web應用中@Controller代碼單元測試的概念,應該做一些擴充——不僅僅局限於代碼本身,也要結合MVC框架中的各個處理過程。

本文接下來的內容代碼,都以Spring Boot為例,首先假設我們通過Spring Boot創建了一個最簡單的Web Mvc應用——包含了一個最簡單的Conroller,處理/users/{id}對應的HTTP請求,返回值是id={id}(通過String.format()方法),那麼可以為它創建如下測試代碼:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;nn@RunWith(SpringJUnit4ClassRunner.class)n@SpringApplicationConfiguration(classes = SpringMvcTestDemoApplication.class)n@WebAppConfigurationnpublic class SpringMvcTestDemoApplicationTests {nn private MockMvc mockMvc;nn @Beforen public void init() {n this.mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();n }nn @Testn public void getUserById() throws Exception {n long id = 1;n this.mockMvc.perform(get("/users/" + id))n .andExpect(status().isOk())n .andExpect(content().string("id=" + id));n }nn}n

運行上述測試時,很容易從控制台中的日誌發現,SpringJUnit4ClassRunner創建了一個Spring Web應用上下文,並且在其中進行了Web Mvc框架的配置——這裡是註冊@RequestMapping方法。接下來mockMvc.perform()方法實際上向該Spring Web應用發起了一個HTTP請求:

  • 請求的url為/users/{id}
  • andExpect()方法也就是測試中常用的Assert
  • status()用於檢查返回狀態嗎,這裡是200
  • content()用於檢查內容

如果我們不小心將@RequestMapping的路由路徑寫錯,那麼這裡運行的結果一定不會是status().isOk(),這也就完成了對HTTP請求路由的測試。接下來我們將繼續探索MVC框架中的其他方面。

Mock Service

在Spring Web應用三層結構里,Controller層代碼通常會調用Service層代碼,例如:

@RestControllernpublic class UserController {nn @Autowiredn private UserService userService;nn @RequestMapping(value = "/users/{id}", method = GET)n public String get(@PathVariable("id") long id) {n String username = userService.getUsername(id);n return String.format("username=%s", username);n }n}n

對UserController進行單元測試需要排除Service代碼的影響,所以需要對Service進行Mock,這裡我們使用Mockito框架,在Spring上下文中Mock一個UserService對象:

@Configurationnpublic class TestContext {nn @Beann public UserService userServiceMock() {n return Mockito.mock(UserService.class);n }n}n

同時通過Mockito的API來MockUserService.getUsername(long id)方法,@Controller的測試代碼如下:

@RunWith(SpringJUnit4ClassRunner.class)n@SpringApplicationConfiguration(classes = {n SpringMvcTestDemoApplication.class,n TestContext.classn})n@WebAppConfigurationnpublic class SpringMvcTestDemoApplicationTests {nn @Autowiredn UserService userService;nn @Autowiredn UserController controller;nn MockMvc mockMvc;nn @Beforen public void init() {n this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();n }nn @Testn public void getUserById() throws Exception {n long id = 1L;n String ricky = "Ricky";n Mockito.when(userService.getUsername(id)).thenReturn(ricky);n this.mockMvc.perform(get("/users/" + id))n .andExpect(status().isOk())n .andExpect(content().string("username=" + ricky));n }nn}n

由於需要進行依賴注入,所以UserService和UserController都使用@Autowired註解。Mockito.when(userService.getUsername(id)).thenReturn(ricky);表明userService.getUsername()方法的參數為1L時,返回值為"Ricky",Mockito提供能很多強大的Mock API,更多用法請參考官方文檔。

測試REST API

當我們構建REST服務時,大多數情況會使用JSON作為數據交換格式,Spring MVC測試框架同樣提供了一種簡潔的方式對JSON結果進行斷言,假設現在有@Controller如下:

@RequestMapping(value = "/users/{id}/json", method = GET)npublic User getUser(@PathVariable("id") long id) {n String username = userService.getUsername(id);n return new User(id, username);n}nnstatic class User {n public long id;n public String username;n //構造方法,Getter/Setter略n}n

實際應用返回的JSON數據是:

{n "id": 1,n "username": "Ricky"n}n

測試代碼可以這樣斷言:

@Testnpublic void getUser() throws Exception {n this.mockMvc.perform(get("/users/{id}/json", id).accept(MediaType.APPLICATION_JSON))n .andExpect(status().isOk())n .andExpect(jsonPath("$.id").value(id.intValue()))n .andExpect(jsonPath("$.username").value(ricky));n}n

$.id, $.username都是JsonPath提供的JSON表達式,可以通過jsonPath、value()等方法來輕鬆對JSON數據進行斷言而不需要自己編寫JSON文本處理。

推薦閱讀:

SpringMVC 如何與redis整合開發?
@Autowired和@Resource的區別是什麼?
Spring MVC異常處理

TAG:Spring | SpringMVC框架 |