Spring MVC控制器的单元测试:REST API

Spring MVC提供了一种创建REST API的简便方法。 但是,为这些API编写全面而快速的单元测试一直很麻烦。 Spring MVC测试框架的发布使我们可以编写可读,全面且快速的单元测试。

这篇博客文章描述了如何使用Spring MVC Test框架编写REST API的单元测试。 在这篇博客中,我们将为控制器方法编写单元测试,这些方法为待办事项提供CRUD功能。

让我们开始吧。

使用Maven获取所需的依赖关系

通过将以下依赖项声明添加到我们的POM文件中,我们可以获得所需的测试依赖项:

  • Hamcrest 1.3( hamcrest-all )。 在为响应编写断言时,我们使用Hamcrest匹配器。
  • Junit 4.11。 我们需要排除hamcrest-core依赖性,因为我们已经添加了hamcrest-all依赖性。
  • Mockito 1.9.5( mockito-core )。 我们使用Mockito作为我们的模拟库。
  • Spring测试3.2.3发布
  • JsonPath 0.8.1( json-pathjson-path-assert )。 在为REST API返回的JSON文档编写断言时,我们使用JsonPath。

相关的依赖项声明如下所示:

<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>

让我们继续讨论一下单元测试的配置。

配置我们的单元测试

我们将在此博客文章中编写的单元测试使用基于Web应用程序上下文的配置。 这意味着我们通过使用应用程序上下文配置类或XML配置文件来配置Spring MVC基础结构。

因为本教程的第一部分描述了配置应用程序的应用程序上下文时应遵循的原则,所以本博文中未讨论此问题。

但是,我们必须在这里解决一件事。

配置示例应用程序的Web层的应用程序上下文配置类(或文件)不会创建异常解析器bean。 本教程前面部分中使用的SimpleMappingExceptionResolver类将异常类名称映射到抛出配置的异常时呈现的视图。

如果我们正在实现“常规” Spring MVC应用程序,那么这是有道理的。 但是,如果要实现REST API,则希望将异常转换为HTTP状态代码。 默认情况下,此行为由ResponseStatusExceptionResolver类提供。

我们的示例应用程序还具有一个自定义异常处理程序类,该类以@ControllerAdvice批注进行批注 。 此类处理验证错误和应用程序特定的异常。 我们将在本博客文章的后面部分详细讨论此类。

让我们继续前进,了解如何为REST API编写单元测试。

编写REST API的单元测试

在开始为REST API编写单元测试之前,我们需要了解两点:

  • 我们需要知道Spring MVC Test框架的核心组件是什么。 这些组件在本教程的第二部分中进行了描述。
  • 我们需要知道如何使用JsonPath表达式编写JSON文档的断言。 我们可以通过阅读我的博客文章获得此信息,该文章描述了如何使用JsonPath编写干净的断言 。

接下来,我们将看到运行中的Spring MVC Test框架,并为以下控制器方法编写单元测试:

  • 第一个控制器方法返回待办事项列表。
  • 第二种控制器方法返回单个待办事项的信息。
  • 第三种控制器方法将新的待办事项条目添加到数据库,并返回添加的待办事项条目。

获取待办事项

第一个控制器方法返回从数据库中找到的待办事项列表。 让我们先来看一下该方法的实现。

预期行为

通过执行以下步骤来实现将所有待办事项返回到数据库的控制器方法:

  1. 它处理发送到url'/ api / todo'的GET请求。
  2. 它通过调用TodoService接口的findAll()方法获取Todo对象的列表。 此方法返回存储在数据库中的所有待办事项。 这些待办事项条目总是以相同的顺序返回。
  3. 它将接收到的列表转换为TodoDTO对象的列表。
  4. 它返回包含TodoDTO对象的列表。

TodoController类的相关部分如下所示:

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;}
}

当返回TodoDTO对象列表时,Spring MVC将此列表转换为包含对象集合的JSON文档。 返回的JSON文档如下所示:

[{"id":1,"description":"Lorem ipsum","title":"Foo"},{"id":2,"description":"Lorem ipsum","title":"Bar"}
]

让我们继续并编写一个单元测试,以确保此控制器方法按预期工作。

测试:找到待办事项

