Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

A Note on Unit Test Spring MVC Applications

5.00/5 (1 vote)
3 Apr 2018CPOL2 min read 13K   90  
This is a note on unit test Spring MVC applications.

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.

XML
<dependencies>         
        <!-- Servlet jars for compilation, provided by Tomcat -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-servlet-api</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
        
         <!-- Spring MVC dependencies -->
        <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>
        
        <!-- Test dependencies -->      
        <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.

XML
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.9.10</version>
    <scope>test</scope>
</dependency>
XML
<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.

Java
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:

Java
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.

Java
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.

Java
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".

Java
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.

Java
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

  • 4/2/2018: First revision

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)