Unit Testing Struts 2 Actions

Hopefully this will help others who are trying to unit test Struts 2 Actions.

My goal is to be able to unit test my actions in the full Struts 2 context with the Interceptor stack being run which includes validation.  The big advantage of this type of testing is that it allows you test you validation logic, the Interceptor configuration for you actions, and the results configuration.

The current information on Struts 2 website regarding unit testing was not very helpful.  The guides page has 2 links to external blogs with some example code for unit testing with Spring.  I used these as starting points but since I’m not using Spring and the examples were heavily dependent on Spring I ended up spending a lot of time in the debugger figuring out how to make this work.

Below is my StrutsTestContext class it makes use of Mockrunner mock Http Servlet classes (mockrunner-servlet.jar).  (If you wish to use a different mock package it should be easy enough to make the change.)

The way this works is you first have to create a Dispatcher which reads your struts configuration. You then use the Dispatcher to create an ActionProxy with the desired request parameters and session attributes.  The ActionProxy will give you access to the Action object so you can set properties or inject mock objects for your test.  You next execute the ActionProxy to run the Interceptor stack and your action, this returns the result so you can test it for correctness.  You can also test the mock Http servlet objects to ensure other result effects have occured (e.g. a session attribute was changed.)

This has been updated for Struts 2.1.6 on 3/5/2009.

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package test.struts;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import com.mockrunner.mock.web.MockHttpServletRequest;
import com.mockrunner.mock.web.MockHttpServletResponse;
import com.mockrunner.mock.web.MockHttpSession;
import com.mockrunner.mock.web.MockServletContext;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ActionProxyFactory;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;

public class StrutsTestContext
{
    public StrutsTestContext(Dispatcher dispatcher,
                             MockServletContext servletContext)
    {
        this.dispatcher = dispatcher;
        this.mockServletContext = servletContext;
    }

    private Dispatcher              dispatcher;
    private MockServletContext      mockServletContext;
    private MockHttpServletRequest  mockRequest;
    private MockHttpServletResponse mockResponse;

    public static Dispatcher prepareDispatcher()
    {
        return prepareDispatcher(null, null);
    }

    public static Dispatcher prepareDispatcher(
           ServletContext servletContext,
           Map<String, String> params)
    {
        if (params == null)
        {
            params = new HashMap<String, String>();
        }
        Dispatcher dispatcher = new Dispatcher(servletContext, params);
        dispatcher.init();
        Dispatcher.setInstance(dispatcher);
        return dispatcher;
    }

    public static ActionProxy createActionProxy(
          Dispatcher dispatcher,
          String namespace,
          String actionName,
          HttpServletRequest request,
          HttpServletResponse response,
          ServletContext servletContext) throws Exception
    {
        // BEGIN: Change for Struts 2.1.6
        Container container = dispatcher.getContainer();
        ValueStack stack = container.getInstance(ValueStackFactory.class).createValueStack();
        ActionContext.setContext(new ActionContext(stack.getContext()));
        // END: Change for Struts 2.1.6

        ServletActionContext.setRequest(request);
        ServletActionContext.setResponse(response);
        ServletActionContext.setServletContext(servletContext);

        ActionMapping mapping = null;
        return dispatcher.getContainer()
           .getInstance(ActionProxyFactory.class)
           .createActionProxy(
            namespace,
            actionName,
            dispatcher.createContextMap(
                request, response, mapping, servletContext),
            true, // execute result
            false);
    }

    public ActionProxy createActionProxy(
               String namespace,
               String actionName,
               Map<String, String> requestParams,
               Map<String, Object> sessionAttributes) throws Exception
    {
        mockRequest = new MockHttpServletRequest();
        mockRequest.setSession(new MockHttpSession());
        mockResponse = new MockHttpServletResponse();

        if (requestParams != null)
        {
            for (Map.Entry<String, String> param :
                       requestParams.entrySet())
            {
                mockRequest.setupAddParameter(param.getKey(),
                                              param.getValue());
            }
        }
        if (sessionAttributes != null)
        {
            for (Map.Entry<String, ?> attribute :
                      sessionAttributes.entrySet())
            {
                mockRequest.getSession().setAttribute(
                    attribute.getKey(),
                    attribute.getValue());
            }
        }

        return createActionProxy(
            dispatcher, namespace, actionName,
            mockRequest, mockResponse, mockServletContext);
    }