通过执行以下步骤,我们可以为此控制器方法编写单元测试:

  1. 创建测试数据,该数据将在调用TodoService接口的findAll()方法时返回。 我们通过使用测试数据构建器类来创建测试数据。
  2. 配置我们的模拟对象,使其在调用findAll()方法时返回创建的测试数据。
  3. 执行GET请求以获取网址“ / api / todo”。
  4. 验证是否返回HTTP状态代码200。
  5. 确认响应的内容类型为“ application / json”,其字符集为“ UTF-8”。
  6. 使用JsonPath表达式$获取待办事项的集合,并确保返回两个待办事项。
  7. 通过使用JsonPath表达式$ [0] .id$ [0] .description$ [0] .title来获取第一个todo条目的id描述标题 。 验证是否返回了正确的值。
  8. 通过使用JsonPath表达式$ [1] .id$ [1] .description$ [1] .title来获取第二个待办事项的ID描述和标题。 验证是否返回了正确的值。
  9. 验证TodoService接口的findAll()方法仅被调用一次。
  10. 确保在测试过程中没有调用模拟对象的其他方法。

我们的单元测试的源代码如下所示:

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对象,其内容类型为“ application / json”,字符集为“ UTF-8”。

TestUtil类的相关部分如下所示:

public class TestUtil {public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(),                       Charset.forName("utf8")                    );
}

获取待办事项条目

我们必须测试的第二个控制器方法返回单个待办事项的信息。 让我们找出如何实现此控制器方法。

预期行为

通过执行以下步骤来实现返回单个待办事项信息的控制器方法:

  1. 它处理发送到url'/ api / todo / {id}'的GET请求。 {id}是一个路径变量,其中包含请求的待办事项条目的ID
  2. 它通过调用TodoService接口的findById()方法获取请求的待办事项条目,并将请求的待办事项条目的ID作为方法参数传递。 此方法返回找到的待办事项条目。 如果未找到待办事项条目,则此方法将抛出TodoNotFoundException
  3. 它将Todo对象转换为TodoDTO对象。
  4. 它返回创建的TodoDTO对象。

我们的控制器方法的源代码如下所示:

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会发生什么?

我们的示例应用程序具有一个异常处理程序类,该类处理由控制器类抛出的应用程序特定的异常。 此类具有异常处理程序方法,当抛出TodoNotFoundException时将调用该方法。 此方法的实现将新的日志消息写入日志文件,并确保将HTTP状态代码404发送回客户端。

RestErrorHandler类的相关部分如下所示:

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. 我们必须编写一个测试,以确保在未找到todo条目时,我们的应用程序能够正常运行。
  2. 我们必须编写一个测试,以在找到待办事项条目时验证是否向客户端返回了正确的数据。

让我们看看如何编写这些测试。

测试1:找不到待办事项条目

首先,当找不到待办事项时,我们必须确保我们的应用程序正常运行。 我们可以按照以下步骤编写一个单元测试来确保这一点:

  1. 将模拟对象配置为在调用findById()方法且请求的待办事项条目的ID为1L时引发TodoNotFoundException
  2. 执行GET请求以获取url'/ api / todo / 1'。
  3. 验证是否返回了HTTP状态代码404。
  4. 确保使用正确的方法参数(1L)仅调用一次TodoService接口的findById()方法。
  5. 验证在此测试期间没有调用TodoService接口的其他方法。

我们的单元测试的源代码如下所示:

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条目

其次,我们必须编写一个测试,以确保在找到请求的待办事项条目时返回正确的数据。 我们可以按照以下步骤编写测试来确保这一点:

  1. 创建Todo对象,该对象在调用我们的service方法时返回。 我们使用测试数据生成器创建此对象。
  2. 配置我们的模拟对象以在使用方法参数1L调用其findById()方法时返回创建的Todo对象。
  3. 执行GET请求以获取url'/ api / todo / 1'。
  4. 验证是否返回HTTP状态代码200。
  5. 确认响应的内容类型为“ application / json”,其字符集为“ UTF-8”。
  6. 通过使用JsonPath表达式$ .id获取待办事项的ID ,并验证ID为1。
  7. 使用JsonPath表达式$ .description获取待办事项的描述 ,并验证该描述是否为“ Lorem ipsum”。
  8. 通过使用JsonPath表达式$ .title获取待办事项的标题 ,并验证标题为“ Foo”。
  9. 确保使用正确的方法参数(1L)仅调用一次TodoService接口的findById()方法。
  10. 验证测试期间未调用模拟对象的其他方法。

我们的单元测试的源代码如下所示:

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);}
}

添加新的待办事项

第三种控制器方法将新的待办事项条目添加到数据库,并返回添加的待办事项条目的信息。 让我们继续前进,了解它是如何实现的。

预期行为

