Java Ninja Chronicles By Norris Shelton

Things I learned in the pursuit of code

Springframework MVC 3.2 introduced the ability to test controllers via MockMVC. In this example, we will test a controller that returns JSON via @ResponseBody.

Here are the dependencies that you will need.

            <!-- This is usually included as part of your normal dependencies.  If not, include this way -->
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>${spring.version}</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>com.jayway.jsonpath</groupId>
                <artifactId>json-path</artifactId>
                <version>${jsonpath.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>com.jayway.jsonpath</groupId>
                <artifactId>json-path-assert</artifactId>
                <version>${jsonpath.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.11</version>
                <scope>test</scope>
            </dependency>

This is an example of the spring context.

<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.xsd
                           http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd">

    <mvc:annotation-driven/>

    <context:component-scan base-package="com.cdi.igs.core.spring.web"/>

</beans>

The basic test class needs to have the following annotations, properties and setup defined. Please note the static imports. You want to use these from the mock mvc request builders.

import com.fasterxml.jackson.databind.ObjectMapper;
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.http.MediaType;
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;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class TestGlobalControllerExceptionHandler {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
//...
}

This is an example test method that calls a spring controller that returns a string. We are creating an empty model object that we convert to JSON. We tell the method that we are calling that we are sending JSON, because the method that we are calling expects to be sent a JSON object. We get validation errors in this case, that we compare with our expected results. The JSON response that we get back from the call to the controller is “{“fieldErrors”:[{“message”:”may not be null”},{“message”:”may not be null”}]}

    @Test
    public void testMyMethod() throws Exception {
        mockMvc.perform(post("/mymethod")
                        .content(new ObjectMapper().writeValueAsString(new TargetExceptionModel()))
                        .contentType(MediaType.APPLICATION_JSON))
               .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.fieldErrors", hasSize(2)))
        .andExpect(jsonPath("$.fieldErrors[*].message", contains("may not be null", "may not be null")))
        .andDo(print());
    }

Here is an example of a controller test method that users other types of matchers. Example data is:

{"status":"SUCCESS","message":null,"previousPage":null,"nextPage":"login/privacy_policy","errors":null,"exception":null,"idSetting":"pri","setting":"privacy-Starfish","value":"test","settingText":"lots and lots of text","date":"01-27-2014"}
    @Test
    public void testGetPrivacyByClientSuccess() throws Exception {
        mockMvc.perform(get("/privacy/pri"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.idSetting", containsString("pri")))
                .andExpect(jsonPath("$.setting", containsString("privacy-Starfish")))
                .andExpect(jsonPath("$.settingText", notNullValue()))
                .andExpect(jsonPath("$.value", containsString("test")))
                .andExpect(jsonPath("$.exception", nullValue()))
                .andDo(print());
    }

I needed to write a method that passed an object that contained an exception object back to the calling method. The data being sent back was:

{"status":"ERROR","message":null,"previousPage":null,"nextPage":null,"errors":null,"exception":{"cause":null,"stackTrace":[{"methodName":"copyProperties","fileName":"PropertyUtilsBean.java","lineNumber":276,"className":"org.apache.commons.beanutils.PropertyUtilsBean","nativeMethod":false},{"methodName":"copyProperties","fileName":"PropertyUtils.java","lineNumber":219,"className":"org.apache.commons.beanutils.PropertyUtils","nativeMethod":false},{"methodName":"getPrivacy","fileName":"LoginService.java","lineNumber":193,"className":"com.cdi.igs.core.login.LoginService","nativeMethod":false},{"methodName":"getPrivacyByClient","fileName":"LoginController.java","lineNumber":94,"className":"com.cdi.igs.services.LoginController","nativeMethod":false},{"methodName":"invoke0","fileName":"NativeMethodAccessorImpl.java","lineNumber":-2,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":true},{"methodName":"invoke","fileName":"NativeMethodAccessorImpl.java","lineNumber":57,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"DelegatingMethodAccessorImpl.java","lineNumber":43,"className":"sun.reflect.DelegatingMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"Method.java","lineNumber":606,"className":"java.lang.reflect.Method","nativeMethod":false},{"methodName":"invoke","fileName":"InvocableHandlerMethod.java","lineNumber":214,"className":"org.springframework.web.method.support.InvocableHandlerMethod","nativeMethod":false},{"methodName":"invokeForRequest","fileName":"InvocableHandlerMethod.java","lineNumber":132,"className":"org.springframework.web.method.support.InvocableHandlerMethod","nativeMethod":false},{"methodName":"invokeAndHandle","fileName":"ServletInvocableHandlerMethod.java","lineNumber":104,"className":"org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod","nativeMethod":false},{"methodName":"invokeHandleMethod","fileName":"RequestMappingHandlerAdapter.java","lineNumber":749,"className":"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter","nativeMethod":false},{"methodName":"handleInternal","fileName":"RequestMappingHandlerAdapter.java","lineNumber":690,"className":"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter","nativeMethod":false},{"methodName":"handle","fileName":"AbstractHandlerMethodAdapter.java","lineNumber":83,"className":"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter","nativeMethod":false},{"methodName":"doDispatch","fileName":"DispatcherServlet.java","lineNumber":945,"className":"org.springframework.web.servlet.DispatcherServlet","nativeMethod":false},{"methodName":"doService","fileName":"DispatcherServlet.java","lineNumber":876,"className":"org.springframework.web.servlet.DispatcherServlet","nativeMethod":false},{"methodName":"processRequest","fileName":"FrameworkServlet.java","lineNumber":961,"className":"org.springframework.web.servlet.FrameworkServlet","nativeMethod":false},{"methodName":"doGet","fileName":"FrameworkServlet.java","lineNumber":852,"className":"org.springframework.web.servlet.FrameworkServlet","nativeMethod":false},{"methodName":"service","fileName":"HttpServlet.java","lineNumber":687,"className":"javax.servlet.http.HttpServlet","nativeMethod":false},{"methodName":"service","fileName":"FrameworkServlet.java","lineNumber":837,"className":"org.springframework.web.servlet.FrameworkServlet","nativeMethod":false},{"methodName":"service","fileName":"TestDispatcherServlet.java","lineNumber":64,"className":"org.springframework.test.web.servlet.TestDispatcherServlet","nativeMethod":false},{"methodName":"service","fileName":"HttpServlet.java","lineNumber":790,"className":"javax.servlet.http.HttpServlet","nativeMethod":false},{"methodName":"doFilter","fileName":"MockFilterChain.java","lineNumber":170,"className":"org.springframework.mock.web.MockFilterChain$ServletFilterProxy","nativeMethod":false},{"methodName":"doFilter","fileName":"MockFilterChain.java","lineNumber":137,"className":"org.springframework.mock.web.MockFilterChain","nativeMethod":false},{"methodName":"perform","fileName":"MockMvc.java","lineNumber":141,"className":"org.springframework.test.web.servlet.MockMvc","nativeMethod":false},{"methodName":"testGetPrivacyByClientFail","fileName":"TestLoginController.java","lineNumber":96,"className":"com.cdi.igs.services.TestLoginController","nativeMethod":false},{"methodName":"invoke0","fileName":"NativeMethodAccessorImpl.java","lineNumber":-2,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":true},{"methodName":"invoke","fileName":"NativeMethodAccessorImpl.java","lineNumber":57,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"DelegatingMethodAccessorImpl.java","lineNumber":43,"className":"sun.reflect.DelegatingMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"Method.java","lineNumber":606,"className":"java.lang.reflect.Method","nativeMethod":false},{"methodName":"runReflectiveCall","fileName":"FrameworkMethod.java","lineNumber":47,"className":"org.junit.runners.model.FrameworkMethod$1","nativeMethod":false},{"methodName":"run","fileName":"ReflectiveCallable.java","lineNumber":12,"className":"org.junit.internal.runners.model.ReflectiveCallable","nativeMethod":false},{"methodName":"invokeExplosively","fileName":"FrameworkMethod.java","lineNumber":44,"className":"org.junit.runners.model.FrameworkMethod","nativeMethod":false},{"methodName":"evaluate","fileName":"InvokeMethod.java","lineNumber":17,"className":"org.junit.internal.runners.statements.InvokeMethod","nativeMethod":false},{"methodName":"evaluate","fileName":"RunBefores.java","lineNumber":26,"className":"org.junit.internal.runners.statements.RunBefores","nativeMethod":false},{"methodName":"evaluate","fileName":"RunBeforeTestMethodCallbacks.java","lineNumber":74,"className":"org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks","nativeMethod":false},{"methodName":"evaluate","fileName":"RunAfterTestMethodCallbacks.java","lineNumber":83,"className":"org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks","nativeMethod":false},{"methodName":"evaluate","fileName":"SpringRepeat.java","lineNumber":72,"className":"org.springframework.test.context.junit4.statements.SpringRepeat","nativeMethod":false},{"methodName":"runChild","fileName":"SpringJUnit4ClassRunner.java","lineNumber":232,"className":"org.springframework.test.context.junit4.SpringJUnit4ClassRunner","nativeMethod":false},{"methodName":"runChild","fileName":"SpringJUnit4ClassRunner.java","lineNumber":89,"className":"org.springframework.test.context.junit4.SpringJUnit4ClassRunner","nativeMethod":false},{"methodName":"run","fileName":"ParentRunner.java","lineNumber":238,"className":"org.junit.runners.ParentRunner$3","nativeMethod":false},{"methodName":"schedule","fileName":"ParentRunner.java","lineNumber":63,"className":"org.junit.runners.ParentRunner$1","nativeMethod":false},{"methodName":"runChildren","fileName":"ParentRunner.java","lineNumber":236,"className":"org.junit.runners.ParentRunner","nativeMethod":false},{"methodName":"access$000","fileName":"ParentRunner.java","lineNumber":53,"className":"org.junit.runners.ParentRunner","nativeMethod":false},{"methodName":"evaluate","fileName":"ParentRunner.java","lineNumber":229,"className":"org.junit.runners.ParentRunner$2","nativeMethod":false},{"methodName":"evaluate","fileName":"RunBeforeTestClassCallbacks.java","lineNumber":61,"className":"org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks","nativeMethod":false},{"methodName":"evaluate","fileName":"RunAfterTestClassCallbacks.java","lineNumber":71,"className":"org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks","nativeMethod":false},{"methodName":"run","fileName":"ParentRunner.java","lineNumber":309,"className":"org.junit.runners.ParentRunner","nativeMethod":false},{"methodName":"run","fileName":"SpringJUnit4ClassRunner.java","lineNumber":175,"className":"org.springframework.test.context.junit4.SpringJUnit4ClassRunner","nativeMethod":false},{"methodName":"run","fileName":"JUnitCore.java","lineNumber":160,"className":"org.junit.runner.JUnitCore","nativeMethod":false},{"methodName":"startRunnerWithArgs","fileName":"JUnit4IdeaTestRunner.java","lineNumber":74,"className":"com.intellij.junit4.JUnit4IdeaTestRunner","nativeMethod":false},{"methodName":"prepareStreamsAndStart","fileName":"JUnitStarter.java","lineNumber":211,"className":"com.intellij.rt.execution.junit.JUnitStarter","nativeMethod":false},{"methodName":"main","fileName":"JUnitStarter.java","lineNumber":67,"className":"com.intellij.rt.execution.junit.JUnitStarter","nativeMethod":false},{"methodName":"invoke0","fileName":"NativeMethodAccessorImpl.java","lineNumber":-2,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":true},{"methodName":"invoke","fileName":"NativeMethodAccessorImpl.java","lineNumber":57,"className":"sun.reflect.NativeMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"DelegatingMethodAccessorImpl.java","lineNumber":43,"className":"sun.reflect.DelegatingMethodAccessorImpl","nativeMethod":false},{"methodName":"invoke","fileName":"Method.java","lineNumber":606,"className":"java.lang.reflect.Method","nativeMethod":false},{"methodName":"main","fileName":"AppMain.java","lineNumber":120,"className":"com.intellij.rt.execution.application.AppMain","nativeMethod":false}],"message":"No origin bean specified","localizedMessage":"No origin bean specified","suppressed":[]},"idSetting":"Starfish1","setting":null,"value":null,"settingText":null,"date":null}
    @Test
    public void testGetPrivacyByClientFail() throws Exception {
        mockMvc.perform(get("/privacy/Starfish1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.idSetting", containsString("Starfish1")))
               .andExpect(jsonPath("$.setting", nullValue()))
               .andExpect(jsonPath("$.settingText", nullValue()))
               .andExpect(jsonPath("$.value", nullValue()))
               .andExpect(jsonPath("$.exception.cause", nullValue()))
               .andExpect(jsonPath("$.exception.stackTrace", notNullValue()))
               .andExpect(jsonPath("$.exception.stackTrace[0].methodName", notNullValue()))
               .andDo(print());
    }

To get the content and store it in a variable outside the test, add .andReturn() to the end of the perform and store the result in a MvcResult, then get the content.

        MvcResult result = mockMvc.perform(post("/login")
                                               .content(
                                                   "{"userName":"screen011","password":"Jason3080!"," +
                                                   ""remoteAddress":"0.0" +
                                                   ".0.0"}"
                                                       )
                                               .contentType(MediaType.APPLICATION_JSON))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.exception", nullValue()))
               .andExpect(jsonPath("$.errors", hasSize(0)))
               .andExpect((jsonPath("$.ssoToken", notNullValue())))
               .andDo(print())
               .andReturn();
        String content = result.getResponse().getContentAsString();

Then if you need to extract a value out of the JSON, you can use JsonPath out of jayway like

        String content = result.getResponse().getContentAsString();

        assertNotNull(content);
        String ssoToken = JsonPath.read(content, "$.ssoToken");
        assertNotNull(ssoToken);

February 27th, 2014

Posted In: Java, MockMVC, Spring, Spring MVC

Tags: , , , , , , ,

2 Comments

I am using a Springframework project and had a method that had been cached before, but stopped working after being refactored. The method cached application properties that were being read from a webdav repository. I noticed while debugging some other production code that the properties file was being read on every request.

    I verified the following:

  • The class was an annotated spring bean
    @Repository
  • The method was annotated as a cacheable method
    @Cacheable(“GlobalPropertiesDao.getProperties”)
  • The method was public
  • The package was being scanned for components
    )
  • The spring context was configured for annotated cache beans
  • Ehcache was configured for caching
        <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:shared="true"/>
        <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cacheManager-ref="ehcache"/>
    
  • Verified that there was an ehcache.xml that was being placed in the root of my classpath
  • Verified that the specified name of my cache was in the ehcache.xml
        <cache name="GlobalPropertiesDao.getProperties" maxElementsInMemory="1000" eternal="false" overflowToDisk="false"
               timeToIdleSeconds="300" timeToLiveSeconds="300"/>
    

Still the method was not being cached. While reading through the Spring documentation, I ran across the following paragraph.

Note
In proxy mode (which is the default), only external method calls coming in through the proxy
are intercepted. This means that self-invocation, in effect, a method within the target object
calling another method of the target object, will not lead to an actual caching at runtime even
if the invoked method is marked with @Cacheable – considering using the aspectj mode in
this case.

Doh!!!! That was it. I provide a method that looks for the property within the properties and it automatically looks for the property that is defined for this environment (property.env=…) and the property without an environment (property=…). The class was composed of 2 methods. One that looks for the property and one that reads the property file. I chose to cache the reading of the properties file so that the webdav would take the file read hit at most once every 5 minutes. Had I chosen to cache the individual properties, the webdav could take the file read hit once per property every 5 minutes.

I started with the following class:

package com.appriss.jxp.properties;

import com.appriss.justicexchange.commons.http.HttpUtils;
import com.appriss.justicexchange.commons.utility.Utility;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Properties;

import static com.appriss.core.Environment.*;


/**
 * JX Property Dao.
 * @author jmiller Jan 19, 2012 12:58:26 PM
 * @author Norris Shelton
 */
@Repository
public class PropertiesDao {

    private static final Logger log = LoggerFactory.getLogger(PropertiesDao.class);

    /**
     * Gets a JX Portal property.  It first tries to find the property with the environment as the suffix.  If not
     * found, it will look for the property without the environment.
     * @param key the key of the property
     * @return the value of the property
     * @throws Exception if the property is not found
     */
    public String getProperty(String key) throws IOException {
        String property = getProperties().getProperty(key + '.' + getEnvironment().toString());
        if (StringUtils.isBlank(property)) {
            property = getProperties().getProperty(key);
            if (StringUtils.isBlank(property)) {
                throw new IOException("Global property not found " + key + '.' + getEnvironment().toString() + " or " + key);
            }
        }
        return property;
    }

    /**
     * Reads the properties from the webdav for this environment.
     * @return Properties object
     */
    @Cacheable("GlobalPropertiesDao.getProperties")
    public Properties getProperties() throws IOException {
        Properties properties = new Properties();
        String response = HttpUtils.getResponse(Utility.getWebdavServer() + "/product/jxportal/config/jxportal.properties");
        if (StringUtils.isNotBlank(response)) {
            //noinspection ConstantConditions
            properties.load(new ByteArrayInputStream(response.getBytes()));
        }
        return properties;
    }
}

The getProperties method on line 54 was being called by the getProperty method on line 41. That was my problem. I ended up splitting up the code into 2 classes. Here are the 2 classes.

package com.appriss.jxp.properties;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static com.appriss.core.Environment.*;


/**
 * JX Property Service class.
 * @author Norris Shelton
 */
@Component
public class PropertiesService {

    @Autowired
    private PropertiesDao propertiesDao;

    /**
     * Gets a JX Portal property.  It first tries to find the property with the environment as the suffix.  If not
     * found, it will look for the property without the environment.
     * @param key the key of the property
     * @return the value of the property
     * @throws IOException if the property is not found
     */
    public String getProperty(String key) throws IOException {
        String property = propertiesDao.getProperties().getProperty(key + '.' + getEnvironment().toString());
        if (StringUtils.isBlank(property)) {
            property = propertiesDao.getProperties().getProperty(key);
            if (StringUtils.isBlank(property)) {
                throw new IOException("Global property not found " + key + '.' + getEnvironment().toString() + " or " + key);
            }
        }
        return property;
    }
}
package com.appriss.jxp.properties;

import com.appriss.justicexchange.commons.http.HttpUtils;
import com.appriss.justicexchange.commons.utility.Utility;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * Properties DAO cache object.
 * NOTE: This is the real cache.  It had to be made into another class because Spring will not proxy a proxied object.
 * Meaning that the cached method was being called by another method in the class and this prevented the properties
 * file from being cached.  It is better to cache the whole properties file instead of each property.  This leads to
 * one read every n minutes instead of x reads every n minutes.
 * <p>Note
 In proxy mode (which is the default), only external method calls coming in through the proxy
 are intercepted. This means that self-invocation, in effect, a method within the target object
 calling another method of the target object, will not lead to an actual caching at runtime even
 if the invoked method is marked with @Cacheable - considering using the aspectj mode in
 this case.</p>
 */
@Repository
public class PropertiesDao {

    /**
     * Reads the properties from the webdav for this environment.
     * @return Properties object
     */
    @Cacheable("GlobalPropertiesDao.getProperties")
    public Properties getProperties() throws IOException {
        Properties properties = new Properties();
        String response = HttpUtils.getResponse(Utility.getWebdavServer() +
                                                "/product/jxportal/config/jxportal.properties");
        if (StringUtils.isNotBlank(response)) {
            //noinspection ConstantConditions
            properties.load(new ByteArrayInputStream(response.getBytes()));
        }
        return properties;
    }

}

Finally, problem solved.

February 26th, 2013

Posted In: Ehcache, Java, Spring

Tags: , , , ,

Leave a Comment

LinkedIn Auto Publish Powered By : XYZScripts.com