When writing webapplications you should, as you also would for any other project, test your code that it works as expected. In our project we are using Tomcat 5.5, Apache struts 2.0.6 and Apache tiles 2.0.1. and I was given the task to write unit tests for our action classes. Testing Actions is a bit more difficult than ordinary classes as they are usually surrounded by a web container which handles them. There is a struts FAQ explaining the principles of struts testing. However you cannot use that example as it is and it leaves out a few important things. So, I started by taking a good part of the code from this how-to (Arsenalist) and continued from there. Here is how I managed to get it rolling.

As you can read in the struts FAQ there are 2 ways to test actions.

  • Instantiate your action and call its methods as you would with any POJO
  • Create a test container that simulates your usual environment and invoke your action as that would do it.

The first approach is simple and does not require much work. Just create your JUnit test as you would do otherwise (in Eclipse there is a wizard  so it is a matter of a few seconds to do that). Once you have your action instance call its methods and check the result of your execute() method (or whatever you configured to run on invocation). The second approach extends the first by adding the test container and invoking the action via a proxy that also will go through the whole chain of filters and interceptors which you have configured. The actual setup is hereby two-fold:

  • Create the test container and configure it
  • Create the action and its proxy

In order to set up the test container you'll need the spring framework (as it provides the mock objects needed). You can get it from the Apache site. Don't forget to also get the struts2-spring-plugin-2.0.8.jar, which is needed to register spring with struts. Add the following files to your build path :

  • spring.jar
  • spring-mock.jar
  • struts2-spring-plugin-2.0.8.jar

To simplify test case creation it is obvious to create an action base test class and derive your other junit test classes from it. Here's what I came up with:

Setup

... 
 
import junit.framework.TestCase;

import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.spring.StrutsSpringObjectFactory;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;

import com.mysql.etools.ApplicationFactory;
import com.mysql.merlin.UITestFakeService;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ActionProxyFactory;
import com.opensymphony.xwork2.ObjectFactory;

public abstract class ActionBaseTestCase extends TestCase {

    /**
     * This holds the last created proxy object. If a test needs more than one proxy (e.g. for
     * several actions) then it must cache this value. Since this isn't the case very frequently and
     * each action can only executed once there is no meaning in creating a central caching solution
     * here.
     */
    protected ActionProxy proxy = null;

    protected Dispatcher dispatcher;

    protected Map<String, Object> sessionMap;

    protected MockServletContext servletContext;

    protected MockHttpServletRequest request;

    protected MockHttpServletResponse response;

    ...

    /**
     * Created action class based on namespace and name.
     */
    @SuppressWarnings("unchecked")
    protected <T> T createAction(final Class<T> clazz, final String namespace, final String name)
        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.
        proxy = dispatcher.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
            namespace, name, nulltruefalse);

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

        // Set the actions context to the one which the proxy is using.
        ActionContext.setContext(proxy.getInvocation().getInvocationContext());
        request = new MockHttpServletRequest();

        request.setServletPath(namespace + "/" + name + ".action");
        response = new MockHttpServletResponse();
        ServletActionContext.setRequest(request);
        ServletActionContext.setResponse(response);
        ServletActionContext.setServletContext(servletContext);

        if (proxy.getAction().getClass().equals(clazz)) {
            final BaseAction action = (BaseAction) proxy.getAction();
            action.setSession(sessionMap);
            return (T) action;
        }
        return null;
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        System.setProperty("java.awt.headless""true");

        sessionMap = new HashMap<String, Object>();

        // You might want to set a user here if you have an authentication
        // interceptor for your web app.

        // Struts setup:
        final String[] config = new String[] { "struts.xml" };

        // Link the servlet context and the Spring context
        servletContext = new MockServletContext();
        final 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
        final StrutsSpringObjectFactory ssf = new StrutsSpringObjectFactory(nullnull,
            servletContext);
        ssf.setApplicationContext(appContext);
        ObjectFactory.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<String, String>());
        dispatcher.init();
        Dispatcher.setInstance(dispatcher);
    }
}

(btw. sorry for the incomplete syntax highlighting, I have yet to find a good code export tool that works with Joomla)

What happens in setUp is:

  • Tell the unit test to to not create a visible window
  • Create a session map that gets values to keep during an entire session
  • Create servlet and application context and point it to your struts config file (in my case struts.xml, which is in the classpath, so no path is needed here)
  • Create an object factory (using the spring framwork for that) and connect it to the application context
  • And finally create a dispatcher for the requests and connect it to our context

The dispatcher is later also used to create action proxies, which happens in method createAction . This method also creates a mock http request and sets up other classes needed in the context for this new action.

Usage

Using this code is very simple. Derive your test cases from this base test case class and create your action in the setUp() method like this:

... 

public class MyActionTest extends ActionBaseTestCase {

    private MyAction acton;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        action = createAction(MyAction.class, "", "MyAction");
       ...

    }

Later in the tests do not call

     action.execute()

but

    proxy.execute()

which will then call the proper action method (either execute() or whatever you configured in your struts.xml). The variable "proxy" is defined in the base test class and is set when you create your action.

It took me quite a while to collect all the pieces for this solution and I'm happy it works so beautifully. What remains is a proper setup for tiles. Currently I cannot test actions that involve a tiles type in an action result. This has to be left for a later article. If anyone of you can give me a hint to make tiles work, I'd much appreciate it.

 
[to be continued...]