Testing Web Apps with Spring
Framework
October 18, 2014
JavaDay’14, Kyiv
Dmytro Chyzhykov
3
Slides and Code Examples
- Build web applications with Spring Web MVC
- Familiar with mock objects unit testing (JUnit and Mockito)
4
Assumptions on Audience
Spring Controllers Testing

- Example Domain Model

- Subject Under Test

- Pure Unit Tests

Spring MVC Test Framework

- Standalone Server-Side Integration Tests

- Web Application Context Server-Side Integration Tests
Further materials
Q&A
5
Agenda
Example Domain Model
6
7
Yandex.TV Service
8
Domain
public class Channel {
private Integer id;
private String title;
!
// constructors, getters/setters
}
9
Domain
public class Channel {
private Integer id;
private String title;
!
// constructors, getters/setters
}
DAO
@Repository
@Transactional
public interface ChannelRepository {
!
Channel findOne(Integer id);
!
}
Subject Under Test
10
11
Subject Under Test
@Controller
@RequestMapping("/channels")
public class ChannelController {
}
12
Subject Under Test
@Controller
@RequestMapping("/channels")
public class ChannelController {
@Autowired ChannelRepository channelRepository;
}
13
Subject Under Test
@Controller
@RequestMapping("/channels")
public class ChannelController {
@Autowired ChannelRepository channelRepository;
!
@ResponseBody
@RequestMapping(value = "/{id}",
method = RequestMethod.GET)
public Channel getChannel(@PathVariable int id) {
// ...
}
}
14
Subject Under Test
@Controller
@RequestMapping("/channels")
public class ChannelController {
@Autowired ChannelRepository channelRepository;
!
@ResponseBody
@RequestMapping(value = "/{id}",
method = RequestMethod.GET)
public Channel getChannel(@PathVariable int id) {
Channel channel = channelRepository.findOne(id);
!
if (channel != null) {
return channel;
}
!
// ...
}
}
15
Subject Under Test
@Controller
@RequestMapping("/channels")
public class ChannelController {
@Autowired ChannelRepository channelRepository;
!
@ResponseBody
@RequestMapping(value = "/{id}",
method = RequestMethod.GET)
public Channel getChannel(@PathVariable int id) {
Channel channel = channelRepository.findOne(id);
!
if (channel != null) {
return channel;
}
!
throw new ChannelNotFoundException();
}
}
16
Exception
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ChannelNotFoundException
extends RuntimeException {
!
// constructors
!
}
ChannelController behaviour
- Positive test case

When we are looking for an existent channel by its id
- Negative test case

When we are looking for an absent channel by its id
17
What we are going to test
Pure Unit Testing
18
19http://www.tubelinescaffolding.co.uk/industrial-scaffolding.htm
Scaffolding
20
Unit Test Scaffolding
!
public class ChannelControllerTest {
}
21
Unit Test Scaffolding
!
public class ChannelControllerTest {
@Mock
private ChannelRepository channelRepository;
}
22
Unit Test Scaffolding
!
public class ChannelControllerTest {
@Mock
private ChannelRepository channelRepository;
!
@InjectMocks
private ChannelController channelController =
// optional new ChannelController();
}
23
Unit Test Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerTest {
@Mock
private ChannelRepository channelRepository;
!
@InjectMocks
private ChannelController channelController =
// optional new ChannelController();
}
24
Unit Test Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerTest {
@Mock
private ChannelRepository channelRepository;
!
@InjectMocks
private ChannelController channelController =
// optional new ChannelController();
!
!
@Mock
private Channel channel; // dummy
// test cases go here
}
25
Positive Test Case
@Test
public void itShouldFindChannel() {
when(channelRepository.findOne(1))
.thenReturn(channel);
}
26
Positive Test Case
@Test
public void itShouldFindChannel() {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
assertThat(
channelController.getChannel(1), is(channel)
);
}
27
Negative Test Case
@Test
public void itShouldNotFoundChannel() {
// optional
when(channelRepository.findOne(-1))
.thenReturn(null);
}
28
Negative Test Case
@Test(expected = ChannelNotFoundException.class)
public void itShouldNotFoundChannel() {
// optional
when(channelRepository.findOne(-1))
.thenReturn(null);
!
channelController.getChannel(-1);
}
- Easy to write

- Incredibly fast (a few milliseconds per test case)
29
Pros
- Can use Spring mocks from org.springframework.mock.web

- MockHttpServletRequest/Response/Session

- MockMultipartFile

- MockFilterChain

…

- ModelAndViewAssert from org.springframework.test.web
to apply asserts on a resulting ModelAndView
30
Additional Capabilities on Demand
- A lot left untested

- Request mappings

- Type conversion

- Transactions

- Data binding

- Validation

- Filters

- …

- No Spring annotations used

- No DispatcherServlet interactions

- No actual Spring MVC configuration loaded
31
Caveats
32http://futurama.wikia.com/wiki/File:GoodNewsEveryone.jpg
Good news everyone!
Spring MVC Test Framework
since 3.2
33
<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-test</artifactId>

<version>4.1.1.RELEASE</version>