    public Dispatcher getDispatcher()
    {
        return dispatcher;
    }

    public MockHttpServletRequest getMockRequest()
    {
        return mockRequest;
    }

    public MockHttpServletResponse getMockResponse()
    {
        return mockResponse;
    }

    public MockServletContext getMockServletContext()
    {
        return mockServletContext;
    }

}

Here is an example of using this class to unit test a Login Action.

/*
** Create a Dispatcher.
** This is an expensive operation as it has to load all
** the struts configuration so you will want to reuse the Dispatcher for
** multiple tests instead of re-creating it each time.
**
** In this example I'm setting configuration parameter to override the
** values in struts.xml.
*/
  HashMap<String, String> params = new HashMap<String, String>();
  // Override struts.xml config constants to use a guice test module
  params.put("struts.objectFactory", "guice");
  params.put("guice.module", "test.MyModule");

  MockServletContext servletContext = new MockServletContext();
  Dispatcher dispatcher = StrutsTestContext.prepareDispatcher(
       servletContext, params);

/*
**  Create an ActionProxy based on the namespace and action.
**  Pass in request parameters and session attributes needed for this
**  test.
*/
  StrutsTestContext context = new StrutsTestContext(
      dispatcher, servletContext);
  Map<String, String> requestParams = new HashMap<String, String>();
  Map<String, Object> sessionAttributes = new HashMap<String, Object>();
  requestParams.put("username", "test");
  requestParams.put("password", "test");

  ActionProxy proxy = context.createActionProxy(
      "/admin",      // namespace
      "LoginSubmit", // Action
      requestParams,
      sessionAttributes);

  assertTrue(proxy.getAction() instanceof LoginAction);

  // Get the Action object from the proxy
  LoginAction action = (LoginAction) proxy.getAction();

  // Inject any mock objects or set any action properties needed
  action.setXXX(new MockXXX());

  // Run the Struts Interceptor stack and the Action
  String result = proxy.execute();

  // Check the results
  assertEquals("success", result);

  // Check the user was redirected as expected
  assertEquals(true, context.getMockResponse().wasRedirectSent());
  assertEquals("/admin/WelcomeUser.action",
      context.getMockResponse().getHeader("Location"));

  // Check the session Login object was set
  assertEquals(mockLogin,
      context.getMockRequest().getSession().getAttribute(
         Constants.SESSION_LOGIN));
Advertisements

