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.
/** * 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
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):
Continuous integration is a practice introduced by Extreme Programming (XP) that brings in the idea that developers should check in their code often and that their work should be continuously integrated and tested to ensure that no error goes unnoticed (see article by Martin Fowler). The practice is tightly integrated with the concepts of source control, unit testing and automated building as these are the privileged means of bringing the code together and running tests on it. Continuous integration (CI) not only works nicely but does enforces some pretty important habits to developers.
Developers working on a CI project will be required to use source control for their code. Sounds like a basic tool for any development project but I've seen too many projects where source control is completely absent...CI simply requires it...nice.
Developers working on a CI project will be rewarded by putting in place as many unit tests as possible. How ? By seeing that less errors will sneak by because of the existence of this safety net of testing. It is pretty cool to see that you avoid putting mistakes into production because of this first line of defense. Better our CI screaming at us than our clients right ?
Developers working on a CI project get used to the concept of automated building and the concept of building from scratch. Again, often have I seen in the past cases where code that is not rebuilt regularly from scratch becomes too entangled in dependencies that prevent it from building from a clean slate.
All these make for pretty strong points in favor of CI. And it is not that hard to put it in place...There is a significant amount of tools that joined together make for a great CI platform. Just look around. A winning combination for me, developing in Java, has been:
Cruise Control. A CI framework that glues together all the components to provide a pretty decent CI setup. From HTML reporting to email notification of success failures of builds, this is a pretty cool tool to use.
Subversion. A version control system that addresses a lot of the typical weak spots of other version control systems (e.g. CVS).
Ant. An automated build tool for Java. I doubt anyone working in Java never heard of Ant.
TestNG. A really cool testing framework for Java. Check it out...
All of the above are free tools that you can get and play with, together they make up for a pretty strong CI.
CI takes a step ahead when talking about Continuous Database Integration (CDBI). Paul Duvall at Test Early has been doing some pretty interesting presentations on it. It does bring a new level of testing to your database-driven apps. One which enforces the cooperation between developers and database administrators and builds a structure that ensures that you can deploy your application at any point in time. If you ever seen a project where to redeploy in a new system you have to go chasing for the DB schema required to deploy a clean system, you know the kind of sorrows CDBI can save you from.
I simply *guarantee you* that if you ever start using CI, you will not want to work again without it.
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:
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 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... ;-)