Tuesday, September 11, 2007

Unit Testing Struts 2.0 (Part 3)

Like Dan commented on the Unit Testing Struts 2.0 (Part 2) post, Struts 2.0 has changed it's API enough to make my previous code not work on latest version. So, in response to his comment, here is what I am using right now. Credit where it's due, the setup code is the one that The Arsenalist pointed out on his blog post Unit Testing Struts 2 Actions wired with Spring using JUnit. Again, credit where it's due...my thanks go to "The Arsenalist" for posting his solution.


/**
* 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).
*
* Adapted from code from "The Arsenalist" (http://arsenalist.com/),
* see http://arsenalist.com/2007/06/18/unit-testing-struts-2-actions-spring-junit/
*/
public class StrutsTestCaseSupport {

/**
* Singleton variable
*/
public static StrutsTestCaseSupport _theInstance = null;

/**
* Servlet context
*/
private ServletContext servletContext = null;

/**
* Request dispatcher
*/
private Dispatcher dispatcher = null;

/**
* Singleton access
*/
public static synchronized StrutsTestCaseSupport getInstance()
throws Exception {
if ( _theInstance == null ) {
_theInstance = new StrutsTestCaseSupport();
}
return _theInstance;
}

/**
* Class constructor, take care of Struts initializations
*/
private StrutsTestCaseSupport ()
throws Exception {
String[] config = new String[] { "/WEB-INF/applicationContext.xml" };

// Link the servlet context and the Spring context
servletContext = new MockServletContext(new FileSystemResourceLoader());
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setServletContext(servletContext);
appContext.setConfigLocations(config);
appContext.refresh();
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, appContext);

// Use spring as the object factory for Struts
StrutsSpringObjectFactory ssf = new StrutsSpringObjectFactory(null, null, servletContext);
ssf.setApplicationContext(appContext);
StrutsSpringObjectFactory.setObjectFactory(ssf);

// Dispatcher is the guy that actually handles all requests. Pass in
// an empty Map as the parameters but if you want to change stuff like
// what config files to read, you need to specify them here
// (see Dispatcher's source code)
dispatcher = new Dispatcher(servletContext, new HashMap());
dispatcher.init();
Dispatcher.setInstance(dispatcher);
}

/**
* 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 {
ObjectFactory objectFactory = dispatcher.getContainer().getInstance(ObjectFactory.class);
return objectFactory.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
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace )
throws Exception {
// create a proxy class which is just a wrapper around the action call.
// The proxy is created by checking the namespace and name against the
// struts.xml configuration
ActionProxy proxy = dispatcher.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(actionNamespace, actionName, null, true, false);

// by default, don't pass in any request parameters
proxy.getInvocation().getInvocationContext().setParameters(new HashMap());

// by default, pass along an empty session map
proxy.getInvocation().getInvocationContext().setSession(new HashMap());

// set the actions context to the one which the proxy is using
ServletActionContext.setContext(proxy.getInvocation().getInvocationContext());
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletActionContext.setRequest(request);
ServletActionContext.setResponse(response);
ServletActionContext.setServletContext(servletContext);

// set proper URI
request.setRequestURI(actionNamespace + "/" + actionName);

return proxy;
}

/**
* 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 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 sessionMap )
throws Exception {

// create an action proxy as usual
ActionProxy actionProxy = createActionProxy(actionName,actionNamespace);

// 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
* @return the properly injected action
* @throws Exception on processing, configuration errors, test failure
*/
public Object createAction ( String actionName, String actionNamespace )
throws Exception {
ActionProxy actionProxy = createActionProxy(actionName,actionNamespace);
return actionProxy.getAction();
}

/**
* Access to the proxy's response content as a string
* @param proxy the proxy to get the string response for
* @return the proxy's response content as a string
*/
public static String getResponseContentAsString ( ActionProxy proxy )
throws java.io.UnsupportedEncodingException {
return ((MockHttpServletResponse)ServletActionContext.getResponse()).getContentAsString();
}

/**
* Set a hostname in proxy request
* @param proxy the proxy to set hostname to
* @param serverName the server name to set for this proxy's call
*/
public static void setRequestServerName ( ActionProxy proxy, String serverName ) {
((MockHttpServletRequest)ServletActionContext.getRequest()).setServerName(serverName);
}
}


Like mentioned in previous postings, using this class is pretty straightforward, within your test you just do for beans:


YourClass yourInstance = (YourClass)StrutsTestCaseSupport.getInstance().createBean("yourBeanId",new HashMap());


For Actions:

ActionProxy proxy = StrutsTestCaseSupport.getInstance().createActionProxy(yourActionId,yourContextPath);
MyActionClass myActionInstance = (MyActionClass)StrutsTestCaseSupport.getInstance().createAction(yourActionId,yourContextPath);


This code is currently being used with Struts 2.0.8.

19 comments:

Bui said...

Thanks for your post.
But I still cannot run test.
I have two questions, could you please answer for me? Thanks in advance.

1. My test class should extend "TestCase" or it's just a normal java program?