</dependency>
34
Dependency
35
Server-Side Integration Testing
without a Running Servlet Container
Web

Application

Context
DispatcherServlet
Tests
Controllers
MockMvc
- Response status, headers, content

- Spring MVC and Servlet specific results

- Model, flash, session, request attributes

- Mapped controller method

- Resolved exceptions

- Various options for asserting the response body

- JsonPath, XPath, XMLUnit
36
What can be tested
- Almost all template technologies are supported

- JSON, XML, Velocity, Freemarker, Thymeleaf, PDF etc. 

- Except JSP (because it relies on Servlet Container)

- you can assert only on the selected JSP view name

- No actual redirecting or forwarding

- you can assert the redirected or forwarded URL
37
Testing View Layer
Standalone setup for testing one individual controller at a time
without actual Spring MVC configuration loading
38
MockMvc “Standalone” Setup
private ChannelController controller = //...
!
private MockMvc mockMvc;
!
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.build();
}
39
MockMvc “Standalone” Setup
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setValidator(...)
.setViewResolvers(...)
.setHandlerExceptionResolvers(...)
.setMessageConverters(...)
.setLocaleResolver(...)
.addFilter(...)
//...
.build();
With actual Spring MVC configuration loading
40
MockMvc Web App Context Setup
// Scaffolding is omitted
!
@Autowired
private WebApplicationContext wac;
!
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.build();
}
41
Creating and Performing Requests
MockHttpServletRequestBuilder request =
MockMvcRequestBuilders.get("/channels/1")
.param("foo", "bar")
.header(...)
.cookie(...)
.locale(...)
.characterEncoding("UTF-8")
.accept("application/json")
.flashAttr("flash-key", "value")
// ...
.sessionAttr("key", “value");
!
!
mockMvc.perform(request);
42
Applying Asserts
mockMvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$.id").value(1))
// ...
.andExpect(xpath("...")...)
.andExpect(header()...)
.andExpect(cookies()...)
.andExpect(model()...)
.andExpect(view()...)
.andExpect(content()...)
.andExpect(flash()...)
.andExpect(redirectedUrl("..."));
43
Resolved Exception Assert
MvcResult mvcResult = mockMvc
.perform(...)
// ...
.andReturn();
!
assertThat(
mvcResult.getResolvedException(),
instanceOf(ChannelNotFoundException.class)
);
- MockMvcBuilders.* to set up MockMvc instances

- MockMvcRequestBuilders.* to create requests

- MockMvcResultMatchers.* for request result assertions on
44
Useful Static Imports
Standalone Server-Side
Integration Tests
46
Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerStandaloneIT {
@Mock
private ChannelRepository channelRepository;
@InjectMocks
private ChannelController channelController =
new ChannelController();
}
47
Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerStandaloneIT {
@Mock
private ChannelRepository channelRepository;
@InjectMocks
private ChannelController channelController =
new ChannelController();
private Channel channel = new Channel(1, "MTV");
}
48
Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerStandaloneIT {
@Mock
private ChannelRepository channelRepository;
@InjectMocks
private ChannelController channelController =
new ChannelController();
private Channel channel = new Channel(1, "MTV");
!
private MockMvc mockMvc;
}
49
Scaffolding
@RunWith(MockitoJUnitRunner.class)
public class ChannelControllerStandaloneIT {
@Mock
private ChannelRepository channelRepository;
@InjectMocks
private ChannelController channelController =
new ChannelController();
private Channel channel = new Channel(1, "MTV");
!
private MockMvc mockMvc;
!
@Before
public void setUp() {
mockMvc = standaloneSetup(channelController)
.build();
}
// test cases go here
}
50
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
}
51
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
mockMvc.perform(get("/channels/1")
.accept("application/json"))
}
52
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
mockMvc.perform(get("/channels/1")
.accept("application/json"))
.andExpect(status().isOk())
}
53
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
mockMvc.perform(get("/channels/1")
.accept("application/json"))
.andExpect(status().isOk())
.andExpect(content()
.contentType("application/json;charset=UTF-8"))
}
54
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
mockMvc.perform(get("/channels/1")
.accept("application/json"))
.andExpect(status().isOk())
.andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.id").value(1))
}
55
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
when(channelRepository.findOne(1))
.thenReturn(channel);
!
mockMvc.perform(get("/channels/1")
.accept("application/json"))
.andExpect(status().isOk())
.andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("MTV"));
}
56
Negative Test Case
@Test
public void itShouldNotFindChannel() throws Exception {
// optional
when(channelRepository.findOne(-1)).willReturn(null);
}
57
Negative Test Case
@Test
public void itShouldNotFindChannel() throws Exception {
// optional
when(channelRepository.findOne(-1)).willReturn(null);
!
mockMvc.perform(get("/channels/-1")
.accept("application/json"))
}
58
Negative Test Case
@Test
public void itShouldNotFindChannel() throws Exception {
// optional
when(channelRepository.findOne(-1)).willReturn(null);
!
mockMvc.perform(get("/channels/-1")
.accept("application/json"))
.andExpect(status().isNotFound());
}
59
Negative Test Case
@Test
public void itShouldNotFindChannel() throws Exception {
// optional
when(channelRepository.findOne(-1)).willReturn(null);
!
MvcResult mvcResult = mockMvc
.perform(get("/channels/-1")
.accept("application/json"))
.andExpect(status().isNotFound())
.andReturn();
!
assertThat(mvcResult.getResolvedException(),
instanceOf(ChannelNotFoundException.class));
}
60
Demo
ChannelController instantiated
Mock of ChannelRepository injected
MockMvc was set-upped
MockHttpServletRequest prepared
Executed via DispatcherServlet
Assertions applied on the resulting MockHttpServletResponse
Assertions applied on the resulting MvcResult
61
What happened
- Easy to write

