Introduction
This is a note on unit test Spring MVC applications.
Background
This is a note on the unit test of Spring MVC applications. Spring provides a "spring-test" package, it simulates the routing process from a URL to an MVC controller. The test frameworks can run the code in the controllers by the URLs. The two attached examples are identical, except that one of them uses "TestNG" and the other uses "JUnit".
The Maven Dependencies
To create a Spring MVC application and perform a Spring style unit test on it, you will need the following dependencies.
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.0.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
Depending on your choice of TestNG or JUnit, you will also need to selectively add one of the following dependencies.
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.9.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
The Example MVC Application
The simple MVC application in the attached examples has only one controller that is implemented in the "TestController
" class.
package com.song.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.song.web.service.ExampleService;
@Controller
public class TestController {
@Autowired
private ExampleService exampleService;
@ResponseBody
@RequestMapping(value = "/example-end-point", method = RequestMethod.GET)
public Object test() {
return exampleService.getAMap();
}
}
The "@Autowired" "ExampleService
" is implemented as the following:
package com.song.web.service;
import java.util.HashMap;
import org.springframework.stereotype.Service;
@Service
public class ExampleService{
public HashMap<String, String> getAMap() {
HashMap<String, String> map = new HashMap<String, String>();
map.put("V", "This is from a Spring service.");
return map;
}
}
The MVC application is initialized by the "MVCInitializer
" class.
package com.song.web.configuration;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support
.AbstractAnnotationConfigDispatcherServletInitializer;
import com.song.web.filter.NocacheFilter;
import com.song.web.service.ExampleService;
public class MVCInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@EnableWebMvc
@ComponentScan({ "com.song.web.controller" })
public static class MVCConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
ResourceHandlerRegistration registration = registry.addResourceHandler("/*");
registration.addResourceLocations("/");
}
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public ExampleService exampleService() {
return new ExampleService();
}
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { MVCConfiguration.class };
}
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
FilterRegistration.Dynamic nocachefilter = servletContext
.addFilter("nocachefilter", new NocacheFilter());
nocachefilter.addMappingForUrlPatterns(null, false, "/*");
super.onStartup(servletContext);
}
@Override
protected String[] getServletMappings() {
return new String[] { "/api/*" };
}
@Override
protected Class<?>[] getRootConfigClasses() { return null; }
}
If you deploy and run the application, and if you issue a "GET
" request to "http://localhost:8080/ut-spring-mvc-testng/api/example-end-point", you can see the following response in the POSTMAN.
Unit Test With TestNG
In order to initiate the Spring context in the unit test, you need a configuration class that implements the "WebMvcConfigurer" interface.
package com.song.web.controller;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.song.web.service.ExampleService;
@EnableWebMvc
@ComponentScan({ "com.song.web.controller" })
public class TestMVCConfiguration implements WebMvcConfigurer {
@Bean
public ExampleService exampleService() {
return new ExampleService();
}
}
- The "
@ComponentScan
" annotation tells Spring where to find the controllers. - The "
@Bean
" function returns an instance of the "ExampleService
" class. If you do not want to use the actual implementation, you can return a mock of the class for unit test purpose.
The "ControllerTest
" class performs the test on the "TestController
".
package com.song.web.controller;
import java.util.HashMap;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
@WebAppConfiguration
@ContextConfiguration( classes = { TestMVCConfiguration.class })
public class ControllerTest extends AbstractTestNGSpringContextTests {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext wac;
@BeforeMethod
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void ADummyTest() {
MockHttpServletRequestBuilder request
= MockMvcRequestBuilders.get("/example-end-point");
try {
ResultActions resultActions = mockMvc.perform(request);
MvcResult result = resultActions.andReturn();
MockHttpServletResponse response = result.getResponse();
int status = response.getStatus();
Assert.assertEquals(HttpServletResponse.SC_OK, status);
ObjectMapper mapper = new ObjectMapper();
HashMap<String, String> data = new HashMap<String, String>();
mapper.readerForUpdating(data).readValue(response.getContentAsString());
Assert.assertEquals(data.get("V"), "This is from a Spring service.");
} catch(Exception ex) { Assert.fail("Failed - " + ex.getMessage()); }
}
}
In order that TestNG can recognize the test as a Spring style unit test, the "ControllerTest
" class needs to extend the "AbstractTestNGSpringContextTests" class.
Unit Test With JUnit
If you want to use JUnit to perform the test, you can replace "testng" with "junit" in your POM dependencies.
package com.song.web.controller;
import java.util.HashMap;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
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.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith( SpringJUnit4ClassRunner.class )
@WebAppConfiguration
@ContextConfiguration( classes = { TestMVCConfiguration.class })
public class ControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext wac;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void ADummyTest() {
MockHttpServletRequestBuilder request
= MockMvcRequestBuilders.get("/example-end-point");
try {
ResultActions resultActions = mockMvc.perform(request);
MvcResult result = resultActions.andReturn();
MockHttpServletResponse response = result.getResponse();
int status = response.getStatus();
Assert.assertEquals(HttpServletResponse.SC_OK, status);
ObjectMapper mapper = new ObjectMapper();
HashMap<String, String> data = new HashMap<String, String>();
mapper.readerForUpdating(data).readValue(response.getContentAsString());
Assert.assertEquals("This is from a Spring service.", data.get("V"));
} catch(Exception ex) { Assert.fail("Failed - " + ex.getMessage()); }
}
}
Instead of extending the "AbstractTestNGSpringContextTests" class, you need to annotate the "ControllerTest
" class by "@RunWith( SpringJUnit4ClassRunner.class )
".
Test Without Spring Annotations
If you can manually create a controller instance, you can perform the unit tests without the Spring annotations.
@Controller
public class TestController {
private ExampleService exampleService;
@Autowired
public TestController(final ExampleService exampleService) {
this.exampleService = exampleService;
}
@ResponseBody
@RequestMapping(value = "/example-end-point", method = RequestMethod.GET)
public Object test() {
return exampleService.getAMap();
}
}
In the "TestController", we have a constructor to take the "ExampleService" so we can manually construct a functional controller instance.
package com.song.web.controller;
import java.util.HashMap;
import javax.servlet.http.HttpServletResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.song.web.service.ExampleService;
public class ControllerTest {
private MockMvc mockMvc;
@BeforeMethod
public void setup() {
this.mockMvc = MockMvcBuilders
.standaloneSetup(new TestController(new ExampleService())).build();
}
@Test
public void ADummyTest() {
MockHttpServletRequestBuilder request
= MockMvcRequestBuilders.get("/example-end-point");
try {
ResultActions resultActions = mockMvc.perform(request);
MvcResult result = resultActions.andReturn();
MockHttpServletResponse response = result.getResponse();
int status = response.getStatus();
Assert.assertEquals(HttpServletResponse.SC_OK, status);
ObjectMapper mapper = new ObjectMapper();
HashMap<String, String> data = new HashMap<String, String>();
mapper.readerForUpdating(data).readValue(response.getContentAsString());
Assert.assertEquals(data.get("V"), "This is from a Spring service.");
} catch(Exception ex) { Assert.fail("Failed - " + ex.getMessage()); }
}
}
We can create the "MockMvc" instance by passing the controller instance to the "MockMvcBuilders .standaloneSetup()" method without going through the Spring context initializations and proceed with the unit test directly.
MVN & Memory
During the build process, you may encounter memory issue if the project gets large. You may also fail on the unit tests due to memory issues. In such cases, I noticed that the following command can alleviate the problem.
mvn clean install -DargLine="-Xmx4096m"
You may also change the POM to put the "-Xmx" argument to the SureFire plugin.
Points of Interest
- This is a note on unit test Spring MVC applications.
- I hope you like my postings and I hope this note can help you one way or the other.
History