Thursday, March 29, 2007

Unit Testing Struts 2.0 (Part 2)

Edit: for latest on this see recent post.


In response to a comment made to Unit Testing Struts 2.0, here is the updated, complete code for Struts 2.0 testing. Hope this is useful. Have questions ? Want to discuss any of this ? Just drop me a line...Read on, includes support class code, small snippets for creation of Spring beans, Struts 2.0 actions, Struts 2.0 action proxies.


import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ActionProxyFactory;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.entities.ActionConfig;

import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsStatics;
import org.apache.struts2.config.Settings;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.impl.StrutsActionProxyFactory;
import org.apache.struts2.spring.StrutsSpringObjectFactory;
import org.apache.struts2.views.freemarker.FreemarkerManager;

import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.mock.web.MockServletContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;

import java.lang.reflect.Method;

import java.net.URLEncoder;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;


/**
* Class for easier support of Struts related
* testing. Takes care of all the configuration details
* that allow test classes to create beans (Spring),
* actions (Struts), intercepted actions (Struts).
* Class is singleton to minimize hit of initializing
* Struts and related infrastructure (e.g. Hibernate).
*
* @author Francisco Assis Rosa
*/
public class StrutsTestCaseSupport {

/**
* Singleton variable
*/
public static StrutsTestCaseSupport _theInstance;

/**
* Singleton access
*/
public static synchronized StrutsTestCaseSupport getInstance() {
if ( _theInstance == null ) {
_theInstance = new StrutsTestCaseSupport();
}
return _theInstance;
}

/**
* Application context class (encapsulation of applicationContext.xml)
*/
ConfigurableWebApplicationContext _applicationContext;

/**
* Configuration Manager object, to allow for encapusulation of struts.xml,
* creation of actions and their proxied counterparts, creation of
* servlet context from this application context
*/
ConfigurationManager _configurationManager;

/**
* Class constructor, take care of Struts initializations
*/
private StrutsTestCaseSupport () {

// create the struts+spring integrated object factory
// set spring autowiring by name for spring object factory
Settings.set(StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,"name");
StrutsSpringObjectFactory objectFactory = new StrutsSpringObjectFactory();

// set system object facory
ObjectFactory.setObjectFactory(objectFactory);

// set action proxy factory
ActionProxyFactory.setFactory(new StrutsActionProxyFactory());

// create a web application context instance (for spring configuration)
_applicationContext = new XmlWebApplicationContext();

// get ahold of a servlet context to use in the creation of the application context
ServletContext servletContext = createOneServletContext(_applicationContext);
// complete application context initialization, pass in servlet
// context and config file location, force reading of config (via refresh)
_applicationContext.setServletContext(servletContext);
_applicationContext.setConfigLocations(new String[] {"WEB-INF/applicationContext.xml"});
_applicationContext.refresh();

// initialize the object factory with the mock servlet context, application context
objectFactory.init(servletContext);
objectFactory.setApplicationContext(_applicationContext);

// add a default dispatcher to the system
Dispatcher du = new Dispatcher(servletContext);
Dispatcher.setInstance(du);

// pass over to the configuration manager location where struts-default.xml,
// struts-plugin.xml and struts.xml can be found, force reading all
_configurationManager = new ConfigurationManager();
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts-default.xml", false));
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts-plugin.xml", false));
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts.xml", false));
_configurationManager.reload();
}

/**
* create a servlet context useable for a specific action
*
* @param applicationContext the application context to use in the servlet context
* @return the created servlet context
*/
protected ServletContext createOneServletContext (ConfigurableWebApplicationContext applicationContext) {
// create a servlet context for this action, use FileSystemResourceLoader for
// context to find configuration files
ServletContext servletContext = (ServletContext) new MockServletContext(new FileSystemResourceLoader());

// initialize freemarker manager config parameter to null (let FreemarkerManager figure
// out configuration location out of ServletContext)
Settings.set(StrutsConstants.STRUTS_I18N_ENCODING, "UTF-8");
servletContext.setAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY,null);

// hand over application context to servlet context
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, applicationContext);

return servletContext;
}