- Uses Spring annotations

- Always interacts with DispatcherServlet
62
Pros
- A bit slow (about 1 second for the first test case)

- No Actual Spring MVC configuration loaded
63
Caveats
Web Application Context
Server-Side Integration Tests
65
Scaffolding
!
!
!
!
!
!
!
!
public class ChannelControllerWebAppIT {
}
66
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
!
!
!
!
!
!
!
public class ChannelControllerWebAppIT {
}
67
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
!
!
!
!
!
!
public class ChannelControllerWebAppIT {
@Autowired
private WebApplicationContext wac;
}
68
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:somewhere/servlet-context.xml",
"file:somewhere/persistence-context.xml"
})
!
!
public class ChannelControllerWebAppIT {
@Autowired
private WebApplicationContext wac;
}
69
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:somewhere/servlet-context.xml",
"file:somewhere/persistence-context.xml"
})
@Transactional
!
public class ChannelControllerWebAppIT {
@Autowired
private WebApplicationContext wac;
}
70
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:somewhere/servlet-context.xml",
"file:somewhere/persistence-context.xml"
})
@Transactional
@Sql(scripts = "classpath:test-channel-seeds.sql")
public class ChannelControllerWebAppIT {
@Autowired
private WebApplicationContext wac;
}
71
Scaffolding
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:somewhere/servlet-context.xml",
"file:somewhere/persistence-context.xml"
})
@Transactional
@Sql(scripts = "classpath:test-channel-seeds.sql")
public class ChannelControllerWebAppIT {
@Autowired
private WebApplicationContext wac;
!
private MockMvc mockMvc;
!
@Before
public void setUp() {
mockMvc = webAppContextSetup(wac).build();
}
}
72
Positive Test Case
@Test
public void itShouldFindChannel() throws Exception {
mockMvc.perform(get("/channels/1")
.accept("application/json"))
.andExpect(status().isOk())
.andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("MTV"));
}
73
Negative Test Case
@Test
public void itShouldNotFindChannel() throws Exception {
MvcResult mvcResult = mockMvc
.perform(get("/channels/-1")
.accept("application/json"))
.andExpect(status().isNotFound())
.andReturn();
!
assertThat(mvcResult.getResolvedException(),
instanceOf(ChannelNotFoundException.class));
}
74
Demo
Actual Web MVC application context loaded
MockHttpServletRequest prepared
Executed via DispatcherServlet
Assertions applied on the resulting MockHttpServletResponse
Assertions applied on the resulting MvcResult
75
What happened
- Easy to write

- Loads actual Spring MVC configuration (cacheable)

- Uses Spring annotations

- Always Interacts with DispatcherServlet
76
Pros
- Slower than the “Standalone” option (depends on amount of beans
in a particular Spring Mvc configuration)

- Does not replace end-to-end testing like Selenium
77
Caveats
Further Materials
Integration between Spring MVC Test Framework and HtmlUnit.
Repository: https://github.com/spring-projects/spring-test-htmlunit

Documentation: https://github.com/spring-projects/spring-test-
htmlunit/blob/master/src/asciidoc/index.adoc
79
Spring MVC Test HtmlUnit
Spring Framework Reference Documentation

Chapter 11.3 Integration Testing

http://docs.spring.io/spring/docs/current/spring-framework-reference/
htmlsingle/#integration-testing
spring-test artifact source code

https://github.com/spring-projects/spring-framework/tree/master/
spring-test
Spring MVC Showcase

https://github.com/spring-projects/spring-mvc-showcase
Code examples

https://github.com/ffbit/spring-mvc-test-framework-examples
80
Links
Webinar: Testing Web Applications with Spring 3.2

by Sam Brannen (Swiftmind) and Rossen Stoyanchev

https://www.youtube.com/watch?v=K6x8LE7Qd1Q
Spring Testing

by Mattias Severson

https://www.youtube.com/watch?v=LYVJ69h76nw
!
81
Videos
Thank you!
!
Questions?
ffbit@yandex-team.ru
Dmytro Chyzhykov
dmytro.chyzhykov@yandex.ru
ffbit
Senior Software Engineer
at Yandex Media Services
Kyiv, Ukraine
@dcheJava
84
Slides and Code Examples
85http://www.dotatalk.com/wp-content/uploads/2013/09/All-hail-King-Hypno-Toad.jpg

Testing Web Apps with Spring Framework