The first part of this tutorial described how we can configure our unit tests which use the Spring MVC Test framework.
教程第一部分讨论了如何使用Spring MVC测试框架配置我们的单元测试
Now it is time to get our hands dirty and learn how we can write unit tests for “normal” controllers.
现在是时候动手操作学习如何为"普通"controllers写单元测试了
The obvious next question is
下一个最明显的问题是
What is a normal controller?
什么是普通controller
Well, a normal controller (in the context of this blog post) is a controller which either renders a view or handles form submissions.
一个普通controller(在本blog贴出的上文中)就是一个不是渲染视图就是处理一个表单的controller
Let’s get started.
Note: I recommend that you read the first part of this tutorial before reading this blog post (If you have already read it, you are allowed to continue reading)
Getting The Required Dependencies with Maven
使用Maven获取必需的依赖
We can get the required testing dependencies by adding the following dependency declarations to the POM file of our example application:
加入如下依赖声明到POM文件获取测试依赖
- Jackson 2.2.1 (core and databind modules). We use Jackson to transform objects into url encoded String objects.
- Hamcrest 1.3. We use Hamcrest matchers when we are writing assertions for the responses.
- JUnit 4.11 (exclude the hamcrest-core dependency).
- Mockito 1.9.5
- Spring Test 3.2.3.RELEASE
The relevant part of our pom.xml file looks as follows:
pom.xml文件的相关部分如下
<groupId>com.fasterxml.jackson.core </groupId>
<artifactId>jackson-core </artifactId>
<version>2.2.1 </version>
<scope>test </scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core </groupId>
<artifactId>jackson-databind </artifactId>
<version>2.2.1 </version>
<scope>test </scope>
</dependency>
<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>
Let’s move on and find out how we can write unit tests for Spring MVC controllers by using the Spring MVC Test framework.
Writing Unit Tests for Controller Methods
为Controller方法写单元测试
Every unit test which we write to test the behavior of a controller method consists of these steps:
每个测试controller方法行为的单元测试由两步组成:
- We send a request to the tested controller method.
- We verify that we received the expected response.
- 发送一个请求到待测试的方法
- 验证是否收到正确的响应
The Spring MVC Test framework has a few “core” classes which we can use for implementing these steps in our tests. These classes are described in the following:
Spring MVC 测试框架有几个"核心"类, 我们可以在测试中实现他们. 这些类的描述如下:
- We can build our requests by using the static methods of the MockMvcRequestBuilders class. Or to be more specific, we can create request builders which are then passed as a method parameter to the method which executes the actual request.
- 我们可以使用MockMvcRequestBuilders的静态方法构建请求. 更特殊的, 我们可以创建request builders,然后将他们作为参数传给执行真正请求的方法
- The MockMvc class is the main entry point of our tests. We can execute requests by calling itsperform(RequestBuilder requestBuilder) method.
- MockMvc类测试类的主要入口.我们可以通过调用它的 perform(RequestBuilder requestBuilder)方法来执行请求
- We can write assertions for the received response by using the static methods of theMockMvcResultMathers class.
- 我们可以使用MockMvcResultMathers为收到的响应写一个断言
Next we will take a look at some examples which demonstrates how we can use these classes in our unit tests. We will write unit tests for the following controller methods:
下面我们看看一些例子, 他师范了我们如何在单元测试中使用这些类的.我们会为下列controller方法写单元测试
- The first controller method renders a page which shows a list of todo entries.
- 第一个controller方法渲染了一个显示一个todo条目列表的页面
- The second controller method renders a page which shows the information of a single todo entry.
- 第二个controller方法渲染了一个显示单个todo条目的页面
- The third controller method handles form submissions of the form which is used to add new todo entries to the database.
- 第三个controller方法处理了用来添加新的todo条目到数据库的表单提交.
Rendering The Todo Entry List Page
渲染Todo条目列表页面
Let’s start by taking a look at the implementation of the controller method which is used to render the todo entry list page.
让我们从实现渲染todo条目列表页面的controller方法开始
Excpected Behavior
期待行为
The implementation of the controller method which is used to show the information of all todo entries has the following steps:
用来显示所有todo条目信息的controller方法实现如下
- It processes GET requests send to url ‘/’.
- It gets the todo entries by calling the findAll() method of the TodoService interface. This method returns a list of Todo objects.
- It adds the received list to the model.
- It returns the name of the rendered view.
- 他处理一个发给url '/'的GET请求
- 他通过调用TodoService接口的findAll()方法来获取所有todo条目.这个方法返回一个Todo对象的list
- 他添加一个接收列表到model
- 他返回渲染视图的名字
The relevant part of the TodoController class looks as follows:
TodoController相关部分如下:
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller public class TodoController { private final TodoService service; @RequestMapping(value = "/", method = RequestMethod.GET) public String findAll(Model model) { List<Todo> models = service.findAll(); model.addAttribute("todos", models); return "todo/list"; } }
We are now ready to write an unit test for this method. Let’s see how we can do it.
我们现在已经准备好为这个方法写单元测试了.看看怎么做
Test: Todo Entries Are Found
测试: Todo Entries找到了
We can write an unit test for this controller method by following steps:
我们可以通过如下步骤为controller方法写一个单元测试
- Create the test data which is returned when our service method is called. We use a concept called test data builder when we are creating the test data for our test.
- Configure the used mock object to return the created test data when its findAll() method is called.
- Execute a GET request to url ‘/’.
- Ensure that the HTTP status code 200 is returned.
- Ensure that the name of the returned view is ‘todo/list’.
- Ensure that the request is forwarded to url ‘/WEB-INF/jsp/todo/list.jsp’.
- Ensure that model attribute called todos has two items in it.
- Ensure that the model attribute called todos contains the correct items.
- Verify that the findAll() method of our mock object was called only once.
- Ensure that other methods of the mock object were not called during the test.
- 创建一个当service方法被调用的时候返回数据的测试.我们在为我们的测试创造测试数据的时候会使用一个叫做测试数据建造者(test data builder)的概念
- 配置mock对象在findAll()方法调用的时候返回测试数据
- 执行一个到'/'的GET请求
- 保证返回200 HTTP状态吗
- 保证返回的视图名称是'todo/list'
- 保证请求被转发到'/WEB-INF/jsp/todo/list.jsp'
- 保证叫todos的model参数包含两个对象在里面
- 保证叫todos的model的参数包含正确的对象
- 验证mock对象的findAll()方法只被调用了一次
- 保证mock对象的其他方法在测试中没被调用
The source code of our unit test looks as follows:
源代码如下:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; 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 org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import java.util.Arrays; import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestContext.class, WebAppContext.class}) @WebAppConfiguration public class TodoControllerTest { private MockMvc mockMvc; @Autowired private TodoService todoServiceMock; //Add WebApplicationContext field here //The setUp() method is omitted. @Test public void findAll_ShouldAddTodoEntriesToModelAndRenderTodoListView() 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("/")) .andExpect(status().isOk()) .andExpect(view().name("todo/list")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/list.jsp")) .andExpect(model().attribute("todos", hasSize(2))) .andExpect(model().attribute("todos", hasItem( allOf( hasProperty("id", is(1L)), hasProperty("description", is("Lorem ipsum")), hasProperty("title", is("Foo")) ) ))) .andExpect(model().attribute("todos", hasItem( allOf( hasProperty("id", is(2L)), hasProperty("description", is("Lorem ipsum")), hasProperty("title", is("Bar")) ) ))); verify(todoServiceMock, times(1)).findAll(); verifyNoMoreInteractions(todoServiceMock); } }
Rendering The View Todo Entry Page
渲染Todo条目视图
Before we can write the actual unit tests for our controller method, we have to take a closer look at the implementation of that method.
在我们为我们controller方法写出真正的单元测试之前,我们必须更仔细的看看方法的实现
Let’s move on and find out how our controller is implemented.
让我们继续看看我们的controller是怎么实现的
Expected Behavior
期待的行为
The controller method which is used to show the information of a single todo entry is implemented by following these steps:
controller用来显示单个todo条目的方法如下实现
- It processes GET requests send to url ‘/todo/{id}’. The {id} is a path variable which contains the id of the requested todo entry.
- It obtains the requested todo entry by calling the findById() method of the TodoServiceinterface and passes the id of the requested todo entry as a method parameter. This method returns the found todo entry. If no todo entry is found, this method throws aTodoNotFoundException.
- It adds the found todo entry to the model.
- It returns the name of the rendered view.
- 他去除发送到'/todo/{id}'的GET请求.{id}是一个代表todo条目的id的路径变量
- 他通过调用TodoService接口的findById()来获得请求的todo条目并把它的id作为一个方法参数.这个方法返回找到的todo条目.如果没有todo条目被找到,这个方法将抛出 TodoNotFoundException
- 他添加找到的todo条目到model
- 他返回渲染的视图
The source code of our controller method looks as follows:
源代码如下
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @Controller public class TodoController { private final TodoService service; @RequestMapping(value = "/todo/{id}", method = RequestMethod.GET) public String findById(@PathVariable("id") Long id, Model model) throws TodoNotFoundException { Todo found = service.findById(id); model.addAttribute("todo", found); return "todo/view"; } }
Our next question is:
下一个问题是
What happens when a TodoNotFoundException is thrown?
抛出一个TodoNotFoundException会发生什么
In the previous part of this tutorial, we created an exception resolver bean which is used to handle exceptions thrown by our controller classes. The configuration of this bean looks as follows:
在这个教程的前面, 我们创建了一个exception resolver bean, 它用来处理被controller抛出的异常.这个bean的配置如下:
@Bean public SimpleMappingExceptionResolver exceptionResolver() { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties exceptionMappings = new Properties(); exceptionMappings.put("net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", "error/404"); exceptionMappings.put("java.lang.Exception", "error/error"); exceptionMappings.put("java.lang.RuntimeException", "error/error"); exceptionResolver.setExceptionMappings(exceptionMappings); Properties statusCodes = new Properties(); statusCodes.put("error/404", "404"); statusCodes.put("error/error", "500"); exceptionResolver.setStatusCodes(statusCodes); return exceptionResolver; }
As we can see, if a TodoNotFoundException is thrown, our application renders the ‘error/404′ view and returns the HTTP status code 404.
如我们所见,如果一个TodoNotFoundException被抛出, 我们的程序将会渲染"error/404"视图,并返回404HTTP状态码
It is clear that we have to write two tests for this controller method:
显然我们必须为这个controller方法写两个单元测试
- We have to write a test which ensures that our application is working correctly when the todo entry is not found.
- We have to write a test which verifies that our application is working correctly when the todo entry is found.
- 我们要写一个保证我们的程序当todo条目没找到时正确工作的测试
- 我们要写一个验证我们的程序当可以找到todo条目时正确工作的测试
Let’s see how we can write these tests.
让我们看看怎么写这些测试
Test 1: Todo Entry Is Not Found
Test 1: Todo 条目没找到
First, we must ensure that our application is working property when the requested todo entry is not found. We can write the test which ensures this by following these steps:
首先, 我们必须保证我们的程序在todo条目没找到时正常工作.我们可以按如下写这个测试:
- Configure the mock object to throw a TodoNotFoundException when its findById() method is called and the id of the requested todo entry is 1L.
- Execute a GET request to url ‘/todo/1′.
- Verify that the HTTP status code 404 is returned.
- Ensure that the name of the returned view is ‘error/404′.
- Ensure that the request is forwarded to url ‘/WEB-INF/jsp/error/404.jsp’.
- Verify that the findById() method of the TodoService interface is called only once with the correct method parameter (1L).
- Verify that no other methods of the mock object were called during this test.
- 配置mock对象在调用findById()方法的时候抛出一个TodoNotFoundException并确定请求的todo条目id是1L
- 执行'/todo/1'的GET请求
- 验证HTTP状态码是404
- 保证返回的视图名称是'error/404'
- 保证请求被转发到url '/WEB-INF/jsp/error/404.jsp'
- 验证TodoService接口的findById()方法仅仅被使用了正确的参数(1L)调用了一次
- 验证mock对象的其他方法在测试期间没有被调用
The source code of our unit test looks as follows:
源代码如下
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; 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 org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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; @Autowired private TodoService todoServiceMock; //Add WebApplicationContext field here //The setUp() method is omitted. @Test public void findById_TodoEntryNotFound_ShouldRender404View() throws Exception { when(todoServiceMock.findById(1L)).thenThrow(new TodoNotFoundException("")); mockMvc.perform(get("/todo/{id}", 1L)) .andExpect(status().isNotFound()) .andExpect(view().name("error/404")) .andExpect(forwardedUrl("/WEB-INF/jsp/error/404.jsp")); verify(todoServiceMock, times(1)).findById(1L); verifyZeroInteractions(todoServiceMock); } }
Test 2: Todo Entry Is Found
Test 2: Todo条目被找到
Second, we must write a test which ensures that our controller is working properly when a todo entry is found. We can do this by following these steps:
第二, 我们必须写一个保证controller在条目找到的时候正确工作的测试.可以如下做:
- Create the Todo object which is returned when our service method is called. Again, we create the returned Todo object by using our test data builder.
- Configure our mock object to return the created Todo object when its findById() method is called by using a method parameter 1L.
- Execute a GET request to url ‘/todo/1′.
- Verify that the HTTP status code 200 is returned.
- Ensure that the name of the returned view is ‘todo/view’.
- Ensure that the request is forwarded to url ‘/WEB-INF/jsp/todo/view.jsp’.
- Verify that the id of the model object called todo is 1L.
- Verify that the description of the model object called todo is ‘Lorem ipsum’.
- Verify that the title of the model object called todo is ‘Foo’.
- Ensure that the findById() method of our mock object is called only once with the correct method parameter (1L).
- Ensure that the other methods of the mock object were not called during our test.
- 创建一个被service方法返回的Todo对象.我们将再次使用测试数据创建器来创建这个返回的Todo对象
- 配置我们的mock对象在用参数1L调用它的findById()方法时返回放回Todo对象
- 执行一个到'/todo/1'的GET请求
- 验证返回HTTP状态码是200
- 保证返回的视图名称是'todo/view'
- 保证请求转发到url '/WEB-INF/jsp/todo/view.jsp'
- 验证model中叫做todo的对象id是1L
- 验证model中叫todo的对象的description是"Lorem ipsum"
- 验证model中叫做todo的对象的title是'Foo'
- 保证mock对象的findById()方法仅仅被调用了一次
- 保证mock对象中的其他方法没有被调用过
The source code of our unit test looks as follows:
源代码如下
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; 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 org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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; @Autowired private TodoService todoServiceMock; //Add WebApplicationContext field here //The setUp() method is omitted. @Test public void findById_TodoEntryFound_ShouldAddTodoEntryToModelAndRenderViewTodoEntryView() throws Exception { Todo found = new TodoBuilder() .id(1L) .description("Lorem ipsum") .title("Foo") .build(); when(todoServiceMock.findById(1L)).thenReturn(found); mockMvc.perform(get("/todo/{id}", 1L)) .andExpect(status().isOk()) .andExpect(view().name("todo/view")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/view.jsp")) .andExpect(model().attribute("todo", hasProperty("id", is(1L)))) .andExpect(model().attribute("todo", hasProperty("description", is("Lorem ipsum")))) .andExpect(model().attribute("todo", hasProperty("title", is("Foo")))); verify(todoServiceMock, times(1)).findById(1L); verifyNoMoreInteractions(todoServiceMock); } }
Handling The Form Submission of The Add Todo Entry Form
处理添加Todo条目表单的提交
Again, we will first take a look at the expected behavior of our controller method before we will write the unit tests for it.
我们首先会看看我们的controller方法期待的表现
Expected Behavior
The controller method which handles the form submissions of the add todo entry form is implemented by following these steps:
controller方法处理表单提交的实现步骤如下:
- It processes POST requests send to url ‘/todo/add’.
- It checks that the BindingResult object given as a method parameter doesn’t have any errors. If errors are found, it returns the name of the form view.
- It adds a new Todo entry by calling the add() method of the TodoService interface and passes the form object as a method parameter. This method creates a new todo entry and returns it.
- It creates the feedback message about the added todo entry and adds the message to theRedirectAttributes object given as a method parameter.
- It adds the id of the added todo entry to the RedirectAttributes object.
- It returns the name of a redirect view which redirects the request to the view todo entry page.
- 他处理一个发送到'/todo/add'的POST请求
- 他检查BindingResult对象不含任何错误.如果发现了错误,他会返回表单视图的名称
- 他通过调用TodoService的add()方法添加一个新的Todo条目并把表单对象作为方法参数传递.这个方法创建一个新的todo条目并返回
- 他创建一个关于添加todo条目的反馈信息并将其作为方法参数添加这个消息到RedirectAttributes对象
- 他添加这个todo条目的id到RedirectAttributes对象
- 他返回重定向请求到查看todo条目页面的视图
The relevant part of the TodoController class looks as follows:
TodoController相关代码如下:
import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.validation.Valid; import java.util.Locale; @Controller @SessionAttributes("todo") public class TodoController { private final TodoService service; private final MessageSource messageSource; @RequestMapping(value = "/todo/add", method = RequestMethod.POST) public String add(@Valid @ModelAttribute("todo") TodoDTO dto, BindingResult result, RedirectAttributes attributes) { if (result.hasErrors()) { return "todo/add"; } Todo added = service.add(dto); addFeedbackMessage(attributes, "feedback.message.todo.added", added.getTitle()); attributes.addAttribute("id", added.getId()); return createRedirectViewPath("todo/view"); } private void addFeedbackMessage(RedirectAttributes attributes, String messageCode, Object... messageParameters) { String localizedFeedbackMessage = getMessage(messageCode, messageParameters); attributes.addFlashAttribute("feedbackMessage", localizedFeedbackMessage); } private String getMessage(String messageCode, Object... messageParameters) { Locale current = LocaleContextHolder.getLocale(); return messageSource.getMessage(messageCode, messageParameters, current); } private String createRedirectViewPath(String requestMapping) { StringBuilder redirectViewPath = new StringBuilder(); redirectViewPath.append("redirect:"); redirectViewPath.append(requestMapping); return redirectViewPath.toString(); } }
As we can see, the controller method uses a TodoDTO object as a form object. The TodoDTOclass is a simple DTO class which source code looks as follows:
如我们所见,controller方法使用一个TodoDTO对象作为一个表单对象.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. }
The TodoDTO class declares some validation constraints which are described in following:
TodoDTO类声明了如下验证约束
- The title of a todo entry cannot be empty.
- title不能为空
- The maximum length of the description is 500 characters.
- description最长500字符
- The maximum length of the title is 100 characters.
- title最长100字符
If we think about the tests which we should write for this controller method, it is clear that we must ensure that
如果考虑为这个controller 写测试 , 很显然必须保证
- The controller method is working property when the validation fails.
- The controller method is working property when a todo entry is added to the database.
- controller方法在验证失败时能正常工作
- controller方法当todo条目添加到数据库时能正常工作
Let’s find out how we can write these tests.
Test 1: Validation Fails
Test 1: 验证失败
First, we have to write a test which ensures that our controller method is working properly when the validation fails. We can write this test by following these steps:
首先,我们必须写一个保证当验证失败时controller方法正常工作的测试.我们可以按如下步骤写
- Create a title which has 101 characters.
- Create a description which has 501 characters.
- Create a new TodoDTO object by using our test data builder. Set the title and the descriptionof the object.
- Execute a POST request to url ‘/todo/add’. Set the content type of the request to ‘application/x-www-form-urlencoded’. Ensure that the content of our form object is send in the body of the request. Set the form object into session.
- Verify that the HTTP status code 200 is returned.
- Verify that the name of the returned view is ‘todo/add’.
- Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/add.jsp’.
- Verify that our model attribute has field errors in the title and description fields.
- Ensure that the id of our model attribute is null.
- Ensure that the description of our model attribute is correct.
- Ensure that the title of our model attribute is correct.
- Ensure that the methods of our mock object were not called during the test.
- 创建一个有101个字符的title
- 创建一个有501个字符的description
- 使用测试数据创建器创建一个新的TodoDTO.把上述title和description设置到对象中
- 执行一个到'/todo/add'的请求.设置请求的content type为'application/x-www-form-urlencoded'. 保证表单对象在请求体中被发送过来.设置表单对象到session中.
- 验证返回HTTP状态码200
- 验证返回的视图名称叫'todo/add'
- 验证请求转发url是'/WEB-INF/jsp/todo/add.jsp'
- 验证我们的model参数中包含title和description的字段错误信息
- 保证model的id参数是null
- 保证model的description参数是正确的
- 保证model的title参数是正确的
- 保证mock对象的方法在测试期间没被调用
The source code of our unit test looks as follows:
代码如下
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; 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 org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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; @Autowired private TodoService todoServiceMock; //Add WebApplicationContext field here //The setUp() method is omitted. @Test public void add_DescriptionAndTitleAreTooLong_ShouldRenderFormViewAndReturnValidationErrorsForTitleAndDescription() throws Exception { String title = TestUtil.createStringWithLength(101); String description = TestUtil.createStringWithLength(501); TodoDTO formObject = new TodoDTOBuilder() .description(description) .title(title) .build(); mockMvc.perform(post("/todo/add") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(TestUtil.convertObjectToFormUrlEncodedBytes(formObject)) .sessionAttr("todo", formObject) ) .andExpect(status().isOk()) .andExpect(view().name("todo/add")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp")) .andExpect(model().attributeHasFieldErrors("todo", "title")) .andExpect(model().attributeHasFieldErrors("todo", "description")) .andExpect(model().attribute("todo", hasProperty("id", nullValue()))) .andExpect(model().attribute("todo", hasProperty("description", is(description)))) .andExpect(model().attribute("todo", hasProperty("title", is(title)))); verifyZeroInteractions(todoServiceMock); } }
Our test case calls some static methods of the TestUtil class. These methods are described in the following:
我们的测试用例调用TestUtil的一些静态方法.这些方法描述如下
- The createStringWithLength(int length) method creates a new String object with the given length and returns the created object.
- createStringWithLength(int length)方法创建一个给定长度的字符串并返回创建的对象.
- The convertObjectToFormUrlEncodedBytes(Object object) method converts the object into form url encoded String object and returns the content of that String object as a byte array.
- convertObjectToFormUrlEncodedBytes(Object object)方法将对象转换为表单url编码字符串对象并将字符串对象作为一个字节数组返回.
The source code of the TestUtil class looks as follows:
源代码如下
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Iterator; import java.util.Map; import java.util.Set; public class TestUtil { public static byte[] convertObjectToFormUrlEncodedBytes(Object object) { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); Map<String, Object> propertyValues = mapper.convertValue(object, Map.class); Set<String> propertyNames = propertyValues.keySet(); Iterator<String> nameIter = propertyNames.iterator(); StringBuilder formUrlEncoded = new StringBuilder(); for (int index=0; index < propertyNames.size(); index++) { String currentKey = nameIter.next(); Object currentValue = propertyValues.get(currentKey); formUrlEncoded.append(currentKey); formUrlEncoded.append("="); formUrlEncoded.append(currentValue); if (nameIter.hasNext()) { formUrlEncoded.append("&"); } } return formUrlEncoded.toString().getBytes(); } public static String createStringWithLength(int length) { StringBuilder builder = new StringBuilder(); for (int index = 0; index < length; index++) { builder.append("a"); } return builder.toString(); } }
Test 2: Todo Entry Is Added to The Database
Test 2: Todo条目被加入到数据库
Second, we have to write a test which ensures that our controller is working properly when a new todo entry is added to the database. We can write this test by following these steps:
第二, 我们必须写一个保证在一个新的todo条目被加入数据库时controller正常工作的测试.我们可以如下写测试
- Create a form object by using the test data builder class. Set “legal” values to the title anddescription fields of the created object.
- Create a Todo object which is returned when the add() method of the TodoService interface is called.
- Configure our mock object to return the created Todo object when its add() method is called and the created form object is given as a method parameter.
- Execute a POST request to url ‘/todo/add’. Set the content type of the request to ‘application/x-www-form-urlencoded’. Ensure that the content of our form object is send in the body of the request. Set the form object into session.
- Verify that the HTTP status code 302 is returned.
- Ensure that the name of the returned view is ‘redirect:todo/{id}’.
- Ensure that the request is redirected to url ‘/todo/1′.
- Verify that the model attribute called id is ’1′.
- Verify that the feedback message is set.
- Verify that the add() method of our mock object is called only once and that the form object was given as a method parameter.
- Verify that no other methods of the mock object were called during our test.
- 使用测试数据创建器类创建一个表单对象.并将合法数据设置到创建的对象中
- 创建一个Todo对象, 让他在调用TodoService接口的add()方法时返回
- 设置mock对象,用来在调用add()方法的时候返回创建的Todo对象, 并将创建的表单对象作为一个方法参数.
- 执行一个到'/todo/add'的post请求.设置请求的content type为'application/x-www-form-urlencoded'. 保证表单对象从请求体重发送过来.将表单对象设置到请求体中
- 验证是否返回HTTP状态码302
- 保证返回的视图名称是'redirect:todo/{id}'
- 保证请求被重定向到'/todo/1'
- 验证model参数 id 的值为1
- 验证反馈信息被设置
- 验证mock对象的add()方法只被调用了一次,并将表单对象作为方法参数
- 验证mock对象的其他方法没有被调用过
The source code of our unit test looks as follows:
代码如下:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; 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 org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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; @Autowired private TodoService todoServiceMock; //Add WebApplicationContext field here //The setUp() method is omitted. @Test public void add_NewTodoEntry_ShouldAddTodoEntryAndRenderViewTodoEntryView() throws Exception { TodoDTO formObject = new TodoDTOBuilder() .description("description") .title("title") .build(); Todo added = new TodoBuilder() .id(1L) .description(formObject.getDescription()) .title(formObject.getTitle()) .build(); when(todoServiceMock.add(formObject)).thenReturn(added); mockMvc.perform(post("/todo/add") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(TestUtil.convertObjectToFormUrlEncodedBytes(formObject)) .sessionAttr("todo", formObject) ) .andExpect(status().isMovedTemporarily()) .andExpect(view().name("redirect:todo/{id}")) .andExpect(redirectedUrl("/todo/1")) .andExpect(model().attribute("id", is("1"))) .andExpect(flash().attribute("feedbackMessage", is("Todo entry: title was added."))); verify(todoServiceMock, times(1)).add(formObject); verifyNoMoreInteractions(todoServiceMock); } }
Summary
We have now written some unit tests for “normal” controller methods by using the Spring MVC Test framework. This tutorial has taught has four things:
我们使用Spring MVC 测试框架给普通的controller方法写了一些单元测试, 这篇教程教会了四件事情
- We learned to create requests which are processed by the tested controller methods.
- 我们学会了创建被测试controller方法处理的请求
- We learned to write assertions for the responses returned by the tested controller methods.
- 我们学会了为测试controller方法响应写断言
- We learned how we can write unit tests for controller methods which renders a view.
- 我们学会了如何为渲染一个视图的controller方法写单元测试
- We learned to write unit tests for controller methods which handles form submissions.
- 我们学会了如何为处理表单提交请求的controller方法写单元测试
The next part of this tutorial describes how we can write unit tests for a REST API.
这个教程的下一部分我们将学习如何为一个REST API写单元测试
P.S. The example application of this blog post is available at Github. I recommend that you check it out because it has some unit tests which were not covered in this blog post.