We have a legacy application and a lot of presentation code is written using Struts 1.2.4. For unit tetsing the action classes we used the following approach.
StrutsTestCase provides both a Mock Object approach and a Cactus approach to actually run the Struts ActionServlet, allowing you to test your Struts code with or without a running servlet engine. When you want to execute your tests as a part of the continuous integration environment in which all the unit tests should execute without deploying the application to the container.
For the following entry of Struts-config.xml
<action path="/login" type="com.dnbi.simpleaction.LoginAction" name="loginForm" input="/login/login.jsp" scope="request"> <forward name="success" path="/action/simpleAction" /> <forward name="failure" path="/failurePath" /> </action>
and for the following action,
public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { ActionErrors errors = new ActionErrors(); String username = ((LoginForm) form).getUsername(); String password = ((LoginForm) form).getPassword(); if ((!username.equals("vikas")) || (!password.equals("pass"))) errors.add("password", new ActionError("error.password.mismatch")); if (!errors.isEmpty()) { saveErrors(request, errors); return new ActionForward(mapping.getInput()); } HttpSession session = request.getSession(); session.setAttribute("authentication", username); return mapping.findForward("success"); }
the test would look like this
public class LoginActionTest extends MockStrutsTestCase { public LoginActionTest(String testName) { super(testName); } public void setUp() throws Exception { super.setUp(); setInitParameter("validating", "false"); setRequestPathInfo("/login"); } public void testLogin_WithoutParameters_Expected_BackToLoginPage() { actionPerform(); assertEquals(null, getSession().getAttribute("authentication")); verifyForwardPath("/login/login.jsp"); verifyActionErrors(new String[] { "error.username.required", "error.password.required" }); } public void testSuccessfulLogin() { addRequestParameter("username", "vikas"); addRequestParameter("password", "pass"); setRequestPathInfo("/login"); actionPerform(); verifyForward("success"); verifyForwardPath("/action/simpleAction"); assertEquals("vikas", getSession().getAttribute("authentication")); verifyNoActionErrors(); } public void testFailedLogin() { addRequestParameter("username", "deryl"); addRequestParameter("password", "express"); setRequestPathInfo("/login"); actionPerform(); verifyForwardPath("/login/login.jsp"); verifyInputForward(); verifyActionErrors(new String[] { "error.password.mismatch" }); assertNull(getSession().getAttribute("authentication")); } public static void main(String[] args) { junit.textui.TestRunner.run(LoginActionTest.class); }
Now comes the difficult part which would be used in most of the scenarios. The action class would be calling another service/manager to get the work done. When you are doing a unit test, you would like not to make a call to the actual service but instead make a call to the mocked service.
So assume the following action class
Here the action class makes a call to the DoSomethingService. What we would like to do is to mock the service in our test case so that the actual service is not called.
public class ServiceCallingAction extends Action { private DoSomethingService service; private DoSomethingService getService() { return this.service; } public void setService(DoSomethingService service) { this.service = service; } public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { return mapping.findForward(getService().serveAction() ? "success" : "failure"); } }
This becomes difficult with StrutsTestCase because we do not get a handle to the action class. The action class is prepared behind the scenes and is called on the basis of the definition in the struts-config.xml file.
So for example in the test case we are calling the method actionPerform() As soon as this method is called then on the basis of setRequestPathInfo() the action class is prepared and executed behind the scenes. So how do we inject the mocked object.
This is where Aspects help us. So we can ask the aspects to inject the mock before the execute method on the action is called.
So let us see what the aspect would look like
aspect InjectMockService { pointcut mockStrutsTest(StrutsActionMockServiceInjection actionTest): execution(public void StrutsActionMockServiceInjection+.test*()) && this(actionTest); pointcut strutsActionExecute(Action action, StrutsActionMockServiceInjection actionTest): execution(public ActionForward Action+.execute(..)) && this(action) && cflow(mockStrutsTest(actionTest)); before(Action action, StrutsActionMockServiceInjection actionTest): strutsActionExecute(action, actionTest) { actionTest.injectMockService(action); }
now as you can see in the aspect before the execute method on the action is called, we call the injectMockService of the ActionTestClass.
Let us see what the action test class would look like
public class ServiceCallingActionTest extends MockStrutsTestCase implements StrutsActionMockServiceInjection { // ================Create any mock service objects which the action would be // calling ========== private DoSomethingService serviceMock = createMock(DoSomethingService.class); // ================ SETUP & TEARDOWN ======================== protected void setUp() throws Exception { super.setUp(); } protected void tearDown() throws Exception { super.tearDown(); } // ================ INJECT MOCK OBJECTS To the Action========== public void injectMockService(Action action) { ((ServiceCallingAction) action).setService(serviceMock); } // ========================= TEST CASES ======================= public void testSuccess() { setRequestPathInfo("/action/simpleAction"); expect(serviceMock.serveAction()).andReturn(true); replay(serviceMock); actionPerform(); verifyForward("success"); verifyNoActionErrors(); verify(serviceMock); } public void testFailure() { setRequestPathInfo("/action/simpleAction"); expect(serviceMock.serveAction()).andReturn(false); replay(serviceMock); actionPerform(); verifyForward("failure"); verifyNoActionErrors(); verify(serviceMock); } }
so here before the execute method on the action is called, the injectMockService is called on the test class which injects the mocked service object to the action class. Once the mocked object has been injected, we can use the regular easymock syntax to run the test case against the mocked object.
References for this article
1. StrutsTestCase
2. EasyMock
3. AJDT for eclipse
4. Source code for this article
Pete Lund
Monday, August 10, 2009
Thanks for discussing this topic. The approach generally makes sense to me (though I’ve not come across Aspects before and have some further investigation to do!) My question is, how is the Service property of ServiceCallingAction injected in the production scenario? In our test example, I see how the Service property is injected in your example. But I can’t picture the analogous flow in the production scenario; where is the true Service injected?
aldo
Wednesday, September 2, 2009
Hi a question, It is unsafe to have (private DoSomethingService service;)?
Struts create only one Action object so this variable service will be shared to all calls.
Is your var service thread safe?
vikashazrati
Saturday, September 5, 2009
Hi Aldo, though i understand your concern but this topic does not touch upon the thread safety issues. Could you look at http://www.coderanch.com/t/420591/Struts/Struts-Action-Business-layer-Thread to get more clarity? thanks
gate
Thursday, March 10, 2011
where i can download sourcecode for this one. there is an error in StrutsActionMockServiceInjection – StrutsActionMockServiceInjection cannot be resolvoed. help, Thanks!