通过执行以下步骤来实现向数据库添加新的待办事项条目的控制器方法:

  1. 它处理发送到url'/ api / todo'的POST请求。
  2. 它验证作为方法参数给出的TodoDTO对象。 如果验证失败,则抛出MethodArgumentNotValidException
  3. 它通过调用TodoService接口的add()方法向数据库添加一个新的todo条目,并将TodoDTO对象作为方法参数传递。 此方法将新的待办事项添加到数据库,并返回添加的待办事项。
  4. 它将创建的Todo对象转换为TodoDTO对象。
  5. 它返回TodoDTO对象。

我们的控制器方法的源代码如下所示:

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.
}

如我们所见,该类声明了以下三个约束条件:

  1. 描述的最大长度为500个字符。
  2. 待办事项的标题不能为空。
  3. 标题的最大长度为100个字符。

如果验证失败,我们的错误处理程序组件将确保

  1. HTTP状态代码400返回到客户端。
  2. 验证错误将作为JSON文档返回给客户端。

因为我已经写了一篇博客文章 ,描述了我们如何向REST API添加验证,所以本文中没有讨论错误处理程序组件的实现。

但是,如果验证失败,我们需要知道将哪种JSON文档返回给客户端。 该信息在下面给出。

如果TodoDTO对象的标题描述太长,则会将以下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不保证字段错误的顺序。 换句话说,场错误以随机顺序返回。 在为该控制器方法编写单元测试时,必须考虑到这一点。

另一方面,如果验证没有失败,那么我们的控制器方法将以下JSON文档返回给客户端:

{"id":1,"description":"description","title":"todo"
}

我们必须为此控制器方法编写两个单元测试:

  1. 我们必须编写一个测试,以确保验证失败时我们的应用程序能够正常运行。
  2. 我们必须编写一个测试,以确保在将新的待办事项添加到数据库时,我们的应用程序能够正常运行。

让我们找出如何编写这些测试。

测试1:验证失败

我们的第一个测试确保当添加的todo条目的验证失败时,我们的应用程序可以正常运行。 我们可以按照以下步骤编写此测试:

  1. 创建一个具有101个字符的标题
  2. 创建一个包含501个字符的描述
  3. 使用我们的测试数据构建器创建一个新的TodoDTO对象。 设置对象的标题描述
  4. 执行POST请求以发送url'/ api / todo'。 将请求的内容类型设置为“ application / json”。 将请求的字符集设置为“ UTF-8”。 将创建的TodoDTO对象转换为JSON字节并将其发送到请求的正文中。
  5. 验证是否返回了HTTP状态代码400。
  6. 验证响应的内容类型为“ application / json”,并且其内容类型为“ UTF-8”。
  7. 通过使用JsonPath表达式$ .fieldErrors提取字段错误,并确保返回两个字段错误。
  8. 通过使用JsonPath表达式$ .fieldErrors [*]。path获取所有可用路径,并确保找到有关titledescription字段的字段错误。
  9. 通过使用JsonPath表达式$ .fieldErrors [*]。message来获取所有可用的错误消息,并确保找到有关标题描述字段的错误消息。
  10. 验证测试期间未调用模拟对象的方法。

我们的单元测试的源代码如下所示:

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类的两个静态方法。 下面介绍了这些方法:

  • createStringWithLength(int length)方法使用给定的长度创建一个新的String对象,并返回创建的对象。
  • convertObjectToJsonBytes(Object object)方法将作为方法参数给出的对象转换为JSON文档,并将该文档的内容作为字节数组返回

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条目已添加到数据库

第二个单元测试确保将新的待办事项添加到数据库时,控制器能够正常工作。 我们可以按照以下步骤编写此测试:

  1. 使用我们的测试数据构建器创建一个新的TodoDTO对象。 在标题说明字段中设置“合法”值。
  2. 创建一个Todo对象,该对象在调用TodoService接口的add()方法时返回。
  3. 配置我们的模拟对象,使其在调用其add()方法并将TodoDTO对象作为参数时返回创建的Todo对象。
  4. 执行POST请求以发送url'/ api / todo'。 将请求的内容类型设置为“ application / json”。 将请求的字符集设置为“ UTF-8”。 将创建的TodoDTO对象转换为JSON字节并将其发送到请求的正文中。
  5. 验证是否返回HTTP状态代码200。
  6. 验证响应的内容类型为“ application / json”,并且其内容类型为“ UTF-8”。
  7. 使用JsonPath表达式$ .id获取返回的待办事项条目的ID ,并验证ID为1。
  8. 使用JsonPath表达式$ .description获取返回的待办事项的描述 ,并验证该描述是否为“描述”。
  9. 通过使用JsonPath表达式$ .title获取返回的待办事项条目的标题 ,并确保标题为“ title”。
  10. 创建一个ArgumentCaptor对象,该对象可以捕获TodoDTO对象。
  11. 验证TodoService接口的add()方法仅被调用一次,并捕获作为参数给定的对象。
  12. 验证测试期间未调用模拟对象的其他方法。
  13. 验证捕获的TodoDTO对象的ID为null。
  14. 验证所捕获的TodoDTO对象的描述是“说明”。
  15. 验证捕获TodoDTO对象的标题是“冠军”。