2. I created my test class extends TestCase ( JUnit) and it said that could not find : /WEB-INF/applicationContext.xml

Thank you very much!

Francisco Assis Rosa said...

Hi Bui,

Answers to your questions:

1. The Struts test support is oblivious of your testing infrastructure. If you're using JUnit you will have to follow JUnit's test building procedures (derive from TestCase), I actually use TestNG and am not required to derive from any other class, my test class is a regular POJO.

2. Fair enough...this assumes two things: a) /WEB-INF/applicationContext.xml is your Spring bean definition file. You might need to change to reflect your bean definition file. b) You can find WEB-INF/applicationContext.xml either in your classpath or under your test run working directory.

I hope this helps.

Dan said...

The getResponseContentAsString() always returns an empty string for me. I've tried to verify that the MockServletContext with the correct resourceBasePath and a FileSystemResourceLoader, but I'm convinced that it doesn't actually do anything to compile and run jsps. Is there something I'm missing?

Hayden Steep said...

Thanks for the very useful example. I'm very grateful. Here's a fairly minor bug in the example:

createActionProxy ( String actionName, String actionNamespace, Map sessionMap)

This calls:
createActionProxy ( String actionName, String actionNamespace )

However, when it calls the 2 parameter version, the parameters are reversed:
// create an action proxy as usual
ActionProxy actionProxy = createActionProxy(actionName, actionNamespace);

Thanks again.

Hayden Steep said...

I accidentally copied and pasted my 'fixed' version. But the point is still valid :)

Francisco Assis Rosa said...

Thanks Hayden! Corrected the example...great catch! :-)

Shawn said...

I am using maven and all my config files are under my resource directory. How should I list them?

Kent said...

I am getting the following error when attempting to JUnit test my actions. So what am I doing wrong in how I use StrutsTestCaseSupport? -
There is no Action mapped for namespace /login.html and action name loginAction. - [unknown location]
at com.opensymphony.xwork2.DefaultActionProxy.prepare(DefaultActionProxy.java:186)
at org.apache.struts2.impl.StrutsActionProxyFactory.createActionProxy(StrutsActionProxyFactory.java:41)
at com.mycompany.sales.action.StrutsTestCaseSupport.createActionProxy(StrutsTestCaseSupport.java:132)
at com.mycompany.sales.action.StrutsTestCaseSupport.createAction(StrutsTestCaseSupport.java:186)
at com.mycompany.core.action.LoginActionTest.testList(LoginActionTest.java:57)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at
...

Here is the example spring bean config -
applicationContext.xml -
bean id="loginAction" scope="prototype"
class="com.mycompany.core.action.LoginAction">
property name="userService" ref="userService"

Here is the example Struts.xml -
constant name="struts.devMode" value="false"
constant name="struts.i18n.encoding" value="UTF-8"
constant name="struts.action.extension" value="html"
constant name="struts.objectFactory" value="spring"
constant name="struts.custom.i18n.resources" value="ApplicationResources,errors"/>
constant name="struts.multipart.maxSize" value="2097152"
constant name="struts.enable.SlashesInActionNames" value="true"
package name="sales" extends="struts-default"
action name="login" class="com.mycompany.core.action.LoginAction">
result /login.jsp result
/action
...

LoginActionTest.java -
public class LoginActionTest extends TestCase {
...
LoginAction action = (LoginAction)StrutsTestCaseSupport.getInstance().
createAction("loginAction","/login.html");

Francisco Assis Rosa said...

Hi Kent,

Try the following instead:

LoginAction action = (LoginAction) StrutsTestCaseSupport.getInstance().createAction("login","/");

Remember that 'createAction' first parameter is the action name, second parameter is the namespace for the action.

The trick here is that your struts.xml file defined the 'login' action within the 'sales' package. 'loginAction' is really not an action you can find defined in your struts.xml (the 'action name=...' part), and '/login.html' would not be a valid namespace (according to your struts.xml)...

Hope that helps...

web said...
This comment has been removed by a blog administrator.
aplsyn5 said...

Hello,

I found your article extremely useful. I joined a project recently which has been using Struts 1.1 and wanted to find out if its possible to use Spring's unit testing functionality with Struts 1.1.

Thanks for your help.

Francisco Assis Rosa said...

Been helped too many times to count by people on the web posting experiences, problems, solutions. Glad to be able to pay it back by helping someone back. :-)

Anonymous said...

Thanks. This example worked for me and I can test my beans.
(After changing "/WEB-INF/applicationContext.xml" to "classpath*:/WEB-INF/applicationContext.xml")
But I have an EntityManager in my bean, who is injected by @PersistenceContext.
And this injection doesn't work, when I run the JUnit tests.

Does anyone have an idea, why the Annotation doesn't work?

Devin said...

I really want to get this running but am having a lot of trouble. Here are the errors when I try to build it:

org.apache.struts2.config.Settings is not public in org.apache.struts2.config; cannot be accessed from outside package
[javac] import org.apache.struts2.config.Settings;

