javascript
Spring MVC控制器的单元测试:REST API
Spring MVC提供了一種創(chuàng)建REST API的簡便方法。 但是,為這些API編寫全面而快速的單元測試一直很麻煩。 Spring MVC測試框架的發(fā)布使我們可以編寫可讀,全面且快速的單元測試。
這篇博客文章描述了如何使用Spring MVC Test框架編寫REST API的單元測試。 在這篇博客中,我們將為控制器方法編寫單元測試,這些方法為待辦事項提供CRUD功能。
讓我們開始吧。
使用Maven獲取所需的依賴關(guān)系
通過將以下依賴項聲明添加到我們的POM文件中,我們可以獲得所需的測試依賴項:
- Hamcrest 1.3( hamcrest-all )。 在為響應(yīng)編寫斷言時,我們使用Hamcrest匹配器。
- Junit 4.11。 我們需要排除hamcrest-core依賴性,因為我們已經(jīng)添加了hamcrest-all依賴性。
- Mockito 1.9.5( mockito-core )。 我們使用Mockito作為我們的模擬庫。
- Spring測試3.2.3發(fā)布
- JsonPath 0.8.1( json-path和json-path-assert )。 在為REST API返回的JSON文檔編寫斷言時,我們使用JsonPath。
相關(guān)的依賴項聲明如下所示:
<dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest-all</artifactId><version>1.3</version><scope>test</scope> </dependency> <dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope><exclusions><exclusion><artifactId>hamcrest-core</artifactId><groupId>org.hamcrest</groupId></exclusion></exclusions> </dependency> <dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>1.9.5</version><scope>test</scope> </dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>3.2.3.RELEASE</version><scope>test</scope> </dependency> <dependency><groupId>com.jayway.jsonpath</groupId><artifactId>json-path</artifactId><version>0.8.1</version><scope>test</scope> </dependency> <dependency><groupId>com.jayway.jsonpath</groupId><artifactId>json-path-assert</artifactId><version>0.8.1</version><scope>test</scope> </dependency>讓我們繼續(xù)討論一下單元測試的配置。
配置我們的單元測試
我們將在此博客文章中編寫的單元測試使用基于Web應(yīng)用程序上下文的配置。 這意味著我們通過使用應(yīng)用程序上下文配置類或XML配置文件來配置Spring MVC基礎(chǔ)結(jié)構(gòu)。
因為本教程的第一部分描述了配置應(yīng)用程序的應(yīng)用程序上下文時應(yīng)遵循的原則,所以本博文中未討論此問題。
但是,我們必須在這里解決一件事。
配置示例應(yīng)用程序的Web層的應(yīng)用程序上下文配置類(或文件)不會創(chuàng)建異常解析器bean。 本教程前面部分中使用的SimpleMappingExceptionResolver類將異常類名稱映射到拋出配置的異常時呈現(xiàn)的視圖。
如果我們正在實現(xiàn)“常規(guī)” Spring MVC應(yīng)用程序,那么這是有道理的。 但是,如果要實現(xiàn)REST API,則希望將異常轉(zhuǎn)換為HTTP狀態(tài)代碼。 默認(rèn)情況下,此行為由ResponseStatusExceptionResolver類提供。
我們的示例應(yīng)用程序還具有一個自定義異常處理程序類,該類以@ControllerAdvice批注進(jìn)行批注 。 此類處理驗證錯誤和應(yīng)用程序特定的異常。 我們將在本博客文章的后面部分詳細(xì)討論此類。
讓我們繼續(xù)前進(jìn),了解如何為REST API編寫單元測試。
編寫REST API的單元測試
在開始為REST API編寫單元測試之前,我們需要了解兩點:
- 我們需要知道Spring MVC Test框架的核心組件是什么。 這些組件在本教程的第二部分中進(jìn)行了描述。
- 我們需要知道如何使用JsonPath表達(dá)式編寫JSON文檔的斷言。 我們可以通過閱讀我的博客文章獲得此信息,該文章描述了如何使用JsonPath編寫干凈的斷言 。
接下來,我們將看到運(yùn)行中的Spring MVC Test框架,并為以下控制器方法編寫單元測試:
- 第一個控制器方法返回待辦事項列表。
- 第二種控制器方法返回單個待辦事項的信息。
- 第三種控制器方法將新的待辦事項條目添加到數(shù)據(jù)庫,并返回添加的待辦事項條目。
獲取待辦事項
第一個控制器方法返回從數(shù)據(jù)庫中找到的待辦事項列表。 讓我們先來看一下該方法的實現(xiàn)。
預(yù)期行為
通過執(zhí)行以下步驟來實現(xiàn)將所有待辦事項返回到數(shù)據(jù)庫的控制器方法:
TodoController類的相關(guān)部分如下所示:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;import java.util.ArrayList; import java.util.List;@Controller public class TodoController {private TodoService service;@RequestMapping(value = "/api/todo", method = RequestMethod.GET)@ResponseBodypublic List<TodoDTO> findAll() {List<Todo> models = service.findAll();return createDTOs(models);}private List<TodoDTO> createDTOs(List<Todo> models) {List<TodoDTO> dtos = new ArrayList<>();for (Todo model: models) {dtos.add(createDTO(model));}return dtos;}private TodoDTO createDTO(Todo model) {TodoDTO dto = new TodoDTO();dto.setId(model.getId());dto.setDescription(model.getDescription());dto.setTitle(model.getTitle());return dto;} }當(dāng)返回TodoDTO對象列表時,Spring MVC將此列表轉(zhuǎn)換為包含對象集合的JSON文檔。 返回的JSON文檔如下所示:
[{"id":1,"description":"Lorem ipsum","title":"Foo"},{"id":2,"description":"Lorem ipsum","title":"Bar"} ]讓我們繼續(xù)并編寫一個單元測試,以確保此控制器方法按預(yù)期工作。
測試:找到待辦事項
通過執(zhí)行以下步驟,我們可以為此控制器方法編寫單元測試:
我們的單元測試的源代碼如下所示:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc;import java.util.Arrays;import static org.hamcrest.Matchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate TodoService todoServiceMock;//Add WebApplicationContext field here.//The setUp() method is omitted.@Testpublic void findAll_TodosFound_ShouldReturnFoundTodoEntries() throws Exception {Todo first = new TodoBuilder().id(1L).description("Lorem ipsum").title("Foo").build();Todo second = new TodoBuilder().id(2L).description("Lorem ipsum").title("Bar").build();when(todoServiceMock.findAll()).thenReturn(Arrays.asList(first, second));mockMvc.perform(get("/api/todo")).andExpect(status().isOk()).andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8)).andExpect(jsonPath("$", hasSize(2))).andExpect(jsonPath("$[0].id", is(1))).andExpect(jsonPath("$[0].description", is("Lorem ipsum"))).andExpect(jsonPath("$[0].title", is("Foo"))).andExpect(jsonPath("$[1].id", is(2))).andExpect(jsonPath("$[1].description", is("Lorem ipsum"))).andExpect(jsonPath("$[1].title", is("Bar")));verify(todoServiceMock, times(1)).findAll();verifyNoMoreInteractions(todoServiceMock);} }我們的單元測試使用一個稱為APPLICATION_JSON_UTF8的常量,該常量在TestUtil類中聲明。 該常量的值是MediaType對象,其內(nèi)容類型為“ application / json”,字符集為“ UTF-8”。
TestUtil類的相關(guān)部分如下所示:
public class TestUtil {public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8") ); }獲取待辦事項條目
我們必須測試的第二個控制器方法返回單個待辦事項的信息。 讓我們找出如何實現(xiàn)此控制器方法。
預(yù)期行為
通過執(zhí)行以下步驟來實現(xiàn)返回單個待辦事項信息的控制器方法:
我們的控制器方法的源代碼如下所示:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;@Controller public class TodoController {private TodoService service;@RequestMapping(value = "/api/todo/{id}", method = RequestMethod.GET)@ResponseBodypublic TodoDTO findById(@PathVariable("id") Long id) throws TodoNotFoundException {Todo found = service.findById(id);return createDTO(found);}private TodoDTO createDTO(Todo model) {TodoDTO dto = new TodoDTO();dto.setId(model.getId());dto.setDescription(model.getDescription());dto.setTitle(model.getTitle());return dto;} }返回給客戶端的JSON文檔如下所示:
{"id":1,"description":"Lorem ipsum","title":"Foo" }我們的下一個問題是:
拋出TodoNotFoundException會發(fā)生什么?
我們的示例應(yīng)用程序具有一個異常處理程序類,該類處理由控制器類拋出的應(yīng)用程序特定的異常。 此類具有異常處理程序方法,當(dāng)拋出TodoNotFoundException時將調(diào)用該方法。 此方法的實現(xiàn)將新的日志消息寫入日志文件,并確保將HTTP狀態(tài)代碼404發(fā)送回客戶端。
RestErrorHandler類的相關(guān)部分如下所示:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus;@ControllerAdvice public class RestErrorHandler {private static final Logger LOGGER = LoggerFactory.getLogger(RestErrorHandler.class);@ExceptionHandler(TodoNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND)public void handleTodoNotFoundException(TodoNotFoundException ex) {LOGGER.debug("handling 404 error on a todo entry");} }我們必須為此控制器方法編寫兩個單元測試:
讓我們看看如何編寫這些測試。
測試1:找不到待辦事項條目
首先,當(dāng)找不到待辦事項時,我們必須確保我們的應(yīng)用程序正常運(yùn)行。 我們可以按照以下步驟編寫一個單元測試來確保這一點:
我們的單元測試的源代碼如下所示:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc;import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate TodoService todoServiceMock;//Add WebApplicationContext field here.//The setUp() method is omitted.@Testpublic void findById_TodoEntryNotFound_ShouldReturnHttpStatusCode404() throws Exception {when(todoServiceMock.findById(1L)).thenThrow(new TodoNotFoundException(""));mockMvc.perform(get("/api/todo/{id}", 1L)).andExpect(status().isNotFound());verify(todoServiceMock, times(1)).findById(1L);verifyNoMoreInteractions(todoServiceMock);} }測試2:找到Todo條目
其次,我們必須編寫一個測試,以確保在找到請求的待辦事項條目時返回正確的數(shù)據(jù)。 我們可以按照以下步驟編寫測試來確保這一點:
我們的單元測試的源代碼如下所示:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc;import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.*; 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.MockMvcResultMatchers.jsonPath;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate TodoService todoServiceMock;//Add WebApplicationContext field here.//The setUp() method is omitted.@Testpublic void findById_TodoEntryFound_ShouldReturnFoundTodoEntry() throws Exception {Todo found = new TodoBuilder().id(1L).description("Lorem ipsum").title("Foo").build();when(todoServiceMock.findById(1L)).thenReturn(found);mockMvc.perform(get("/api/todo/{id}", 1L)).andExpect(status().isOk()).andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8)).andExpect(jsonPath("$.id", is(1))).andExpect(jsonPath("$.description", is("Lorem ipsum"))).andExpect(jsonPath("$.title", is("Foo")));verify(todoServiceMock, times(1)).findById(1L);verifyNoMoreInteractions(todoServiceMock);} }添加新的待辦事項
第三種控制器方法將新的待辦事項條目添加到數(shù)據(jù)庫,并返回添加的待辦事項條目的信息。 讓我們繼續(xù)前進(jìn),了解它是如何實現(xiàn)的。
預(yù)期行為
通過執(zhí)行以下步驟來實現(xiàn)向數(shù)據(jù)庫添加新的待辦事項條目的控制器方法:
我們的控制器方法的源代碼如下所示:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;import javax.validation.Valid;@Controller public class TodoController {private TodoService service;@RequestMapping(value = "/api/todo", method = RequestMethod.POST)@ResponseBodypublic TodoDTO add(@Valid @RequestBody TodoDTO dto) {Todo added = service.add(dto);return createDTO(added);}private TodoDTO createDTO(Todo model) {TodoDTO dto = new TodoDTO();dto.setId(model.getId());dto.setDescription(model.getDescription());dto.setTitle(model.getTitle());return dto;} }TodoDTO類是一個簡單的DTO類,其源代碼如下所示:
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty;public class TodoDTO {private Long id;@Length(max = 500)private String description;@NotEmpty@Length(max = 100)private String title;//Constructor and other methods are omitted. }如我們所見,該類聲明了以下三個約束條件:
如果驗證失敗,我們的錯誤處理程序組件將確保
因為我已經(jīng)寫了一篇博客文章 ,描述了我們?nèi)绾蜗騌EST API添加驗證,所以本文中沒有討論錯誤處理程序組件的實現(xiàn)。
但是,如果驗證失敗,我們需要知道將哪種JSON文檔返回給客戶端。 該信息在下面給出。
如果TodoDTO對象的標(biāo)題和描述太長,則會將以下JSON文檔返回給客戶端:
{"fieldErrors":[{"path":"description","message":"The maximum length of the description is 500 characters."},{"path":"title","message":"The maximum length of the title is 100 characters."}] }注意 :Spring MVC不保證字段錯誤的順序。 換句話說,場錯誤以隨機(jī)順序返回。 在為該控制器方法編寫單元測試時,必須考慮到這一點。
另一方面,如果驗證沒有失敗,那么我們的控制器方法將以下JSON文檔返回給客戶端:
{"id":1,"description":"description","title":"todo" }我們必須為此控制器方法編寫兩個單元測試:
讓我們找出如何編寫這些測試。
測試1:驗證失敗
我們的第一個測試確保當(dāng)添加的todo條目的驗證失敗時,我們的應(yīng)用程序可以正常運(yùn)行。 我們可以按照以下步驟編寫此測試:
我們的單元測試的源代碼如下所示:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc;import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.*; 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.MockMvcResultMatchers.jsonPath;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate TodoService todoServiceMock;//Add WebApplicationContext field here.//The setUp() method is omitted.@Testpublic void add_TitleAndDescriptionAreTooLong_ShouldReturnValidationErrorsForTitleAndDescription() throws Exception {String title = TestUtil.createStringWithLength(101);String description = TestUtil.createStringWithLength(501);TodoDTO dto = new TodoDTOBuilder().description(description).title(title).build();mockMvc.perform(post("/api/todo").contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(dto))).andExpect(status().isBadRequest()).andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8)).andExpect(jsonPath("$.fieldErrors", hasSize(2))).andExpect(jsonPath("$.fieldErrors[*].path", containsInAnyOrder("title", "description"))).andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder("The maximum length of the description is 500 characters.","The maximum length of the title is 100 characters.")));verifyZeroInteractions(todoServiceMock);} }我們的單元測試使用TestUtil類的兩個靜態(tài)方法。 下面介紹了這些方法:
- createStringWithLength(int length)方法使用給定的長度創(chuàng)建一個新的String對象,并返回創(chuàng)建的對象。
- convertObjectToJsonBytes(Object object)方法將作為方法參數(shù)給出的對象轉(zhuǎn)換為JSON文檔,并將該文檔的內(nèi)容作為字節(jié)數(shù)組返回 。
TestUtil類的源代碼如下所示:
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType;import java.io.IOException; import java.nio.charset.Charset;public class TestUtil {public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));public static byte[] convertObjectToJsonBytes(Object object) throws IOException {ObjectMapper mapper = new ObjectMapper();mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);return mapper.writeValueAsBytes(object);}public static String createStringWithLength(int length) {StringBuilder builder = new StringBuilder();for (int index = 0; index < length; index++) {builder.append("a");}return builder.toString();} }測試2:Todo條目已添加到數(shù)據(jù)庫
第二個單元測試確保將新的待辦事項添加到數(shù)據(jù)庫時,控制器能夠正常工作。 我們可以按照以下步驟編寫此測試:
我們的單元測試的源代碼如下所示:
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc;import static junit.framework.Assert.assertNull; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.*; 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.MockMvcResultMatchers.jsonPath;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate TodoService todoServiceMock;//Add WebApplicationContext field here.//The setUp() method is omitted.@Testpublic void add_NewTodoEntry_ShouldAddTodoEntryAndReturnAddedEntry() throws Exception {TodoDTO dto = new TodoDTOBuilder().description("description").title("title").build();Todo added = new TodoBuilder().id(1L).description("description").title("title").build();when(todoServiceMock.add(any(TodoDTO.class))).thenReturn(added);mockMvc.perform(post("/api/todo").contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(dto))).andExpect(status().isOk()).andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8)).andExpect(jsonPath("$.id", is(1))).andExpect(jsonPath("$.description", is("description"))).andExpect(jsonPath("$.title", is("title")));ArgumentCaptor<TodoDTO> dtoCaptor = ArgumentCaptor.forClass(TodoDTO.class);verify(todoServiceMock, times(1)).add(dtoCaptor.capture());verifyNoMoreInteractions(todoServiceMock);TodoDTO dtoArgument = dtoCaptor.getValue();assertNull(dtoArgument.getId());assertThat(dtoArgument.getDescription(), is("description"));assertThat(dtoArgument.getTitle(), is("title"));} }摘要
現(xiàn)在,我們已經(jīng)使用Spring MVC Test框架為REST API編寫了單元測試。 本教程教會了我們四件事:
- 我們學(xué)習(xí)了為控制器方法編寫單元測試,這些方法從數(shù)據(jù)庫中讀取信息。
- 我們學(xué)習(xí)了為控制器方法編寫單元測試,這些方法將信息添加到數(shù)據(jù)庫中。
- 我們了解了如何將DTO對象轉(zhuǎn)換為JSON字節(jié)并將轉(zhuǎn)換結(jié)果發(fā)送到請求正文中。
- 我們學(xué)習(xí)了如何使用JsonPath表達(dá)式編寫JSON文檔的斷言。
與往常一樣,此博客文章的示例應(yīng)用程序可在Github上獲得 。 我建議您檢查一下,因為它有很多單元測試,而本博客文章中未涉及。
翻譯自: https://www.javacodegeeks.com/2013/08/unit-testing-of-spring-mvc-controllers-rest-api.html
總結(jié)
以上是生活随笔為你收集整理的Spring MVC控制器的单元测试:REST API的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 世嘉游戏《符文工厂 3 豪华版》Stea
- 下一篇: 京东联合多方启动超高清投影标准制定