基于Spring MVC做单元测试

在Spring 3.2之前,测试时一般都是直接new控制器,注入依赖,然后判断返回值。但是我们无法连同Spring MVC的基础设施(如DispatcherServlet调度、类型转换、数据绑定、拦截器等)一起测试,另外也没有现成的方法测试如最终渲染的视图(@ResponseBody生成的JSON/XML、JSP、Velocity等)内容是否正确。从Spring 3.2开始这些事情都可以完成了。而且可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试。

添加Maven依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>

版本信息3.2.6.RELEASE

测试相关的依赖(junithamcrestmockitospring-test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>${hamcrest.core.version}/version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>0.9.1</version>
<scope>test</scope>
</dependency>

版本信息:4.121.32.2.22

Spring MVC测试说明

首先是Spring的几个Annotate

  • RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试;
  • WebAppConfiguration: 测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;value指定web应用的根;
  • ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;

然后是Mockito的Annotate

  • Mock: 如果该对象需要mock,则加上此Annotate;
  • InjectMocks: 使mock对象的使用类可以注入mock对象,在上面这个例子中,mock对象是UserService,使用了UserService的是UserController,所以在Controller加上该Annotate;

Setup方法

  • MockitoAnnotations.initMocks(this): 将打上Mockito标签的对象起作用,使得Mock的类被Mock,使用了Mock对象的类自动与Mock对象关联。
  • mockMvc: 这个对象是Controller单元测试的关键,它的初始化也是在setup方法里面。

Test Case

  • 首先mock了UserService的方法,让其返回一个成功的Result对象。
  • mockMvc.perform: 发起一个http请求。
  • post(url): 表示一个post请求,url对应的是Controller中被测方法的Rest url。
  • param(key, value): 表示一个request parameter,方法参数是key和value。
  • andDo(print()): 表示打印出request和response的详细信息,便于调试。
  • andExpect(status().isOk()): 表示期望返回的Response Status是200。
  • andExpect(content().string(is(expectstring)): 表示期望返回的Response Body内容是期望的字符串。

spring mvc测试框架提供了两种方式,独立安装和集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。

方案一:独立安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class UserControllerTest {
@InjectMocks
private UserController userController;
@Mock
private UserServiceImpl userService;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
}

1、首先自己创建相应的控制器,注入相应的依赖

2、通过MockMvcBuilders.standaloneSetup模拟一个Mvc测试环境,通过build得到一个MockMvc

1.1、配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.2.xsd">
<context:component-scan base-package="org.zsr.mvc" />
<mvc:annotation-driven>
<mvc:message-converters register-defaults="false">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
<property name="supportedMediaTypes">
<util:list>
<value>application/json</value>
</util:list>
</property>
<property name="writeAcceptCharset" value="false"></property>
</bean>
<bean
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
</beans>

1.2、测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package org.zsr.mvc.controller;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.zsr.mvc.domain.User;
import org.zsr.mvc.service.impl.UserServiceImpl;
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 com.alibaba.fastjson.JSON;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class UserControllerTest {
@InjectMocks
private UserController userController;
@Mock
private UserServiceImpl userService;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
@Test
public void testGet() throws Exception {
final int userId = 3;
Mockito.when(userService.getUser(userId)).thenAnswer(new Answer<User>() {
public User answer(InvocationOnMock invocation) throws Throwable {
User user = new User();
user.setId(userId);
user.setAge(20);
user.setFirstName("bob");
return user;
}
});
mockMvc.perform(get("/user/" + userId)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.age", is(20))).andExpect(jsonPath("$.data.firstName", is("bob")))
.andExpect(jsonPath("$.data.id", is(userId))).andExpect(jsonPath("$.success", is(true)));
}
@Test
public void testSave() throws Exception {
mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(new User())))
.andExpect(status().isOk()).andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.success", is(true)));
}
}

方案二:集成Web环境方式

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextConfiguration({ "classpath:test-context.xml" })
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

1、@WebAppConfiguration:测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;value指定web应用的根;

2、通过@Autowired WebApplicationContext wac:注入web环境的ApplicationContext容器;

3、然后通过MockMvcBuilders.webAppContextSetup(wac).build()创建一个MockMvc进行测试;

注意:集成Web环境方式是根据指定的xml配置文件(或者Java注解类)构造应用上下文ApplicationContext,如果配置文件中没有将依赖的服务(本案例是:userService)模拟出来,则依赖的服务是真实的;在使用mockMvc测试Controller中的Method时候(其中涉及userService接口调用),依赖的服务userService实际上会执行其中的逻辑(可能有复杂的计算或者RPC调用等)。这种情况不是我所预期的,实际的需求是:模拟依赖的服务userService调用接口,返回自定义的数据。总结一句话:Controller依赖的所有服务都是mock的。解决方法:springockito

2.1、配置文件:

1
2
3
4
5
6
<dependency>
<groupId>org.kubek2k</groupId>
<artifactId>springockito</artifactId>
<version>1.0.9</version>
<scope>test</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util"
xmlns:mockito="http://www.mockito.org/spring/mockito"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.mockito.org/spring/mockito
http://www.mockito.org/spring/mockito.xsd">
<!-- <context:component-scan base-package="org.royrusso.mvc" /> -->
<mvc:annotation-driven>
<mvc:message-converters register-defaults="false">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
<property name="supportedMediaTypes">
<util:list>
<value>application/json</value>
</util:list>
</property>
<property name="writeAcceptCharset" value="false"></property>
</bean>
<bean
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<mockito:mock id="userService"
class="org.zsr.mvc.service.impl.UserServiceImpl" />
<bean class="org.zsr.mvc.controller.UserController"></bean>
</beans>

2.2、测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package org.zsr.mvc.controller;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.zsr.mvc.domain.User;
import org.zsr.mvc.service.impl.UserServiceImpl;
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 com.alibaba.fastjson.JSON;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextConfiguration({ "classpath:test-context.xml" })
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
@Autowired
private UserServiceImpl userService;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void testGet() throws Exception {
final int userId = 3;
Mockito.when(userService.getUser(userId)).thenAnswer(new Answer<User>() {
public User answer(InvocationOnMock invocation) throws Throwable {
User user = new User();
user.setId(userId);
user.setAge(16);
user.setFirstName("bob");
return user;
}
});
mockMvc.perform(get("/user/" + userId)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.age", is(16))).andExpect(jsonPath("$.data.firstName", is("bob")))
.andExpect(jsonPath("$.data.id", is(userId))).andExpect(jsonPath("$.success", is(true)));
}
@Test
public void testSave() throws Exception {
mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(new User())))
.andExpect(status().isOk()).andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.success", is(true)));
}
}

其它代码

UserServiceImpl类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.zsr.mvc.service.impl;
import org.zsr.mvc.domain.User;
import org.zsr.mvc.service.IUserService;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements IUserService {
@Override
public User getUser(int id) {
User user = new User();
return user;
}
@Override
public void saveUser(User user) {
}
}

RestResponse类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.zsr.mvc.rest;
public class RestResponse<T> {
private boolean success;
private String message;
private T data;
public RestResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
}

UserController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.zsr.mvc.controller;
import org.zsr.mvc.domain.User;
import org.zsr.mvc.rest.RestResponse;
import org.zsr.mvc.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@ResponseBody
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public RestResponse<User> getUser(@PathVariable("id") int id) {
User user = userService.getUser(id);
return new RestResponse<User>(true, "", user);
}
@ResponseBody
@RequestMapping(method = RequestMethod.POST)
public RestResponse saveUser(@RequestBody User user) {
userService.saveUser(user);
return new RestResponse<String>(true, "", null);
}
}

参考:

基于Spring MVC做单元测试

Spring MVC测试框架详解

Unit Test Spring MVC Rest Service

基于Spring Test和Mockito进行单元测试

热评文章