/**
* Build one action context for an accessmethod and an access url
*
* @param serverName the hostname that the request will need to hook up to
* @param accessMethod http method to use (e.g. 'get', 'post', 'put', etc)
* @param accessUrl the url to access
* @return the map for the action's context
* @throws Exception on processing, configuration errors, test failure
*/
public Map buildActionContext ( String serverName, String accessMethod, String accessUrl, Map requestParamMap )
throws Exception {
// get ahold of a brand new servlet context
ServletContext servletContext = createOneServletContext(_applicationContext);

// create fake request and response objects
MockHttpServletRequest request = new MockHttpServletRequest(servletContext,accessMethod,accessUrl);
MockHttpServletResponse response = new MockHttpServletResponse();

// set request server name
request.setServerName(serverName);

// add request parameters
if ( "get".equals(accessMethod) ) {
for ( String oneParamName : requestParamMap.keySet() ) {
request.addParameter(oneParamName,requestParamMap.get(oneParamName));
}
} else if ( "post".equals(accessMethod) ) {
String requestBody = "";
for ( String oneParamName : requestParamMap.keySet() ) {
if ( requestBody.length() > 0 ) {
requestBody += "&";
}
requestBody += oneParamName + "=" + URLEncoder.encode(requestParamMap.get(oneParamName),"UTF-8");
}
request.setContent(requestBody.getBytes());
}

// add context, request and response to an action context map
Map actionContext = new HashMap();
actionContext.put(StrutsStatics.SERVLET_CONTEXT,servletContext);
actionContext.put(StrutsStatics.HTTP_REQUEST,request);
actionContext.put(StrutsStatics.HTTP_RESPONSE,response);
actionContext.put(ActionContext.DEV_MODE,new Boolean(false));
// add request parameters to action context
Map actionContextParams = new HashMap();
for ( String oneParamName : requestParamMap.keySet() ) {
String[] paramValue = new String[1];
paramValue[0] = requestParamMap.get(oneParamName);
actionContextParams.put(oneParamName,paramValue);
}
actionContext.put(ActionContext.PARAMETERS,actionContextParams);

return actionContext;
}

/**
* create a bean from the object factory (all wired up from Spring)
*
* @param beanName the name of the bean to get from the object factory
* @param extraContent any extra content information to pass along to the bean building
* process
* @return the object factory created bean
* @throws Exception on processing, configuration errors, test failure
*/
public Object createBean ( String beanName, Map extraContext )
throws Exception {
return ObjectFactory.getObjectFactory().buildBean(beanName,extraContext);
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace, Map actionContext)
throws Exception {
return createActionProxy(actionName,actionNamespace,actionContext,new HashMap());
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @param sessionMap the request/invocation session map (for http session map mocking)
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace, Map actionContext, Map sessionMap )
throws Exception {
ActionProxy actionProxy = ActionProxyFactory.getFactory().createActionProxy(_configurationManager.getConfiguration(),actionNamespace,actionName,actionContext);

// set the session map in the action proxy's invocation
actionProxy.getInvocation().getInvocationContext().setSession(sessionMap);

return actionProxy;
}

/**
* create an action object, bypass all it's stacks. Have it properly injected
* according to configurations.
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @return the properly injected action
* @throws Exception on processing, configuration errors, test failure
*/
public Object createAction ( String actionName, String actionNamespace, Map actionContext )
throws Exception {
// get ahold of the action's configuration via the XWorkConfigRetriever class
ActionConfig actionConfig = _configurationManager.getConfiguration().getRuntimeConfiguration().getActionConfig(actionNamespace,actionName);

// create one instance of the action to test using the object factory, pass in action config and context
return ObjectFactory.getObjectFactory().buildAction(actionName, actionNamespace, actionConfig, actionContext);
}



And using it to create any bean:


MyClass myObject = (MyClass)StrutsTestCaseSupport.getInstance().createBean("myBeanName",new HashMap());


Or to do a full fledged Struts 2.0 action proxy test:


// create action for ActionSearchTest
Map requestParameters = new HashMap();
requestParameters.put("searchMode","quick");
requestParameters.put("searchText","Testing");
Map actionContext = StrutsTestCaseSupport.getInstance().buildActionContext("struts.assisrosa.com","get","/search/results",requestParameters);

// create the proxy for the action, this encapsulates all
// the interception stack up to the real action
ActionProxy proxy = StrutsTestCaseSupport.getInstance().createActionProxy("results","/search",actionContext);

// let the full stack run
String result = proxy.execute();

// confirm result, any exception thrown will cause test to fail
assert result.equals("success");



Or a Struts 2.0 action test (no proxy in front of it):


MyAction myAction = (MyAction) StrutsTestCaseSupport.getInstance().createAction("hello","/site",actionContext);
String result = myAction.execute();
assert result.equals("myExpectedResult");