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:
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:
Action myAction = getAction("myActionUrl");
String actionResult = myAction.execute();
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:
With this support class, I can now write my tests:
/**
* 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
// create action context for my action, feedOr test unproxyed actions directly:
// into the action context all request parameters
MaprequestParameters = 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;
// create action for my actionSo 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... ;-)
MaprequestParameters = 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");