Writing unit tests for Spring MVC controllers has traditionally been both simple and problematic.
Although it is pretty simple to write unit tests which invoke controller methods, the problem is that those unit tests are not comprehensive enough.
For example, we cannot test controller mappings, validation and exception handling just by invoking the tested controller method.
Spring MVC Test solved this problem by giving us the possibility to invoke controller methods through theDispatcherServlet.
This is the first part of my tutorial which describes the unit testing of Spring MVC controllers and it describes how we can configure our unit tests.
Let’s get started.
We can get the required dependencies by declaring the following testing dependencies in our pom.xmlfile:
The relevant part of our pom.xml file looks as follows:
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
>
|
Note: If you have to use Spring Framework 3.1, you can write unit tests for your controllers by using spring-test-mvc. This project was included in the spring-test module when Spring Framework 3.2 was released.
Let’s move on and take a quick look at our example application.
The example application of this tutorial provides CRUD operations for todo entries. In order to understand the configuration of our test class, we must have some knowledge about the tested controller class.
At this point, we need to know the answers to these questions:
We can get the answers to those questions by taking a look at the source code of the TodoControllerclass. The relevant part of the TodoController class looks as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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.
}
|
As we can see, our controller class has two dependencies: TodoService and MessageSource. Also, we can see that our controller class uses constructor injection.
At this point this is all there information we need. Next we will talk about our application context configuration.
Maintaining a separate application context configurations for our application and our tests is cumbersome. Also, It can lead into problems if we change something in the application context configuration of our application but forget to do the same change for our test context.
That is why the application context configuration of the example application has been divided in a such way that we can reuse parts of it in our tests.
Our application context configuration has been divided as follows:
Note: The example application has also a working application context configuration which uses XML configuration files. The XML configuration files which correspond with the Java configuration classes are:exampleApplicationContext.xml, exampleApplicationContext-web.xml and exampleApplicationContext-persistence.xml.
Let’s take a look at the application context configuration of our web layer and find out how we can configure our test context.
The application context configuration of the web layer has the following responsibilities:
Let’s move on and take a look at the Java configuration class and the XML configuration file.
If we use Java configuration, the source code of the WebAppContext class looks as follows:
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
|
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;
}
}
|
If we use XML configuration, the content of the exampleApplicationContext-web.xml file looks as follows:
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
>
|
The configuration of our test context has two responsibilities:
Let’s find out how we configure our test context by using Java configuration class and XML configuration file.
If we configure our test context by using Java configuration, the source code of the TestContext class looks as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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
);
}
}
|
If we configure our test context by using an XML configuration, the content of the testContext.xml file looks as follow:
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
>
|
We can configure our test class by using one of the following options:
Let’s move on and find out how we can configure our test class by using both configuration options.
We can configure our test class by following these steps:
The source code of our test class looks as follows:
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
|
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;
}
}
|
Using the standalone configuration has two problems:
We can configure our test class by following these steps:
The source code of our test class looks as follows:
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
|
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();
}
}
|
The configuration of our test class looks a lot cleaner than the configuration which uses standalone configuration. However, the “downside” is that our test uses the full Spring MVC infrastructure. This might be an overkill if our test class really uses only a few components.
We have now configured our unit test class by using both the standalone setup and theWebApplicationContext based setup. This blog post has taught us two things:
The next part of this tutorial describes how we can write unit tests for “normal” Spring MVC controllers.
P.S. The example application of this blog post is available at Github.