package org.springframework.mock.web does not exist
[javac] import org.springframework.mock.web.MockServletContext;


package org.springframework.mock.web does not exist
[javac] import org.springframework.mock.web.MockHttpServletResponse;

[javac] symbol : class MockServletContext
[javac] location: class org.apache.struts2.StrutsTestCaseSupport
[javac] servletContext = new MockServletContext(new FileSystemResourceLoader());

There are a bunch more but I think they are all related to the above errors.

Is it my version of struts? Here are some of the struts libraries I have:
strutstest-2.1.4.jar
struts2-core-2.0.11.jar

Thanks for any help.

Devin said...

So I figured out the first few errors for the missing org.springframework.mock.web stuff.

But I am still having issues with this:

StrutsTestCaseSupport.java:11: org.apache.struts2.config.Settings is not public in org.apache.struts2.config; cannot be accessed from outside package
[javac] import org.apache.struts2.config.Settings;

Do I need something else? A special library?

Thanks.

seo.media said...
This comment has been removed by a blog administrator.
bhup1980 said...

Hi Francisco Assis Rosa

Thanks a lot for the post.
I have issue with bean scope.

I am using struts SessionAware classes and I have to do the offline testing of my webapplication.

When I do the testing by using this example. The testing halts due to the no session or request thread bound as error suggested.

Please suggest solution for the same, what could be fault in the code.

---------WEB.xml----------------
**context-param**
*parama* pplicationContext.xml *param*
**context-param**

**no filters**

**listener**
*listener-class*
org.springframework.web.context.request.RequestContextListener
*listener-class*
**listener**
----------------End Web.xml -------


---------------applicationContext.xml---------
Contains all DAO bean
------end applicationContext.xml---


------code------------------------
public static void main(String args[]) throws Throwable {
try {

String executionPath = System.getProperty("user.dir");
System.out.println(executionPath);
System.out.print("Executing at =>"+executionPath.replace("\\", "/"));

System.out.println(getClassLocation());
LoginAction loginAction = (LoginAction) StrutsTestCaseSupport.getInstance()
.createAction("Login", "/");
String results = loginAction.execute();
System.out.println("Result :" + results);

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();

}

}
--------------end code---------



---------error ------------------
Unable to instantiate Action, jp.co.nec.nhm.sm.web.action.LoginAction, defined for 'Login' in namespace '/'No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. - action - file:/E:/eclipse/eclipseGN/workspace/1117SystemManager/target/classes/struts.xml:151:72
at com.opensymphony.xwork2.DefaultActionInvocation.createAction(DefaultActionInvocation.java:294)
at com.opensymphony.xwork2.DefaultActionInvocation.init(DefaultActionInvocation.java:365)
at com.opensymphony.xwork2.DefaultActionInvocation.access$000(DefaultActionInvocation.java:38)
at com.opensymphony.xwork2.DefaultActionInvocation$1.doProfiling(DefaultActionInvocation.java:83)
at com.opensymphony.xwork2.util.profiling.UtilTimerStack.profile(UtilTimerStack.java:455)
at com.opensymphony.xwork2.DefaultActionInvocation.*init*(DefaultActionInvocation.java:74)
at com.opensymphony.xwork2.DefaultActionProxy.prepare(DefaultActionProxy.java:189)
at org.apache.struts2.impl.StrutsActionProxyFactory.createActionProxy(StrutsActionProxyFactory.java:41)
at test.StrutsTestCaseSupport.createActionProxy(StrutsTestCaseSupport.java:122)
at test.StrutsTestCaseSupport.createAction(StrutsTestCaseSupport.java:183)
at test.SpringTest.main(SpringTest.java:22)

bhup1980 said...

Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:121)
at org.springframework.web.context.support.WebApplicationContextUtils$1.getObject(WebApplicationContextUtils.java:113)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:660)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:611)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireByType(AbstractAutowireCapableBeanFactory.java:1039)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:950)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireBeanProperties(AbstractAutowireCapableBeanFactory.java:325)
at com.opensymphony.xwork2.spring.SpringObjectFactory.autoWireBean(SpringObjectFactory.java:167)
at com.opensymphony.xwork2.spring.SpringObjectFactory.buildBean(SpringObjectFactory.java:154)
at com.opensymphony.xwork2.spring.SpringObjectFactory.buildBean(SpringObjectFactory.java:128)
at com.opensymphony.xwork2.ObjectFactory.buildBean(ObjectFactory.java:143)
at com.opensymphony.xwork2.ObjectFactory.buildAction(ObjectFactory.java:113)
at com.opensymphony.xwork2.DefaultActionInvocation.createAction(DefaultActionInvocation.java:275)
... 10 more

-------end error----------------

adler said...

Hi
I am trying to use your code with struts spring plugin 2.1.8.jar and struts2 2.1.6.jar but I am having problems with StrutsSpringObjectFactory
I does not compile, the
StrutsSpringObjectFactory objectFactory = new StrutsSpringObjectFactory();

Other compilation errors too. Please can you help how to resolve?