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

Wednesday, November 15, 2006

iPodaholic

Last year I dove into the iPod addiction...Maybe not very typically, I really did not get into it because of music but because of technical podcasts. Music I can do in a number of other ways but the amount of technical podcasts that are out there for free is just overwhelming and it can be a fantastic source for free training and technical news feed. I have not regretted diving into it yet and am currently in a iPodaholic state. This is a fantastic way to make your commute useful and pleasurable. Ironically I now think my commute is just too short (got me an iTrip FM transmitter to hook it up to my car radio). :-) Podcast shows are getting more and more interesting and show durations are getting longer with deeper more involved content coming in. To any developer these days I say that listening to podcasts is now an essential way to keep up to date, in addition to book reading, playing with cool stuff after-hours, magazines and blogs (and I wonder where time goes!?!!?). I should also be saying that podcasting is not only about technical podcasts.....there is pretty much a podcast for any thing you might think of.

Anyway, these are the must-have-on-my-iPod podcasts:

  • The Java Posse: in my opinion, the best podcast out there on Java development. These guys do news info update, analysis of tools, overall software development discussions. Absolutely top of my list.
  • Software as She Developed: pretty interesting podcast on software development. The author Michael Mahemoff is the author of the Ajax Design Patterns book so you can expect this podcast to bring in a good amount of that experience.
  • Audible Ajax: name says all, a really good podcast on Ajax.
  • Software Engineering Radio: good presentations on software engineering in general. Covering topics like agile development, SOA, development processes, etc.
  • Javapolis: one of the best Java conferences around. You can find podcast feeds for some of the presentations in here. A free way to "be" at the conference.
  • TalkCrunch: a really interesting podcast on web 2.0 companies.
  • Venture Voice: talks and discussions on entrepreneurship.
  • PodTech's Entrepreneurship Podcast: talks and discussions on entrepeneurship.
If you have not tried the podcasting listening experience I would say you need to try it! You can try it before you buy your player just by getting iTunes for instance...

And...if you have any other interesting tech podcast please drop me a line! I want more! :-)