Testing Struts actions with Spring dependencies
July 31st, 2008The main idea of this post is to explain the way to test Struts actions with service dependencies using StrutsTestCase library. In my case all the actions are handled by Spring and the dependencies are injected just before initializing. That is clear that testing the actions in isolation requires bypassing Spring and letting Struts do the inizializing. But this time we have to inject the dependencies before the action is executed.
I can hear you saying, “Hey no problem at all, mock the service and put it into the action”. Yes but what action instance do you have in your hands prior to execution? Yes you don’t. StrutsTestCase library does not provide you the functionality to get your hands on action instance, you only have the ability to invoke the execution of the action instance that will be created somewhere. –not so mystery, we know where the action is initialized– In this case we have got a couple of ways to achive the objective, that’s enough talk for now, lets get our hands dirty.
We have got an action named LoginAction and this action has a service dependency, UserService, and the implementation of this service is injected into the action via Spring.
public class LoginAction extends Action {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
// some logic using userService
String username = request.getAttribute("username");
String password = request.getAttribute("password");
if(userService.login(username, password)) {
request.getSession().setAttribute("username", username);
return mapping.findForward("success");
}
return mapping.findForward("error");
}
}
The action given above is a simple action that checks if the login information is correct or not. According to the result returned from the service, the action decides to forward the request to success or error. Let’s write a unit test for this action using STC.
public class LoginActionTest extends MockStrutsTestCase {
public static Test suite() {
return new TestSuite(LoginActionTest.class);
}
public void testSuccessfulLogin() {
setRequestPathInfo("/login");
addRequestParameter("username", "witt");
addRequestParameter("password", "eyesony");
actionPerform();
verifyForward("success");
assertEquals("witt", getSession().getAttribute("username"));
}
}
In order to test this action in isolation it is obvious that we have to mock UserService and put it into the action instead of Spring, because if we depend on Spring for this job then this test will not be a unit test. It is also obvious that we have to make changes to web.xml and struts-config.xml in order to bypass Spring. We dont need Spring for dependency injection, we are going to inject mocked versions of dependencies. It is also possible to let Spring do its job, then afterwards to override the objects injected with the mocked versions but this will cause unnecessary longer test times.
Now how can we access to action instance from within MockStrutsTestCase, unfortunately there is no method or so to get action instance. There is however two ways to achive this objective; one not-so-pretty and other one the aop way. First the not-so-pretty way.
- MockStrutsTestCase has a public getter method that returns ActionServlet and as you know from Struts api, ActionServlet has a protected getter method that returns RequestProcessor. Finally RequestProcessor has a protected Hashmap for storing action instances. This means that if we get our hands on that protected Hashmap, then we get our hands on the action instances. If you subclass both the ActionServlet and RequestProcessor, technically you can access the action instances. However the action instances are created just before you make the actionPerform() call from MockStrutsTestCase. So you also have to override the processActionPerform() method of RequestProcessor and have to create your mock in that method and inject into the action. The downside of this method is that you have to subclasses RequestProcessor at least one for every different test class you want to write, because every time you propably need different mocks of different services with different expectations.
- Second one is the AOP way, we can intercept the Action.execute call and we can get the action instance just before execution by the help of aop. The only thing we have to do is to define a pointcut to capture the action and a before advice that will let us inject our mock into the action instance. I am not going to go into details of writing aspects, if you are not familiar with aop i suggest you to make a search on google before going on reading. I am going to use AspectJ for implementation, it is possible to use different AOP frameworks.
The following is the source code of our aspect.
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForward;
aspect StrutsActionPreExecuteNotifier {
// this pointcut selects the test method's execution join point and
// exposes the current executing test object instance
pointcut mockStrutsTest(StrutsActionPreExecuteListener actionTest):
execution(public void StrutsActionPreExecuteListener+.test*())
&& this(actionTest);
// this pointcut selects the action execution join point. It is combined
// with the first pointcut using cflow; means the combined scope is limited
// to the call flow of the action test method.
pointcut strutsActionExecute(
Action action, StrutsActionPreExecuteListener actionTest):
execution(public ActionForward Action+.execute(..))
&& this(action)
&& cflow(mockStrutsTest(actionTest));
// the before advice that is bound to the previous point cut provides
// listener's preActionExecuteOccured method to be called passing the
// the action instance for mock injection.
before(Action action, StrutsActionPreExecuteListener actionTest):
strutsActionExecute(action, actionTest) {
actionTest.preActionExecuteOccurred(action);
}
}
The following is the interface that our test classes have to implement in order to have opportunity to inject mocked service objects into the action instance under test. If the test classes do not implement this interface then there will be no matching point cut.
import org.apache.struts.action.Action;
public interface StrutsActionPreExecuteListener {
public void preActionExecuteOccurred(Action action);
}
The source of the modified version of our test class is given below.
public class LoginActionTest extends MockStrutsTestCase
implement StrutsActionPreExecuteListener{
public static Test suite() {
return new TestSuite(LoginActionTest.class);
}
public void testSuccessfulLogin() {
setRequestPathInfo("/login");
addRequestParameter("username", "witt");
addRequestParameter("password", "eyesony");
actionPerform();
verifyForward("success");
assertEquals("witt", getSession().getAttribute("username"));
}
public void preActionExecuteOccured(Action action) {
LoginAction lAction = (LoginAction) action;
// mock the service and put it into the action
lAction.setUserService(....);
}
}
There is nothing special about this code, we simply got the action instance, casted it to LoginAction and put our mocked version of UserService into it. In order to mock the service we can implement the service or we can use a mock library –easymock, jmock– in order to mock and set expectations runtime. If you don’t know how to use mock libraries you can have a look at my post on mocking.
If you implement StrutsActionPreExecuteListener interface in your test class, then before the execution of the methods with names starting with ‘test’, the test class’s preActionExecuteOccured method will be called with the appropriate action instance.
You can do what ever you want with the action instance, the execution flow will wait for the method to return. After your preActionExecuteOccured method’s excution finishes, your test method body will start executing. You can have more than one test method but there will be only one preActionExecuteOccured method, so you have to do all the work for different test scenerios in the same method. But this is not so bad after all you do not have to subclass ActionServlet and RequestProcessor.