Sunday, November 19, 2006

Unit Testing Struts 2.0


Edit: for latest on this see recent post.


Unit testing is now (or should be) an established step of the development process in any project. If you're not writing unit tests, you are pretty much leaving yourself ready to commit errors over and over again. Granted there is a category of testing other than unit testing that can be put in place to give you that safety net (see WebTst ;-) ) but unit testing has it's well deserved place in the must do list.

Testing however will only happen, let's face it, if it becomes dead easy (or close to that) for developers to write tests. Crunch time has the tendency to make developers drop their testing efforts and if putting them up is in any way hard or cumbersome, it will not happen.

So I started looking at simplifying unit testing for Struts 2.0 (recently merged from WebWork). This is, IMHO, a pretty smart and elegant web framework (topic for another post maybe) that if you have not seen, should take a look at. I am using TestNG for my testing framework (again, a topic for another post maybe, again a pretty smart framework). One of the selling points of WebWork and Struts 2.0 was the idea that testing your actions should be pretty simple due to the nature of the framework. Dependency injection would be a good step to achieve simplicity in testing and allow you to detach yourself from the need of a servlet container to run your tests.

And so I dived into trying to add unit testing to my Struts 2.0 actions. Here is what I would like to do ideally to test my actions purely:


Action myAction = getAction("myActionUrl");
String actionResult = myAction.execute();
and to test my actions with the interceptor chain in front of them, something which I believe should be pretty important to test in the context of Struts 2.0:


ActionProxy myActionProxy = getActionProxy("myActionUrl");
String result = myActionProxy.execute();


I would then like to do testing on results coming out for execution of the actions. Both testing on result strings and testing on HTML returned in the case of the action proxy where we can get access to the fully processed response. Ideally I would like to make it as simple as above, could make it a bit more involved in some cases...but it should always be dead-easy to write a test. So the answer (to me) is a support class to help with writing unit tests. Ready for code dump ? Here it goes, snipped in the non-relevant aspects. Class implements the singleton pattern and relevant methods for Struts testing are:


/**
* 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
* @returns 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
* @returns the map for the action's context
*/
public Map buildActionContext ( String serverName, String accessMethod, String accessUrl, Map requestParamMap ) {
// 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
for ( String oneParamName : requestParamMap.keySet() ) {
request.addParameter(oneParamName,requestParamMap.get(oneParamName));
}

// 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.PARAMETERS,new HashMap());
actionContext.put(ActionContext.DEV_MODE,new Boolean(true));

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
* @returns the object factory created bean
*/
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)
* @returns the proxyed action
*/
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)
* @returns the proxyed action
*/
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)
* @returns the properly injected action
*/
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);
}

With this support class, I can now write my tests:
       // create action context for my action, feed
// into the action context all request parameters
Map requestParameters = new HashMap();
requestParameters.put("param1","param1-value");
requestParameters.put("param2","param2-value");
Map actionContext = StrutsTestCaseSupport.getInstance().buildActionContext("my.hostname.com","get","/myActionNamespace/myActionName",requestParameters);

// create the proxy for the action, this encapsulates all
// the interception stack up to the real action
ActionProxy proxy = StrutsTestCaseSupport.getInstance().createActionProxy("myActionName","myActionNameSpace",actionContext);
// if needed be, get ahold of particular action underlying proxy and
// inject parameters as required

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

// confirm result
assert result.equals("myTestResponseString");

// look into mock HttpServletResponse, do whatever
// tests I need to do: returned HTML, returned headers,
// cookies, etc...
String responseXml = ((MockHttpServletResponse)actionContext.get(StrutsStatics.HTTP_RESPONSE)).getContentAsString();
assert responseXml.indexOf("success") != -1;
Or test unproxyed actions directly:
       // create action for my action
Map requestParameters = new HashMap();
requestParameters.put("param1","param1-value");
requestParameters.put("param2","param2-value");
Map actionContext = StrutsTestCaseSupport.getInstance().buildActionContext("my.hostname.com","get","/myActionNamespace/myActionName",requestParameters);

// create the proxy for the action, this encapsulates all
// the interception stack up to the real action
Action myAction = StrutsTestCaseSupport.getInstance().createAction("myActionName","myActionNameSpace",actionContext);
// if needed be, get ahold of particular action underlying proxy and
// inject parameters as required

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

// confirm result
assert result.equals("myTestResponseString");
So testing became *a lot* simpler to me...my support class deals with all infrastructure hooking up and my test is simplified...and more tests get written... ;-)

9 comments:

JohnJ said...

This is exactly what I'm looking for does this exist today? Is this considered in-container testing (like Cactus).

Is there something similar that we can use with older versions of webwork (not the struts 2 version)?

Francisco Assis Rosa said...

Hi John,

The code in this post was original written for WebWork 2.2.X. Then, once Struts 2.0 came out, adapted to Struts 2.0. The backwards conversion from this code to WebWork 2.2.X should be straightforward...mainly a question of translating locations from Struts 2.0 to WebWork.

The code in the post is in production and serving me happily (have actually added today a couple of new unit tests in a couple of minutes)... :-)

Although closer to Cactus, this approach does allow you, to some extent, to test the returned values out of your server (e.g. headers, HTML). All information can be extracted from the results. Notice the example does extract results from the HttpServletResponse object for analysis.

As for functional testing, this approach can work, but probably is not the most appropriate, requiring some time investment in test building. For functional testing I have used WebTst (http://webtst.sourceforge.net/). This allows me to quickly capture complex interactions, allows me to run those same tests as part of a test suites run automatically periodically (e.g. nightly) to do a full regression test run of your functionality. Finally, I have used WebTst as well hooked-up to ant as part of the deployment process as means to run a series of functional smoke tests.

I hope I answered your questions...

Anonymous said...

could you post your imports?

thanks!

colin

rodmclaughlin said...

Thanks for this code, but I wonder if you could post the import statements. I can't find versions of Settings and ObjectFactory that have the methods you call in this code.
org.apache.struts2.config.Settings
is private, for example.

Francisco Assis Rosa said...

Hi there,
Thanks for your comment. In response, please check out Unit Testing Struts 2.0 (Part 2). I hope this gets you what you need. Let me know if I can be of further help.

Anonymous said...

Have you gotten this code to work when Spring is the default struts object factory? Using spring bean ids for the action class seems to fail. And even forcing StrutsSpringObjectFactory as the objectFactory fails since a new object factory (not the spring one) is created every time. I'd love to get around using no-arg constructors with ClassPathXmlApplicationContext...getBean calls. Any help would be much appreciated. Thanks Francisco!

Francisco Assis Rosa said...

Have you looked at http://fassisrosa.blogspot.com/2007/03/unit-testing-struts-20-part-2.html ? That is the code I am using currently. All bean definitions come from the Spring descriptor applicationContext.xml file...

You can do something as simple as:

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

Anonymous said...

What do you mean? The return value of createAction() is the action class. Whatever public methods are available in your action are available during the tests. Just have a method which returns a model object in your action..

Francisco Assis Rosa said...

You are correct 'shocks and struts'. You can indeed get the action object out of the createAction() method. And in your action you can expose whatever methods you might need. There are however some cases where you will want to access in your unit tests some Spring managed beans. Beans that are not necessarily hooked up to your action. For those cases you can use the createBean method...