我们的单元测试的源代码如下所示:

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"));}
}

摘要

现在,我们已经使用Spring MVC Test框架为REST API编写了单元测试。 本教程教会了我们四件事:

  • 我们学习了为控制器方法编写单元测试,这些方法从数据库中读取信息。
  • 我们学习了为控制器方法编写单元测试,这些方法将信息添加到数据库中。
  • 我们了解了如何将DTO对象转换为JSON字节并将转换结果发送到请求正文中。
  • 我们学习了如何使用JsonPath表达式编写JSON文档的断言。

与往常一样,此博客文章的示例应用程序可在Github上获得 。 我建议您检查一下,因为它有很多单元测试,而本博客文章中未涉及。

参考: Spring MVC控制器的单元测试: Petri Kainulainen博客上的JCG合作伙伴 Petri Kainulainen提供的REST API 。

翻译自: https://www.javacodegeeks.com/2013/08/unit-testing-of-spring-mvc-controllers-rest-api.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/367070.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flask框架基本使用(2)(响应与重定向)

#转载请留言联系 flask 框架基本使用(1)&#xff1a;https://www.cnblogs.com/chichung/p/9756935.html 1. flask 自定义返回状态码与响应头 from flask import Flask,make_responseappFlask(__name__)app.route("/") def index():#666是指定的状态码&#xff0c;nam…

web.xml中webAppRootKey

<context-param> <param-name>webAppRootKey</param-name> <param-value>myproject.root</param-value> </context-param> 1、<param-value>里的值可以随便写配置。如果不配置默认值是"webapp.root"。 2、<param-value…

mpvue小程序以及微信直播踩坑总结

前段时间刚写完一个mpvue的小程序&#xff0c;现在得闲必须赶紧记录和总结一下&#xff0c;不然很多东西又要忘了 我是比较熟悉vue的语法&#xff0c;但是也犹豫过是用原生还是用mpvue&#xff0c;因为那时候原生小程序已经相当成熟而mpvue才刚出来&#xff08;踩坑和填坑的前…

适用于高级Java开发人员的十大书籍

Java是当今最流行的编程语言之一。 有很多适合初学者的书籍。 但是对于那些使用Java进行过一段时间编程的人来说&#xff0c;其中有些人看起来有些简单和多余。 初学者的书没有带来新鲜有趣的想法。 但是&#xff0c;高级Java书籍并不总是一应俱全&#xff0c;部分原因是因为它…

python apscheduler执行_python apscheduler 每两小时执行一次

from datetime import datetimefrom apscheduler.schedulers.blocking import BlockingSchedulerdef job_function():print("Hello World")sched BlockingScheduler()每2小时触发sched.add_job(job_function, interval, hours2)sched.start()设定执行区间sched.add_…

Ajax知识总结

一 AJAX Asynchronous JavaScript and XML&#xff08;异步的 JavaScript 和 XML&#xff09;。 AJAX 不是新的编程语言&#xff0c;而是一种使用现有标准的新方法。AJAX 最大的优点是在不重新加载整个页面的情况下&#xff0c;可以与服务器交换数据并更新部分网页内容。AJAX …

Leetcode 456. 132 Pattern

题目的意思就是给你一个数组 里面一堆数&#xff0c;你是否能找到这样三个数&#xff0c;第一个数比第三个数小&#xff0c;第二个数最大。比如 1, 3, 2 或者 4, 9, 5 这种&#xff0c;数字可以不连续在一起&#xff0c;但是要保证顺序不变&#xff0c;只要有这么一组数就可以…

「起点订阅页」Checkbox 美化引发的蝴蝶效应

本文作者&#xff1a;任家乐 原创声明&#xff1a;本文为阅文前端团队 YFE 成员出品&#xff0c;请尊重原创&#xff0c;转载请联系公众号 (id: yuewen_YFE) 获取授权&#xff0c;并注明作者、出处和链接。 性能风暴 「据说亚马逊雨林的一只蝴蝶偶尔扇动几下翅膀&#xff0c;可…