30 Responses to Unit Testing Struts 2 Actions

  1. pea says:

    Hi,

    Thanks for this!
    I am still about to test it out, yes the struts2 unit testing web pages are pretty lame 😦

    [snip]

    also, could you kindly include the import statements for the second code block?
    for example, I do not know where to get “Constants.SESSION_LOGIN”.

    lastly, what example values would be used here:
    params.put(“struts.objectFactory”, “guice”);
    params.put(“guice.module”, “test.MyModule”);

    i do not quite understand what their uses are.

    Thanks again!
    pea

  2. glindholm says:

    The second block of code is snippets from my unit tests it just meant as an example of use, you will not be able to compile and run it as you don’t have my action classes and configuration etc.

    The guice params lines are meant to show that you can override struts.xml configuration constants for your unit tests. In this case I’m using a guice test module for this unit test.
    params.put(”struts.objectFactory”, “guice”);
    params.put(”guice.module”, “test.MyModule”);

  3. Harsha says:

    when you are creating the proxy the name of the action class is LoginSubmit and when you are getting the action from the proxy it is LoginAction,

    Is that correct?

  4. Harsha says:

    I am getting this error message

    2008-07-15 17:07:00,406 FATAL [StrutsSpringObjectFactory.java:74] : ********** FATAL ERROR STARTING UP SPRING-STRUTS INTEGRATION **********
    Looks like the Spring listener was not configured for your web app!
    Nothing will work until WebApplicationContextUtils returns a valid ApplicationContext.
    You might need to add the following to web.xml:

    org.springframework.web.context.ContextLoaderListener

    any leads?

  5. glindholm says:

    In the example the action is LoginSubmit as in the URL http://…./admin/LoginSubmit.action the action class is LoginAction.

  6. glindholm says:

    I don’t use Spring so can’t really help you. There are links on the Struts 2 website under “Core Developers Guide” under Unit Testing. These links both use Spring.

  7. Harsha says:

    Thanks I guess I am getting a NPE at dispatcher.init(); and the console says FATAL ERROR STARTING UP…..

    Do I need to pass in the any other parameter to initialize the serveltContect correctly?

    Please let me know, I would like to go this route.

    Thanks

  8. glindholm says:

    Sorry, there is extra setup needed to unit test with Spring. I don’t use Spring so can not help you, see the Unit Testing links on the struts website.

  9. bejey says:

    Hi~
    Thanks for this!

    I don’t understand “params.put(“guice.module”, “test.MyModule”);”
    please show me the full source for unit test

  10. glindholm says:

    Sorry if this is causing undo confusion but it’s simply meant to illustrate that you can override configuration properties in struts.xml (or struts.properties).

    In my struts.xml file I define a constant like this…
    < constant name=”guice.module” value=”production.MyModule” />

    And for this test I’m want to override this value and use “test.MyModule” instead.

  11. J D says:

    Wow.. That was very helpful. I was trying to go a step furthur by mocking the service layer calls.

    TestService testService = EasyMock.createMock(TestService.class);
    EasyMock.expect(testService.method(1L, null))
    EasyMock.replay(testService);
    ActionProxy proxy = context.createActionProxy(“/main”, “ActionClass_method”, requestParams, sessionAttributes);
    String result = proxy.execute();
    assertNotNull(result);
    EasyMock.verify();

    I get an invocation exception since the service mock object is not available to the action class. Any idea on how I can push the mock object in.

    • glindholm says:

      If you are using dependency injection to get your service objects to your actions then you would need to override your DI configuration to inject your test or mock services instead of what the real ones. I use Guice for DI and in my example I override the configuration to use my test module instead of the normal production module. I don’t try to EasyMock service objects (that get injected) it seems like it would be hard to configure, but I do often use stub objects that produce an expected results such as return an error code or throw an exception.

  12. J D says:

    Alright, I had to take some time to process all the info.
    1. Dependency Injection – Isn’t struts 2.X always doing that or did I miss something.
    2. Guice – Guice looked useful. I have downloaded it and am now trying to configure it but in the meantime a quick question. Doesn’t it only inject the service to my action. How do I mock the call to the service method.
    3. “stub objects that produce an expected results” – what do you use to stub the service calls.

    Also, I must add, your post was a good start for struts testing and I did get some JUnits written. Thanks !!

  13. glindholm says:

    A lot of these are general Struts questions and you would be better off asking then in the Struts Users Mailing list. There are more then one way to test Struts apps and others may be able to help you better.
    Struts has some DI capabilities but most people seem to use either Spring or Guice for better DI functionality. You can configure Struts to use either of these for DI instead of the builtin facility.

  14. spaace says:

    This is very helpful as there are not a ton of resources on this, so thanks!

    I am still unable to get this working completely though. It seems that I get a null ActionProxyFactory back from this call.

    ActionProxyFactory proxyFactory = dispatcher.getContainer().getInstance(ActionProxyFactory.class);

    I am able to grab the other named factories out such as the Logger and Locale but there is nothing for ActionProxyFactory?

    Any ideas or suggestions?

    Thanks again.

    • glindholm says:

      My only guess is that the dispatcher is not reading your config or you have a config problem (or a non-standard config which works differently). I would check your classpath to ensure it is setup the same for testing as for running. Only other thing is to sit in the debugger, step thru and see what’s happening, this can be kinda rough and it took me and it took me a lot of time in the debugger to figure this out.

  15. spaace says:

    Thanks so much for the reply. I have debugged this thing much more that I would like now 😉 and think you are correct about the configuration. I wonder if you could shed some light on how it may work for my specific implementation. I have my struts object factory pointed at the struts-spring factory. Also, I am using tiles results. Here is the code I have to create my dispatcher:

    private StrutsBeanFactory() throws Exception
    {
    // Don’t know if I need the web.xml or not. I get a jspWriter error when I don’t do this
    // which makes me think it will need the web.xml to understand what to do with tiles.
    String[] config = new String[]{“classpath*:context.xml”, “classpath*:hibernate.cfg.xml”, “/WEB-INF/web.xml”};

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

    StrutsSpringObjectFactory factory = new StrutsSpringObjectFactory(null, null, servletContext);
    factory.setApplicationContext(appContext);
    StrutsSpringObjectFactory.setObjectFactory(factory);

    HashMap params = new HashMap();

    // Don’t know if I need to load the struts.xml files here or not
    // If I attempt this I get “ERROR | XmlConfigurationProvider.java – Unable to find parent packages struts-default” error in console output
    params.put(“config”, “classpath*:struts.xml”);
    dispatcher = new Dispatcher(servletContext, params);

    dispatcher.init();
    Dispatcher.setInstance(dispatcher);
    }

    Any ideas? Thanks again for the reply… I have a project at work that needs this and it’s kicking my trash so far. 😉

  16. sourav says:

    Can u please give the Testcase completely.Also i am getting BadClass version when i used the MockServletContext.

    • glindholm says:

      Sorry, a complete test case would also require me to post a complete application which I’m not going to do.
      You need to download mockrunner-0.4.1 from http://mockrunner.sourceforge.net/ then add to your testing environment the mockrunner-servlet.jar, be sure to use the correct version that matches the version of Java (1.5, 1.6 etc) you are using.

  17. M says:

    If you’re happy for people to copy and re-use this code could you please include a license notice with it?

    I’d like to use it but can’t until you state that you’re making it available under say the Apache license or the LGPL etc.

  18. Danilo says:

    Hey…

    firstly, thanks a lot for your helpfull post! it was amazing for me. it`s working nice now, but i have a problem: my struts action print on response the answer that is a big xml content. the xml if rightly formmed, but when my StrutsTestContext class gets the response trough the code:
    “StrutsTestContext context = (…);
    String response = context.getMockResponse().getOutputStreamContent();”
    as the response is big (21812 chars), its clipped and the final size is 16384 chars.

    Someone knows how can i solve this problem!?

    thanks

    • glindholm says:

      I’m glad you find StrutsTestContext helpful.
      Are you sure you’re not comparing bytes to chars? An OutputStream is for bytes and String is for chars.
      When you convert between bytes and chars you need to ensure you are using the same character encoding.
      If that doesn’t help them please use the Struts mailing list to post general questions as this is not a Struts support forum.

  19. Danilo says:

    Hei glindholm,
    thanks for your fast reply! 😀
    my app works well on the web with struts! the problem is not struts at all, this problem occours when i get the result form my junit class test. i debug my app to see if its returning the clipped response but doesn’t. the problem is when the StrutsTestContext gets the response content trough getOutputStreamContent() method.

    • glindholm says:

      I suggest you look at mockrunner’s MockHttpServletResponse and MockServletOutputStream and see what’s happening in the debugger. I see they default to “ISO-8859-1” character encoding, and you probably want to use UTF-8 .

  20. Haroon Rafique says:

    Hi Greg,

    I have revised some of the code that the arsenalist and I wrote with very good performance improvements.

    Basically I changed:
    dispatcher.init();
    to:
    if (configurationManager != null) {
    dispatcher.setConfigurationManager(configurationManager);
    }
    dispatcher.init();
    if (configurationManager == null) {
    configurationManager = dispatcher.getConfigurationManager();
    }

    with a static variable declaration at the top:
    private static ConfigurationManager configurationManager;

    This way the xwork configuration only happens once (resulting in faster code and no memory leak). I used a memory profiler to come to that conclusion.

    YMMV, cheers,

    Haroon

  21. billy vandory says:

    man, i can’t believe the amount of people that take blogs like these and try out the code line for line. It’s an example, people, you kinda have to use the concepts presented here and code it for your situation. For example, if you are using SPRING of course this example will not work, there’s no evidence of spring anywhere in the presented code. But instead of crying for help, wouldn’t it make sense to modify the code to bootstrap the Spring IOC container? What have we become. Yes, it’s a cocky arrogant rant, just in a bad mood today. Sorry everyone !!

  22. Betsy says:

    Excellent post. I will be going through many of these issues as well.
    .

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: