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");

5 comments:

Dan Bradley said...

It seems that the Struts API has changed a bit since you wrote this. The code will not compile against Struts 2.0.9. (Specifically, many of the constructor arguments appear to have changed, to Dispatcher, StrutsXmlConfigurationProvider, others.)

Any chance you have updated this to work with the latest Struts?

Francisco Assis Rosa said...

Hi Dan,

Thanks for your comment. You are absolutely right and I have now been using a new version. You can find the new version on the blog here.

Not enough to repeat that the code I am currently using came originally from a posting at The Arsenalist and can be found here.

I hope this helps.

Francisco Assis Rosa said...

I should probably add I am currently using Struts 2.0.8.

Anonymous said...

Thank you so much! most of this was so helpful to me!!

Cheers,
Andy

Thanks again.

Anonymous said...

It is certainly interesting for me to read this blog. Thank author for it. I like such topics and anything that is connected to them. I definitely want to read more soon.
Alex
Phone jammer