python polar函数_Python可视化很简单,可是你会吗?python绘制饼图、极线图和气泡图,让我来教教你吧,一文教会!!!...

matplotlib库作为Python数据化可视化的最经典和最常用库&#xff0c;掌握了它就相当于学会了Python的数据化可视化&#xff0c;今天呢&#xff0c;咱们就一起来聊聊关于Python如何去绘制饼图、极线图和气泡图吧好啦&#xff0c;废话少说&#xff0c;咱们就开始吧&#xff01;用…

Java中的访问者设计模式–示例教程

访客模式是行为设计模式之一 。 当我们必须对一组相似类型的对象执行操作时&#xff0c;将使用访问者模式。 借助访问者模式&#xff0c;我们可以将操作逻辑从对象移动到另一个类。 例如&#xff0c;假设有一个购物车&#xff0c;我们可以在其中添加不同类型的项目&#xff08…

函数递归与二分法

1.什么是函数递归 函数的递归调用是函数嵌套调用的一种特殊形式&#xff0c; 特殊在调用一个函数的过程中又直接或者间接地调用了该函数本身 递归本质就是一个循环的过程&#xff0c; 但是递归必须满足两个原则&#xff1a; 1.每进入下一层递归&#xff0c;问题的规模必须有所减…

使用Oracle WebLogic创建部署计划

创建部署计划 部署计划是JSR-88部署标准的一部分&#xff0c;尽管在该标准中未明确说明。 部署计划是一个XML文档&#xff0c;用于定义自定义WebLogic Server部署环境。 此配置可用于覆盖在应用程序归档文件中定义的特定设置。 有许多原因可能导致您不希望修改应用程序存档的原…

记录一个前端架构的想法

前端&#xff0c;真的是让我哭笑不得的职业&#xff0c;从几年前作为打酱油的理想职业到现在的热门职业&#xff0c;无疑在这个过程中&#xff0c;门槛变高了&#xff0c;而且还是非常高。一大堆的框架和库&#xff0c;像什么vue啦、react啦、angular啦、webpack啦等等等等。让…

java文件解压文件_java 文件解压缩

直接上代码&#xff1a;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.I…

JAVA 遍历Map对象

方式一 这是最常见的并且在大多数情况下也是最可取的遍历方式。在键值都需要时使用。 Map<Integer, Integer> map new HashMap<Integer, Integer>(); for (Map.Entry<Integer, Integer> entry : map.entrySet()) { System.out.println("Key " …

sol - 0x60,61,62

[例题]走廊泼水节 设当前扫描到边x,y&#xff0c;长度为z&#xff0c;x所处的并查集为Sx&#xff0c;y所处的并查集为Sy&#xff1b; 对于任意u属于Sx&#xff0c;v属于Sy&#xff0c;我们可以知道u&#xff0c;v之间必连一条边 但是我们要在保证x,y之间的边属于唯一最小生成树…

将Spring Bean注入非托管对象

依赖注入带来的好处可能会上瘾。 使用注入配置应用程序结构比手动完成所有解析要容易得多。 当我们有一些在容器外部实例化的非托管类时&#xff0c;例如在Vaadin UI组件或JPA实体等其他框架中&#xff0c;它们是很难被退出的。 当我们使用域驱动设计时&#xff0c;后者尤其重要…

java 编程原理_Java网络编程 -- 网络编程基础原理

Hello&#xff0c;今天记录下 Java网络编程 --> 网络编程基础原理。一起学习&#xff0c;一起进步。继续沉淀&#xff0c;慢慢强大。希望这文章对您有帮助。若有写的不好的地方&#xff0c;欢迎评论给建议哈&#xff01;初写博客不久&#xff0c;我是杨展浩。这是我的第十五…

JavaScript基础之--- 深拷贝与浅拷贝

理解深拷贝和浅拷贝之前&#xff0c;先来看一下JavaScript的数据类型。 1、基本类型和引用类型 //案例1 var num1 1, num2 num1; console.log(num1) //1 console.log(num2) //1 num2 2; //修改num2 console.log(num1) //1 console.log(num2) //2 //案例2 var obj1 {x: 1, …

vue菜鸟从业记:完成项目最后一公里之真机测试和打包上线

最近我朋友王小闰他们公司的项目开发已经进入收尾阶段&#xff0c;前后端并行开发的差不多了&#xff0c;联调也调过了&#xff0c;上篇文章里也讲到了&#xff0c;所谓联调&#xff0c;就仿佛在说“我也不知道我的接口文档写的对不对&#xff0c;我们验证一下吧&#xff1f;我…