jinfeng_wang

          G-G-S,D-D-U!

          BlogJava 首頁 新隨筆 聯(lián)系 聚合 管理
            400 Posts :: 0 Stories :: 296 Comments :: 0 Trackbacks
          Unit Test Your Struts Applicationby Lu Jian
          09/22/2004

          Unit testing is an important part of a good development process. Although there are many unit testing frameworks and technologies, such as JUnit, Cactus, EasyMock, HttpUnit, and so on, developers often find that it is difficult to unit test their Struts applications.

          This article introduces StrutsUT, a simple extension to the Cactus framework, to help solve this problem. It provides two solutions to unit test Struts applications: a "traditional" solution and one based on AspectJ. Developers can choose either for their convenience.

          This article explains the initial idea of Cactus from a developer's point of view and extends this idea further into the Struts domain, which is the core of StrutsUT. The reader should have some knowledge of and experience in Struts framework, JUnit, Cactus, and/or AspectJ.

          An Overview of JUnit and Cactus

          What is JUnit?

          JUnit is a framework to create and perform unit tests on Java classes. With the help of mock objects and override technology, JUnit can perform unit tests for most Java applications. See References below to learn more about JUnit and mock objects. In this article, I chose EasyMock as the mock object implementation.

          Below is a simple Java class with its test case.

          //SimpleClass.java
          package unittest.simple;
          
          public class SimpleClass {
              public SimpleClass() {
              }
              
              public String foo(int n) {
                  ExternalInf inf = getExternalInf();
                  String s = inf.doSomeExtThing(n);
                  return "Woo!" + s;
              }
              
              protected ExternalInf getExternalInf() {
                  ExternalInf inf = null;
                  
                  //do some operation to get the interface
                  //JNDI call or something else
                  return inf;
              }
          }
          
          //ExternalInf.java
          
          package unittest.simple;
          
          public interface ExternalInf {
              //return "Great" when n equals 10
              String doSomeExtThing(int n);
          } 
          
          //SimpleClassTest.java
          
          package unittest.simple;
          
          import org.easymock.MockControl;
          
          import junit.framework.TestCase;
          
          public class SimpleClassTest extends TestCase {
             protected void setUp() throws Exception {
                super.setUp();
             }
          
             protected void tearDown() throws Exception {
                super.tearDown();
             }
          
             //Test foo() method in SimpleClass
             public void testFoo() {
                //define the mock object
                MockControl controller = MockControl.
                     createControl(ExternalInf.class);
                final ExternalInf inf = (ExternalInf)
                     controller.getMock();
                 
                //define the behavior of mock object
                inf.doSomeExtThing(10);
                controller.setReturnValue("Great");
                controller.replay();
                  
                //use override technology to bridge from 
                //mock object to the class to be tested
                SimpleClass instance = new SimpleClass() {
                   protected ExternalInf getExternalInf() {
                      return inf;
                   }
                };
                  
                //start test
                String result = instance.foo(10);
                  
                //do verification between expected result 
                //and actual result
                assertEquals("Woo!Great", result);
                  
                //do verification on the mock object
                controller.verify();
             }
          }

          In the example above, we use a mock object to simulate the external interface and override the getExternalInf method to bridge the mock object to the real class to be tested. It this article, I call these two technologies "traditional unit-test technologies".

          The Problems

          When we look at a web application, we find it is still possible to use mock objects and override technology to do the unit test. But this approach has several limitations:

          • It involves a large workload.

            We need to mock up the HttpServletRequest, HttpServletResponse, and Session objects. This is a tedious and error-prone task.

          • It is difficult to compare the expected and actual results.

            The servlet output is not an object structure, but an output stream that contains a run of strings. It is difficult to compare whether two blocks of HTML documents are "equal" or not.

          Why Cactus?

          The most effective way to solve the first problem is very straightforward. Since all web containers already implement these interfaces, why would we need to mock them? Cactus makes use of the web container's implementation to simplify this job. It uses an "in-container" strategy to extend the JUnit framework to the web container.

          Cactus has a two-part unit test framework. One part is the traditional JUnit framework. There is an additional WebRequest, which allows user to specify request parameters and add HTTP headers. The other is the "in-container" part, which uses a special servlet to bridge the user-defined WebRequest to a real HttpServletRequest. It also provides a "join point" to interact with the test case. This join point gives test case writers a chance to add request attributes and session attributes, and call the real class (an EJB, TagLib, Servlet, or Filter class) to be tested or forward the HttpServletRequest to the real web resource (a specific servlet or JSP page).

          Figure 1 shows a high-level view of Cactus' traditional unit-testing architecture.

          Cactus Architecture
          Figure 1. The traditional Cactus test case execution flow

          Cactus also integrates HttpUnit to simplify the result comparison. HttpUnit transfers the HTML document into a WebResponse object, which contains all kinds of HTML elements, such as forms, tables, input fields, buttons, and so on. This makes the result comparison much easier.

          Refer to How Cactus Works to learn more about Cactus.

          Below is a simple servlet class with its test case.

          // SimpleServlet.java
          package unittest.cactus;
          
          import java.io.IOException;
          import java.io.PrintWriter;
          
          import javax.servlet.ServletException;
          import javax.servlet.http.HttpServlet;
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          
          public class SimpleServlet extends HttpServlet {
             //Return html document with an enabled button
             //if type param == normal
             //Otherwise, return html document with 
             //disabled button
             protected void doGet(
                    HttpServletRequest request,
                    HttpServletResponse response) 
                    throws ServletException, IOException {
                String attr = request.getParameter("type");
                PrintWriter pw = response.getWriter();
                response.setContentType("text/html");
                pw.print(
                  "<html><head/><body>");
                pw.print("<form name='form1'>");
                if (attr.equals("normal")) {
                   pw.print("<input type=button 
                             name='button1' 
                             value='Click me'/>");
                } else {
                    pw.print("<input type=button 
                              name='button1' 
                              value='Click me' 
                              disabled/>");
                }
                pw.print("</form>");
                pw.print("</body></html>");
             }
          } 
          
          //SimpleServletTest.java
          package unittest.cactus;
          
          import org.apache.cactus.ServletTestCase;
          import org.apache.cactus.WebRequest;
          
          import com.meterware.httpunit.Button;
          import com.meterware.httpunit.HTMLElement;
          import com.meterware.httpunit.WebResponse;
          
          public class SimpleServletTest 
                                  extends ServletTestCase {
              
             public SimpleServletTest(String name) {
                super(name);
             }
          
             protected void setUp() throws Exception {
                super.setUp();
             }
          
             protected void tearDown() throws Exception {
                super.tearDown();
             }
          
             //prepare http request parameters
             //set type parameter to normal
             public void beginDoGet1(WebRequest request) {
                request.addParameter("type", "normal");
             }
              
             //test case 1 for doGet() method 
             //in SimpleServlet
             public void testDoGet1() {
               SimpleServlet servlet = new SimpleServlet();
               try {
                  servlet.doGet(request, response);
               } catch (Exception e) {
                  fail("Unexpected exception: " + e);
               }
             }
          
             //compare the result
             public void endDoGet1(WebResponse response) {
                HTMLElement[] elements = null;
                try {
                   elements = response.
                            getElementsWithName("button1");
                   assertEquals(1, elements.length);
                   assertFalse(((Button)elements[0]).
                                             isDisabled());
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          
             //prepare http request parameters
             //set type parameter to abnormal
             public void beginDoGet2(WebRequest request) {
                request.addParameter("type", "abnormal");
             }
              
             //test case 2 for doGet() method 
             //in SimpleServlet
             public void testDoGet2() {
                SimpleServlet servlet=new SimpleServlet();
                try {
                   servlet.doGet(request, response);
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          
             //compare the result
             public void endDoGet2(WebResponse response) {
                HTMLElement[] elements = null;
                try {
                   elements = response.
                            getElementsWithName("button1");
                   assertEquals(1, elements.length);
                   assertTrue(((Button)elements[0]).
                                             isDisabled());
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          }




          The Problems With Unit Testing a Struts Application

          What is Struts?

          Struts is a successful web-application framework that uses a central controller to control the page flow. The control logic is represented by the Struts configuration file. Refer to struts.apache.org to learn more about Struts.


          Below are a simple Struts configuration file and its related Action and Form, as well as its JSP file.

          struts-config.xml
          
          <?xml version="1.0" encoding="ISO-8859-1" ?>
          <!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
          "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
          <struts-config>
          
            ...
            <form-beans>
              <form-bean 
                        name="simpleForm"
                        type="unittest.struts.SimpleForm"/>
            </form-beans>
          
            <action-mappings>
              <action    
                        path="/strutsTest"
                        type="unittest.struts.SimpleAction"
                        name="simpleForm"
                        scope="request">
               <forward 
                        name="succeed" 
                        path="/result.jsp"/>
              </action>
            </action-mappings>
            ...
          
          </struts-config>
          
          
          //SimpleForm.java
          package unittest.struts;
          
          import org.apache.struts.action.ActionForm;
          
          public class SimpleForm extends ActionForm {
              String name;
          
              public String getName() {
                  return name;
              }
          
              public void setName(String name) {
                  this.name = name;
              }
          } 
          
          //SimpleAction.java
          package unittest.struts;
          
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          
          import org.apache.struts.action.Action;
          import org.apache.struts.action.ActionForm;
          import org.apache.struts.action.ActionForward;
          import org.apache.struts.action.ActionMapping;
          
          import unittest.simple.ExternalInf;
          
          public class SimpleAction extends Action {
          
              //Get the name from the form and put it 
              //into request attribute
              public ActionForward execute(
                              ActionMapping mapping,
                              ActionForm form,
                              HttpServletRequest request, 
                              HttpServletResponse response)
                              throws Exception {
                  SimpleForm simpleForm = (SimpleForm)form;
                  ExternalInf inf = getExternalInf();
                  String name = simpleForm.getName();
                  if (name == null) {
                      name = "anonymous";
                  }
                  request.setAttribute("name", name + 
                                   inf.doSomeExtThing(10));
                  return mapping.findForward("succeed");
              }
              
              protected ExternalInf getExternalInf() {
                  ExternalInf inf = null;
                  //do some operation to get the 
                  //external interface
                  //Use JNDI operation or something else
                  return inf;
              }
          } 
          
          
          //result.jsp
          
          <%@ page contentType=
                          "text/html; charset=UTF-8"%>
          <%@ taglib uri="/WEB-INF/struts-bean.tld" 
                     prefix="bean" %>
          
          <html>
          <body>
          <form name="form1">
            The name is <input type="text" name="name" 
                    value='<bean:write name="name"/>'/>
          </form>
          </body>
          </html>

          The Problems

          Struts separates the view and controller from the old servlet approach. This helps developers to implement each function more clearly, but it presents some problems when doing unit tests. There are two approaches to do unit testing on Struts applications:

          • Using Cactus to test a single Action class

            It is possible to use Cactus to unit test the execute() method in the Action class. Test case writers can continue to use the mock object and override approaches. However, they have to construct the ActionMapping object by themselves. ActionMapping is a Struts class that is constructed by Struts itself, according to the content of the Struts configuration file. To ask the test case writers to take on this responsibility is not a good choice.

            Below is a test case for SimpleAction.

            //SimpleActionTest.java
            package unittest.struts;
            
            import org.apache.cactus.ServletTestCase;
            import org.apache.struts.action.ActionForward;
            import org.apache.struts.action.ActionMapping;
            import org.easymock.MockControl;
            
            import unittest.simple.ExternalInf;
            
            public class SimpleActionTest 
                                    extends ServletTestCase {
            
               protected void setUp() throws Exception {
                  super.setUp();
               }
            
               protected void tearDown() throws Exception {
                  super.tearDown();
               }
            
               //test execute() method in SimpleAction
               public void testExecute() {
                  //define the mock object
                  MockControl controller = MockControl.
                            createControl(ExternalInf.class);
                  final ExternalInf inf = (ExternalInf)
                                        controller.getMock();
                    
                  //define the behavior of mock object
                  inf.doSomeExtThing(10);
                  controller.setReturnValue("Great");
                  controller.replay();
            
                  //create SimpleAction class
                  //use override technology to bridge from 
                  //mock object to the class
                  //to be tested
                  SimpleAction action = new SimpleAction() {
                     protected ExternalInf getExternalInf() {
                          return inf;
                     }
                  };
                  //prepare the SimpleForm
                  SimpleForm form = new SimpleForm();
                  form.setName("Dennis");
                  //prepare the ActionMapping
                  ActionMapping mapping=new ActionMapping();
                  mapping.addForwardConfig(new ActionForward(
                                               "succeed", 
                                               "/result.jsp", 
                                               false));
                   
                  try {
                     //do the test
                     ActionForward forward=action.execute(
                                                   mapping, 
                                                   form, 
                                                   request, 
                                                   response);
                     //compare the result
                     assertNotNull(forward);
                     assertEquals("/result.jsp", 
                                  forward.getPath());
                     //verify the mock object
                     controller.verify();
                  } catch (Exception e) {
                     fail("Unexpected exception: " + e);
                  }
               }
            } 
          • Using Cactus for an end-to-end test

            In some circumstances, there is a requirement to do an end-to-end test in Struts. For example, there is a test case to test whether a specific button in the response HTML document is disabled or not, via a certain URL. There are many limitations in this kind of test. Because the Struts framework controls the whole flow, from accepting the URL to returning the response, there is no "join point" to interact with the test case. It is impossible to use mock objects and overrides. This presents some difficulties for unit testing the Struts application using external interfaces.

          Both of these approaches have serious limitations. The first approach uses traditional unit test technologies, but has limited scope. It can only test the Action class, which is only part of the Struts framework. The second approach has a larger scope and provides an end-to-end test solution, but cannot use the traditional unit test technologies. It is very difficult to write test cases without the help of these technologies.



          A Simple Solution

          Why not combine the two approaches together? Since Struts has done the job of constructing the ActionMapping according to the Struts configuration file, it is a good choice to leave the mapping construction job to Struts. What we need to do is just to provide a join point around the execute() method in the Action class that is called by Struts. Test case writers can make use of this join point to prepare the ActionForm and use traditional unit test technologies to prepare an Action class that uses external interfaces.


          The idea is to extend the Cactus framework's "in-container" part to interact with the test case two times in the web container. One is called by the Cactus-specific servlet, ServletRedirector, as usual. The other is called by the Struts framework. Because Cactus and Struts are both running in the same JVM/web container, they can interact with the same test case instance.

          Introducing StrutsUT

          The solution presented here, StrutsUT, provides such an extension to help unit test Struts applications. Here's how it works:

          1. A client-side test runner creates the test case instance and initiates it by calling the begin() method. For each test point XXX in the test case, it calls the beginXXX() method to prepare request parameters and/or request headers.

          2. The client sends the request to the server-side Cactus redirect servlet.

          3. The redirect servlet creates the test case instance on the server side according to the information from request, and assigns the HttpServletRequest, HttpServletResponse, and HttpSession to the test case public fields.

          4. The redirect servlet calls the setUp() method in the test case to satisfy the test precondition and calls testXXX() to launch the test process.

          5. The request is redirected to the Struts RequestProcessor.

          6. RequestProcessor uses the same test case instance and calls prepareFromXXX() and prepareActionXXX() to prepare the ActionForm and Action instance.

          7. The RequestProcessor calls the execute() method in Action.

          8. The RequestProcessor calls endActionXXX() method in the test case to do any necessary verification and prepare the next join point, if needed.

          9. The Struts framework finishes the remaining operations and returns the control flow.

          10. The Cactus redirect servlet calls the tearDown() method in the test case to clear the test environment.

          11. The Cactus redirect servlet finishes the test case invocations.

          12. The Cactus redirect servlet returns the response to client-side test runner.

          13. The client-side test runner calls the endXXX() method in the test case to verify the response for each test point XXX, and calls the end() method to clear the status of the test case.

          Figure 2 shows the StrutsUT test case execution flow.

          Figure 2
          Figure 2. StrutsUT test case execution flow

          With StrutsUT, test case writers now can do more in the test case:

          • Use prepareFormXXX() method to prepare the ActionForm, which will be the argument the execute() method in the Action class.

          • Use the prepareActionXXX() method to prepare the Action instance to be called.

          • Use the endActionXXX() method to do any necessary verification and prepare the next join point, if needed, after calling Action's execute() method.

          Like the extra methods in Cactus' ServletTestCase--begin(), beginXXX(), endXXX(), end(), setUp(), and tearDown()--it is not mandatory to provide these extra methods. Use them when needed.

          There are two implementations in StrutsUT to satisfy the idea described above.

          The StrutsUT Traditional Solution

          In order to insert such a join point within the control flow of Struts, it is necessary to extend Struts' central controller, RequestProcessor, to interact with the test case. We also have to extend Cactus' test case base class, ServletTestCase, to add extra information about the test point name and test case instance that will be used by the Struts central controller to call the correct test helper methods on the exact test case instance.

          StrutsUT replaces the Struts central controller, RequestProcessor, with a subclass called StrutsUnitTestRequestProcessor, and uses StrutsServletTestCase to replace Cactus' ServletTestCase as the test case base class.

          A Simple Test Case

          // SimpleStrutsTest.java
          package unittest.struts;
          
          import javax.servlet.RequestDispatcher;
          
          import org.apache.cactus.WebRequest;
          import org.apache.struts.action.Action;
          import org.apache.struts.action.ActionForm;
          import org.apache.struts.action.ActionMapping;
          import org.easymock.MockControl;
          import org.jingle.unittest.struts.*;
          
          import unittest.simple.ExternalInf;
          
          import com.meterware.httpunit.WebForm;
          import com.meterware.httpunit.WebResponse;
          
          public class SimpleStrutsTest 
                            extends StrutsServletTestCase {
             //define the mock object
             MockControl controller = MockControl.
                          createControl(ExternalInf.class);
          
             ExternalInf inf = (ExternalInf) 
                                      controller.getMock();
          
             //make sure call the super.setup() when 
             //override this method
             protected void setUp() throws Exception {
                super.setUp();
             }
          
             //make sure call the super.tearDown() 
             //when override this method
             protected void tearDown() throws Exception {
                super.tearDown();
             }
          
             public void beginStrutsTestAction(
                                      WebRequest request) {
             }
          
             //Prepare ActionForm
             public ActionForm prepareFormStrutsTestAction(
                          ActionMapping mapping) {
                SimpleForm form = new SimpleForm();
                form.setName("Dennis");
                return form;
             }
          
             //Prepare the Action
             public Action prepareActionStrutsTestAction(
                                   ActionMapping mapping) {
                //define the behavior of mock object
                controller.reset();
                inf.doSomeExtThing(10);
                controller.setReturnValue("Great");
                controller.replay();
          
                //Use override technology to bridge the 
                //mock object to the class to be tested
                SimpleAction action = new SimpleAction() {
                   protected ExternalInf getExternalInf() {
                      return inf;
                   }
                };
                return action;
             }
          
             public void testStrutsTestAction() {
                //forward to the action to be tested
                RequestDispatcher rd = this.request
                   .getRequestDispatcher("/strutsTest.do");
                try {
                   rd.forward(this.request, this.response);
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          
             //verify the mock object after the execution 
             //of action
             public ActionResult 
                              endActionStrutsTestAction() {
                controller.verify();
                //continue the struts framework process
                return null; 
             }
          
             //compare the result html documents
             public void endStrutsTestAction(
                                    WebResponse response) {
                try {
                   WebForm form = response.getForms()[0];
                   assertEquals(
                          "DennisGreat", 
                          form.getParameterValue("name"));
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          } 



          How to Write the Test Case


          Here are the key points of how to write the test case.

          • Each test case should extend from StrutsServletTestCase.

          • Define a public ActionForm prepareFormXXX(ActionMapping) method if you want to prepare the ActionForm instance.

          • Define a public Action prepareActionXXX(ActionMapping) method if you want to prepare the Action instance.

          • Define a public ActionResult endActionXXX() method if you want to verify the mock object and/or introduce the next join point. The return value of this method is an ActionResult object that contains a String attribute. In a real Struts application environment, it is quite possible that the forward of an action is also an action instead of a JSP file. In this circumstance, the return value of this method is used to specify the next join point position. For example, if the return ActionResult contains the string YYY, the Struts framework will try to call prepareFormYYY() and prepareActionYYY() before calling the next Action, and call endActionYYY() after the Action execution. The next example gives you a more complicated scenario and test case. There are two predefined ActionResult instances: TERMINATE and CONTINUE. TERMINATE simply terminates the process of Struts and return the control flow. CONTINUE or null will continue the Struts operation as usual.

          A More Complicated Example

          Change the Struts configuration file to make the Action forward more complicated.

          //struts-config.xml
          
              ...
              <action  
                        path="/strutsTest"
                        type="unittest.struts.SimpleAction"
                        name="simpleForm"
                        scope="request">
                <forward 
                        name="succeed"              
                  path="/anotherTest.do"/>
              </action>
          
              <action    
                        path="/anotherTest"
                        type="unittest.struts.AnotherAction">
                <forward 
                        name="succeed"              
                        path="/result.jsp"/>
              </action>  
              ...
          
          </struts-config>

          Introduce an extra Action into the scenario:

          //AnotherAction.java
          package unittest.struts;
          
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          
          import org.apache.struts.action.Action;
          import org.apache.struts.action.ActionForm;
          import org.apache.struts.action.ActionForward;
          import org.apache.struts.action.ActionMapping;
          
          import unittest.simple.ExternalInf;
          
          public class AnotherAction extends Action {
             public ActionForward execute(
                              ActionMapping mapping, 
                              ActionForm form,
                              HttpServletRequest request, 
                              HttpServletResponse response)
                              throws Exception {
                String name = (String)request.
                                      getAttribute("name");
                ExternalInf inf = getExternalInf();
                request.setAttribute("name", name + 
                                    inf.doSomeExtThing(1));
                return mapping.findForward("succeed");
             }
          
             protected ExternalInf getExternalInf() {
                ExternalInf inf = null;
                //do some operation to get the 
                //external interface
                //Use JNDI operation or something else
                return inf;
             }
          } 

          Write a new test case to test this complicated scenario:

          //AnotherStrutsTest.java
          package unittest.struts;
          
          import javax.servlet.RequestDispatcher;
          
          import org.apache.cactus.WebRequest;
          import org.apache.struts.action.Action;
          import org.apache.struts.action.ActionForm;
          import org.apache.struts.action.ActionMapping;
          import org.easymock.MockControl;
          import org.jingle.unittest.struts.*;
          
          import unittest.simple.ExternalInf;
          
          import com.meterware.httpunit.WebForm;
          import com.meterware.httpunit.WebResponse;
          
          public class AnotherStrutsTest 
                            extends StrutsServletTestCase {
             ...
             //Same as what in SimpleStrutsTest
          
             //Prepare the AnotherAction
             public Action prepareActionAnotherAction(
                                   ActionMapping mapping) {
                //define the behavior of mock object
                controller.reset();
                inf.doSomeExtThing(1);
                controller.setReturnValue("Little");
                controller.replay();
                
                //Use override technology to bridge the 
                //mock object to the class to be tested
                AnotherAction action=new AnotherAction() {
                   protected ExternalInf getExternalInf() {
                      return inf;
                   }
                };
                return action;
             }
             ...
          
             //verify the mock object after the execution 
             //of action
             public ActionResult 
                              endActionStrutsTestAction() {
                controller.verify();
                //Introduce next join point "AnotherAction"
                return new ActionResult("AnotherAction");
             }    
          
             //compare the result html documents
             public void endStrutsTestAction(
                                    WebResponse response) {
                try {
                   WebForm form = response.getForms()[0];
                   assertEquals(
                           "DennisGreatLittle", 
                           form.getParameterValue("name"));
                } catch (Exception e) {
                   fail("Unexpected exception: " + e);
                }
             }
          } 






          StrutsUT AspectJ Solution

          Compared to the traditional solution, the AspectJ solution is more transparent to the test case writer. There's no extension to the Struts central controller and test case base class, just an added aspect: StrutsTestCaseAspect.

          Refer to the References section at the end of the article to learn more about AspectJ.

          // StrutsTestCaseAspect.java
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          
          import org.apache.struts.action.ActionForm;
          import org.apache.struts.action.ActionForward;
          import org.apache.struts.action.ActionMapping;
          import org.apache.cactus.ServletTestCase;
          import org.apache.struts.action.Action;
          import org.jingle.unittest.struts.util.*;
          import org.jingle.unittest.struts.*;
          
          public aspect StrutsTestCaseAspect {
              
            pointcut setup(ServletTestCase testcase) 
            : execution(protected void org.apache.cactus.ServletTestCase+.setUp())
              && this(testcase);
              
            pointcut teardown(ServletTestCase testcase) 
            : execution(protected void org.apache.cactus.ServletTestCase+.tearDown())
              && this(testcase);
              
            pointcut actionExecute(ActionMapping mapping, ActionForm form, 
                     HttpServletRequest request, HttpServletResponse response) 
            : execution(public ActionForward org.apache.struts.action.Action+.execute
                 (ActionMapping, ActionForm, HttpServletRequest, HttpServletResponse))
              && args(mapping, form, request, response);
              
            after(ServletTestCase testcase) 
            : setup(testcase) 
              && (!cflowbelow(setup(ServletTestCase))) 
              && (!within(org.jingle.unittest.struts.StrutsServletTestCase+)) {
                 ... //add extra info about the test point and test case instance
            }
              
            before(ServletTestCase testcase)
            : teardown(testcase) 
              && (!cflowbelow(teardown(ServletTestCase))) 
              && (!within(org.jingle.unittest.struts.StrutsServletTestCase+)) {
                 ... //clear the extra info
            }
              
            ActionForward around(ActionMapping mapping, 
                                 ActionForm form, 
                                 HttpServletRequest request, 
                                 HttpServletResponse response) throws Exception
            : actionExecute(mapping, form, request, response) 
              && (!cflowbelow(actionExecute(ActionMapping, 
                                            ActionForm, 
                                            HttpServletRequest, 
                                            HttpServletResponse))) 
              && if (request.getAttribute(
                   StrutsTestCaseConstants.WRAPPER_ACTION_ATTR_NAME) == null) {
                ... //introduce the join points before and after the execution()
                    //in Action
            }
          }

          There are three pointcuts defined in the aspect: setup, teardown, and actionExecute.

          • The setup pointcut will be captured after the setUp() method in the test case executes, to save extra information about the test point and test case instance.

          • The teardown pointcut will be captured before tearDown()'s method execution in the test case, to clear any extra information.

          • The actionExecute pointcut will be captured during the execute() method's execution in the Action class to introduce join points.

          A Simple Test Case

          The AspectsimpleStrutsTest is almost the same as SimpleStrutsTest used in the traditional solution, except that it extends the ServletTestCase defined in the Cactus framework, instead of StrutsServletTestCase (provided by StrutsUT).

          // AspectSimpleStrutsTest.java
          package unittest.struts;
          
          ...
          public class AspectSimpleStrutsTest extends ServletTestCase {
              ... //same as SimpleStrutsTest
          }

          How to Write the Test Case

          Writing test cases in the AspectJ solution is similar to how you write them in the traditional solution, except for the first rule: each test case should extend from the ServletTestCase defined in Cactus framework.

          The more complicated test case in the AspectJ solution, AspectAnotherStrutsTest, is also available in the sample code package.

          How to Install StrutsUT

          Here are the steps to install StrutsUT:

          • Prepare the web container. It can be Tomcat, JBoss, or another web container product.

          • Install the Struts framework into your web container.

          • Install Cactus into your web container.

          • Copy StrutsUT.jar into your web application lib directory: %CONTEXT_PATH%/WEB-INF/lib.

          • If you're using mocks, copy easymock.jar into the web application lib directory.

          • If you're using the StrutsUT traditional solution, modify the Struts configuration file (it is most likely named struts-config.xml) in %CONTEXT_PATH%/WEB-INF. Search for the following lines in the file:

            <controller processorClass="org.apache.struts.action.RequestProcessor"
            </controller>

            And replace that with:

            <controller processorClass="org.jingle.unittest.struts.StrutsUnitTestRequestProcessor"
            </controller>

            If you're using the Tiles plug-in with Struts, replace the following lines:

            <controller
                processorClass="org.apache.struts.tiles.TilesRequestProcessor"
            </controller>

            with:

            <controller
                processorClass="org.jingle.unittest.struts.TilesUnitTestRequestProcessor"
            </controller>
          • If you're using the StrutsUT AspectJ solution, extract StrutsTestCaseAspect.java from the StrutsUT .jar file and compile with your source code and test cases using the AspectJ compiler.

          Now, the test case is ready to be launched by the JUnit test runner.

          Conclusion

          Struts is a widely used framework for web applications. Cactus is a web application unit-test framework. They are two sharp weapons for web application developers, but there are some limitations when unit testing Struts applications using Cactus. StrutsUT provides two solutions to solve this problem by introducing a join point into the Struts execution flow. The traditional solution extends the Struts Request Processor and Cactus test-case base class. The AspectJ solution keeps the Cactus and Struts framework untouched but acquires the source code and test cases compiled with the AspectJ compiler. In the future, this limitation will be removed if the AspectJ compiler supports runtime weaving.

          References

          Lu Jian is a senior Java architect/developer with four years of Java development experience.


          posted on 2005-03-20 18:21 jinfeng_wang 閱讀(1461) 評(píng)論(0)  編輯  收藏 所屬分類: structs

          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 津市市| 广安市| 特克斯县| 饶阳县| 怀远县| 信丰县| 古丈县| 淮滨县| 双城市| 凌海市| 加查县| 景洪市| 临沂市| 盖州市| 綦江县| 磐安县| 陕西省| 宁国市| 新绛县| 冕宁县| 石楼县| 仙桃市| 宁蒗| 文水县| 丹巴县| 铜川市| 灵寿县| 嘉兴市| 虞城县| 天全县| 旬邑县| 吴堡县| 宁晋县| 容城县| 七台河市| 凤山市| 墨竹工卡县| 昌乐县| 杭锦后旗| 保德县| 宝兴县|