为 SpringMVC 编写测试用例通常被认为是既简单又复杂的。
虽然直接写调用Controller方法的测试用例不难,但问题在于这些测试用例不够全面。
例如,仅通过直接的方法调用我们测不到 Controller 的映射、校验和异常处理。
SpringMVC 提供给我们通过 DispathServlet 调用 Controller 方法的能力解决了这个问题。
这篇文章是本系列 SpringMVC 单元测试指南的第一部分,主要说明了如何对单元测试做相关配置。
现在开始吧。
我们可以通过在 pom.xml 中声明以下来获取相关依赖:
体现在 pom.xml 中的相关部分看起来应该是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</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>
注意: 如果你使用的是 Spring Framework 3.1,你可以使用 spring-test-mvc 写测试用例。这个模块从 Spring Framework 3.2 开始就默认包含了。
让我们继续看看我们的例子程序。
本指南中的样例程序为 Todo 实体类提供了 CRUD 的功能。为了帮助理解测试类的相关配置,我们必须对被测试类有点基本了解。
此时此刻, 我们需要知道这几个问题的答案:
我们可以看看 TodoController 类的源代码来查找问题的答案。相关的部分是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
@Controller
public class TodoController {
private final TodoService service;
private final MessageSource messageSource;
@Autowired
public TodoController(MessageSource messageSource, TodoService service) {
this.messageSource = messageSource;
this.service = service;
}
//Other methods are omitted.
}
正如我们看到的,这个测试类有两个依赖:TodoService 和 MessageSource。而且,我们可以知道这个测试类使用了构造方法注入。
现在我们已经知道了我们想知道的。下面我们讨论下程序执行环境的设置。
为我们的应用程序和测试程序维护不同的执行环境是很麻烦的。而且,如果我们修改了应用程序需要的执行环境而忘了对测试环境做对应修改就可能导致某些问题。
这也是为什么我们的样例应用程序执行环境配置要做以下拆分,只有这样我们才能在测试中重用它们。
应用程序执行环境拆分规则:
注意: 样例程序还有一个通过 XML 配置文件定义的工作环境。和上文的几个 JAVA 配置类相同功能的配置文件是:exampleApplicationContext.xml、exampleApplicationContext-web.xml 和 exampleApplicationContext-persistence.xml。
现在让我们看看 WEB 层的环境配置,并思考一下如何为测试环境作相应配置。
WEB 层配置项主要有以下功能:
现在继续,让我们看看对应的 Java 类配置和 XML 文件配置。
如果我们通过 Java 类配置,那么 WebAppContext 类的代码看起来是这样的:
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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import java.util.Properties;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {
"net.petrikainulainen.spring.testmvc.common.controller",
"net.petrikainulainen.spring.testmvc.todo.controller"
})
public class WebAppContext extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@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;
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
如果我们通过 XML 文件配置,exampleApplicationContext-web.xml 文件中的内容看起来是这样的:
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<mvc:annotation-driven/>
<mvc:resources mapping="/static/**" location="/static/"/>
<mvc:default-servlet-handler/>
<context:component-scan base-package="net.petrikainulainen.spring.testmvc.common.controller"/>
<context:component-scan base-package="net.petrikainulainen.spring.testmvc.todo.controller"/>
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException">error/404</prop>
<prop key="java.lang.Exception">error/error</prop>
<prop key="java.lang.RuntimeException">error/error</prop>
</props>
</property>
<property name="statusCodes">
<props>
<prop key="error/404">404</prop>
<prop key="error/error">500</prop>
</props>
</property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
</bean>
</beans>
测试环境配置项主要有以下功能:
现在让我们看看如何通过 Java 类和 XML 分别配置我们的测试环境。
如果我们通过 Java 类配置,那么 TestContext 类的代码看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.mockito.Mockito;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
@Configuration
public class TestContext {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
@Bean
public TodoService todoService() {
return Mockito.mock(TodoService.class);
}
}
如果我们通过 XML 文件配置,testContext.xml 文件中的内容看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n/messages"/>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
<bean id="todoService" name="todoService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="net.petrikainulainen.spring.testmvc.todo.service.TodoService"/>
</bean>
</beans>
我们可以通过下面这些方式来对我们的测试类作相关配置:
@Controller
注解) 并对 SpringMVC 做些基本设置。在配置简单和直接的情况下这种方式也是可行的。现在让我们看看如何将这两种配置方式付诸实践。
我们可以遵循以下几步:
@RunWith
注解注释类,并确保测试类会被通过 MockitoJUnitRunner 执行。@Mock
注解注释类。exceptionResolver()
方法。这个方法生成一个配置良好的 SimpleMappingExceptionResolver 对象并返回。messageSource()
方法。这个方法生成一个配置良好的 ResourceBundleMessageSource 对象并返回。validator()
方法。这个方法生成并返回一个 LocalValidatorFactoryBean 对象。viewResolver()
方法。这个方法生成一个配置良好的 InternalResourceViewResolver 对象并返回。setUp()
方法并使用 @Before
注释注解它。这个注解可以确保这个方法会在每个测试执行前被调用。这个方法通过调用 MockMvcBuilders 的 standaloneSetup()
方法生成一个 MockMvc 对象并对它做好对应基本配置。测试类的源代码如下:
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
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import java.util.Properties;
@RunWith(MockitoJUnitRunner.class)
public class StandaloneTodoControllerTest {
private MockMvc mockMvc;
@Mock
private TodoService todoServiceMock;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(messageSource(), todoServiceMock))
.setHandlerExceptionResolvers(exceptionResolver())
.setValidator(validator())
.setViewResolvers(viewResolver())
.build();
}
private HandlerExceptionResolver 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;
}
private MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
private LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
private ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
使用独立配置主要有以下两个问题:
基本步骤如下:
@RunWith
注解注释类,并确保测试类会被通过 MockitoJUnitRunner 执行。@ContextConfiguration
注解,并确保使用正确的配置类(或者XML配置文件)。如果我们用 Java 类做相关配置,那么就把类名设到 classes
属性上。类似,如果我们用 XML 做配置,那么就把配置文件设到 locations
属性上。@WebAppConfiguration
注释类。这个注解可以确保为我们的单元测试加载的执行环境是一个 WebApplicationContext。@Autowired
注解。@Autowired
注解。setUp()
方法并添加 @Befor
注解。它可以确保这个方法在每个测试执行前被调用。这个方法的主要职责有:在每个测试执行前重置冒烟对象并通过调用 MockMvcBuilders 类的 webAppContextSetup()
方法重新生成 MockMvc 对象。测试类源代码如下:
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
import org.junit.Before;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
//@ContextConfiguration(locations = {"classpath:testContext.xml", "classpath:exampleApplicationContext-web.xml"})
@WebAppConfiguration
public class WebApplicationContextTodoControllerTest {
private MockMvc mockMvc;
@Autowired
private TodoService todoServiceMock;
@Autowired
private WebApplicationContext webApplicationContext;
@Before
public void setUp() {
//We have to reset our mock between tests because the mock objects
//are managed by the Spring container. If we would not reset them,
//stubbing and verified behavior would "leak" from one test to another.
Mockito.reset(todoServiceMock);
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
}
这个测试类的配置信息比通过独立配置的代码简洁不少。但是,它的弊端就是为每个测试都使用了完全的 SpringMVC 基础配置。如果我们测试依赖确实较少的情况下确实会比较消耗性能。
我们已经通过独立配置和基于 WebApplicationContext 对我们的测试类做了相应配置。这篇文章也教了我们两件事情: