When I wrote the first part of this mini article series I left open a few things, mainly the Apache Tiles setup. In the meantime this problem also has been solved and I'm now able to give you nice little Java class, which contains everything that is necessary for Action Unit Testing with Apache Struts and Tiles.

A major problem for me was that every action that had a tiles forwarder as result handler caused a test failure once it was completed. Part of that problem is that Apache Tiles support is configured in web.xml rather than struts.xml. This is a problem because web.xml is a configuration file read by Tomcat not your application. Adding support for tiles can be done in different ways. We use a tiles listener for that and simply added

    <listener>
        <listener-class>org.apache.struts2.tiles.StrutsTilesListener</listener-class>
    </listener>

to our web.xml file.  Nonetheless this setting is not used by the Unit Test setup as it does not consider web.xml at all. So what can we do to make it work? To answer this question I had to dive deeply into the source code of Apache Tiles, Apache Struts and even sometimes in Tomcat's code (one reason more why open source software is so great, try that with proprietary stuff). In the end I found out how to manually instantiate a tiles listener. Another point was the tiles configuration file. The one for struts (in our case struts.xml) is found autmatically, as it is in the class path, but tiles.xml simply was ignored. I got around this problem by using a file system resource loader and specified the paths to both struts.xml and tiles.xml. You have of course to adjust these pathes and the configuration file names to your environment.

Another big problem I had with that solution was that each test took around 1-2 seconds just for its setup (all the struts and tiles setup happend again and again). The tests themselves were very quick. To speed this up I moved the entire test container stuff to static storage and could improve the execution speed so by around factor 40. The whole test suite (currently consisting of ~200 tests) now executes in (handstopped) 5 seconds (a bit more than 2 used for setup). Nice!

Now to the code itself.  Below is what I came up with at the end. It should compile and work nicely for you. However, I have removed anything that was specific to my environment so it might be that something is wrong. Let me know if you find odditiies or if you have a more clever idea than me to make all that work.

package ... your package name here ...;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContextEvent;

import junit.framework.TestCase;

import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.spring.StrutsSpringObjectFactory;
import org.apache.struts2.tiles.StrutsTilesListener;
import org.apache.tiles.impl.BasicTilesContainer;
import org.springframework.core.io.FileSystemResourceLoader;
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.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 MockHttpServletRequest request;

    protected MockHttpServletResponse response;

    private static Map<String, Object> sessionMap;

    protected static Dispatcher dispatcher;

    protected static MockServletContext servletContext;

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

        // Create fake back end here.
        // ...

        // ===== Struts setup:
        // Create and use a file system resource loader otherwise Tiles will not find
        // our configuration file. The default resource loader is able to find struts.xml
        // if it is in the classpath, but not tiles.xml.
        final FileSystemResourceLoader loader = new FileSystemResourceLoader();

        final String[] config = new String[] { "WEB-INF/classes/struts.xml" };

        servletContext = new MockServletContext(loader);
        final XmlWebApplicationContext appContext = new XmlWebApplicationContext();

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

        servletContext
            .addInitParameter(BasicTilesContainer.DEFINITIONS_CONFIG, "WEB-INF/tiles.xml");

        // Creating the tiles listener statically (not via class loader).
        final StrutsTilesListener tilesListener = new StrutsTilesListener();
        final ServletContextEvent event = new ServletContextEvent(servletContext);
        tilesListener.contextInitialized(event);

        // Use spring as the object factory for Struts
        final StrutsSpringObjectFactory ssf = new StrutsSpringObjectFactory("auto""true",
            servletContext);
        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);
    }

    /**
     * Helper method to avoid direct access to the session map.
     
     @return The number of entries currently stored in the session map.
     */
    protected static int getSessionMapSize() {
        return sessionMap.size();
    }

    /**
     * Returns the object that is stored under the given name in the session.
     
     @param name
     *            The name of the entry to be returned.
     @return The object stored under the given name, if there is one, or null if not.
     */
    protected static Object getSessionValue(final String name) {
        if (sessionMap.containsKey(name)) {
            return sessionMap.get(name);
        }
        return null;
    }

    /**
     * Tells the caller if a certain value exists in the session map.
     
     @param name
     *            The name of the value to look for.
     @return <code>true</code> if the value is there, otherwise <code>false</code>.
     */
    protected static boolean sessionContains(final String name) {
        return sessionMap.containsKey(name);
    }

    /**
     * Writes the given value into the session map.
     
     @param name
     *            The name under which to store the value.
     @param value
     *            The value to store in the map.
     */
    protected static void setSessionValue(final String name, final Object value) {
        sessionMap.put(name, value);
    }

    /**
     * Created action class based on namespace and name.
     
     @param clazz
     *            The class type to return.
     @param namespace
     *            The namespace in the application where the action lives.
     @param name
     *            The name of the action (for lookup in struts.xml).
     */
    @SuppressWarnings("unchecked")
    protected <T> T createAction(final Class<T> clazz, final String namespace, final String name)
        throws Exception {
        return createAction(clazz, namespace, name, null);
    }

    /**
     * Created action class based on namespace and name.
     
     @param clazz
     *            The class type to return.
     @param namespace
     *            The namespace in the application where the action lives.
     @param name
     *            The name of the action (for lookup in struts.xml).
     @param methodName
     *            The method to be executed on this action. Leave <code>null</code> for default
     *            method (i.e. execute).
     */
    @SuppressWarnings("unchecked")
    protected <T> T createAction(final Class<T> clazz, final String namespace, final String name,
        final String methodNamethrows 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, null, true, false);
        proxy.setMethod(methodName);

        // 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();

        final String pathIntroducer = (namespace.endsWith("/")) ? namespace : namespace + "/";
        request.setServletPath(pathIntroducer + name + ".action");
        response = new MockHttpServletResponse();
        ServletActionContext.setRequest(request);
        ServletActionContext.setResponse(response);
        ServletActionContext.setServletContext(servletContext);

        if (proxy.getAction().getClass().equals(clazz)) {

            // BaseAction is the class from which all other actions are derived. You should have a similar

            // action that is derived from ActionSupport and implements SessionAware.
            final BaseAction action = (BaseActionproxy.getAction();
            action.setSession(sessionMap);
            return (Taction;
        }
        return null;
    }

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

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

       // Do the setup here that is needed for every test again like log-in a user.
       
    }

    // ---------------------------------------------------------------------------------------------

}

 
Using this class is very straight forward. Look at this sample test case below:
 
package ... your package here...;

import ......ActionBaseTestCase;

/**
 @author Mike
 */
public class DeleteCategoryInfoTest extends ActionBaseTestCase {

    private DeleteCategoryInfo action;
    
    // ---------------------------------------------------------------------------------------------

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

        action = createAction(DeleteCategoryInfo.class, """DeleteCategoryInfo");
    }

    public void testNoId() throws Exception {
        assertEquals(Action.INPUT, proxy.execute());
    }

    public void testInvalidId() throws Exception {
        action.setCategoryId(-100);
        assertEquals(Action.INPUT, proxy.execute());
    }

    public void testValidId() throws Exception {
        action.setCategoryId(6);
        assertEquals(Action.SUCCESS, proxy.execute());
    }

    // ---------------------------------------------------------------------------------------------

}
 
The proxy variable used in the example is implicitely created when you create the action. The
proxy.execute()
call will trigger the method which is set by
proxy.setMethod()
or
.execute()
if none is set. This is the same behavior as what you can set in the struts configuration file.