<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<!-- saved from url=(0032)http://www.onjava.com/lpt/a/5210 -->
<HTML><HEAD><TITLE>ONJava.com: Unit Test Your Struts Application</TITLE>
<META http-equiv=Content-Type content="text/html; charset=euc-kr">
<SCRIPT language=javascript 
src="http://onjava.com/common.js"></SCRIPT>

<META content=NOARCHIVE name=GOOGLEBOT>
<META content="NOINDEX, NOFOLLOW" name=ROBOTS>
<META content="MSHTML 6.00.2800.1400" name=GENERATOR></HEAD>
<BODY text=#000000 bgColor=#ffffff><A href="http://www.onjava.com/"><IMG 
height=82 alt=ONJava.com 
src="http://onjava.com/images/onjava/onjava_logo.jpg" 
width=294 align=left border=0></A> <DUMMY>&nbsp;&nbsp;&nbsp;</DUMMY> <BR 
clear=all>&nbsp;<FONT face=verdana,arial,helvetica size=1>Published on <B><A 
href="http://www.onjava.com/">ONJava.com</A></B> (<A 
href="http://www.onjava.com/">http://www.onjava.com/</A>)<BR><!--  ---------- End of PERL ---------------------  -->&nbsp;http://www.onjava.com/pub/a/onjava/2004/09/22/test-struts.html<BR>&nbsp;<A 
href="http://www.onjava.com/pub/a/general/print_code.html">See this</A> if 
you're having trouble printing code examples</FONT><BR><BR clear=all><!-- CS_PAGE_BREAK -->
<H2><IMG height=91 alt="Unit Test Your Struts Application" hspace=10 
src="http://www.onjava.com/onjava/2004/09/22/graphics/111-struts_test.gif" 
width=111 align=left border=0> Unit Test Your Struts Application</H2>by <A 
href="http://www.onjava.com/pub/au/2000">Lu Jian</A><BR>09/22/2004<BR clear=all><br>
__번역 : 오버가이__
<!--  sidebar begins  --><!--  don't move sidebars  --><!--  sidebar ends  -->
<P>Unit test는 휼륭한 개발 process의 중요한 부분을 차지한다. 많은 unit test framework들과 기술들이 있지만, 예를 들어 JUnit, Cactus, 
EasyMock, HttpUnit 등등, 종종 개발자들은 자신들의 Struts application을 unit test하는 것이 어렵다는것을 알고 있다.</P>
<P>이 기사는 <A href="http://www.onjava.com/onjava/2004/09/22/examples/strutsut.jar">StrutsUT</A>(<A href="http://jakarta.apache.org/cactus/index.html">Cactus</A> framework을 다소 확장)을 소개한다. StrutsUT는 개발자들이 느끼는 위의 문제해결를 도울수 있다. StrutsUT는 Struts application들을 unit test하기위한 두개의 방안을 제공한다: 전통적인 해결방안과 다른 하나는 AspectJ기반의 해결방안. 개발자들은 그것들의 개발 편의성을 고려하여 어느것이든 선택할수 있다.</P>
<P>이 기사는 개발자의 관점에서 Cactus의 초기개념을 설명하고 이 개념을 Struts 영역으로 한층더 확대한다. 이것이 StrutsUT의 핵심이다. 독자들은 Struts framework, JUnit, Cactus, AspectJ대해서 약간의 경험과 지식을 갖고 있어야 한다.</P>
<H3>An Overview of JUnit and Cactus</H3>
<H4>What is JUnit?</H4>
<P>JUnit은 Java class들에 관한 unit test들을 생성하고 수행하기 위한 framework이다. <I>Mock object</I>들과 override 기술을 이용하여, JUnit은 대부분 Java application들에 대한 unit test들을 수행 할수 있다. JUnit과 mock object들에 대해 더 알아보기 위해 아래 reference들을 보도록 하자. 이 기사에서는, 저자는 mock object 구현체로서 <A href="http://www.easymock.org/">EasyMock</A>을 선택했다.</P>
<P>아래는 test case를 이용하는 간단한 Java class이다.</P><PRE>
<CODE>//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;
        
        //interface를 얻어오기 위한 어떤 operation을 한다.
        //JNDI를 호출 또는 다른 어떤것
        return inf;
    }
}

//ExternalInf.java

package unittest.simple;

public interface ExternalInf {
    //n이 10과 같을때 "Great"를 return
    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();
   }

   //SimpleClass안에 foo()메소드를 Test
   public void testFoo() {
      //mock object를 정의
      MockControl controller = MockControl.
           createControl(ExternalInf.class);
      final ExternalInf inf = (ExternalInf)
           controller.getMock();
       
      //mock object의 behavior를 정의
      inf.doSomeExtThing(10);
      controller.setReturnValue("Great");
      controller.replay();
        
      //mock object로터 test를 받기 위한 class에 연결을 위해
      //override 기술을 사용
      SimpleClass instance = new SimpleClass() {
         protected ExternalInf getExternalInf() {
            return inf;
         }
      };
        
      //test 시작
      String result = instance.foo(10);
        
      //실제값과 기대값사이의 검증을 한다.
      assertEquals("Woo!Great", result);
        
      //mock object상의 검증을 한다.
      controller.verify();
   }
}</CODE></PRE>
<P>위 예제에서, 우리는 외부 interface를 시뮬레이션을 하기 위해 mock object를 사용하고 테스트할 실제 class에 mock object를 연결하기 위해 <CODE>getExternalInf</CODE> 메소드를 override한다. 이 기사에서, 위 두개의 기술들을 다음과 같이 "traditional unit-test technologies"라고 저자는 명명것이다. </P>
<H4>The Problems</H4>
<P>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:</P>
<UL>
  <LI>
  <P>It involves a large workload. </P>
  <P>We need to mock up the <CODE>HttpServletRequest</CODE>, 
  <CODE>HttpServletResponse</CODE>, and <CODE>Session</CODE> objects. This is a 
  tedious and error-prone task.</P>
  <LI>
  <P>It is difficult to compare the expected and actual results.</P>
  <P>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.</P></LI></UL>
<H4>Why Cactus?</H4>
<P>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.</P>
<P>Cactus has a two-part unit test framework. One part is the traditional JUnit 
framework. There is an additional <CODE>WebRequest</CODE>, 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 
<CODE>WebRequest</CODE> to a real <CODE>HttpServletRequest</CODE>. 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 <CODE>EJB</CODE>, <CODE>TagLib</CODE>, 
<CODE>Servlet</CODE>, or <CODE>Filter</CODE> class) to be tested or forward the 
<CODE>HttpServletRequest</CODE> to the real web resource (a specific servlet or 
JSP page).</P>
<P>Figure 1 shows a high-level view of Cactus' traditional unit-testing 
architecture.</P>
<P><IMG height=226 alt="Cactus Architecture" 
src="http://www.onjava.com/onjava/2004/09/22/graphics/archi.gif" 
width=434><BR><I>Figure 1. The traditional Cactus test case execution 
flow</I></P>
<P>Cactus also integrates HttpUnit to simplify the result comparison. HttpUnit 
transfers the HTML document into a <CODE>WebResponse</CODE> 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.</P>
<P>Refer to <A href="http://jakarta.apache.org/cactus/how_it_works.html">How 
Cactus Works</A> to learn more about Cactus.</P>
<P>Below is a simple servlet class with its test case.</P><PRE><CODE>// 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(
        "&lt;html&gt;&lt;head/&gt;&lt;body&gt;");
      pw.print("&lt;form name='form1'&gt;");
      if (attr.equals("normal")) {
         pw.print("&lt;input type=button 
                   name='button1' 
                   value='Click me'/&gt;");
      } else {
          pw.print("&lt;input type=button 
                    name='button1' 
                    value='Click me' 
                    disabled/&gt;");
      }
      pw.print("&lt;/form&gt;");
      pw.print("&lt;/body&gt;&lt;/html&gt;");
   }
} 

//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);
      }
   }
}</CODE></PRE>
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>
  <TBODY>
  <TR>
    <TD>
      <P class=secondary><!-- CS_PAGE_INDEX --></P></TD>
    <TD>
      <P class=secondary align=right><A 
      href="http://www.onjava.com/lpt/a/<!--CS_NEXT_REF-->"></A></P></TD></TR></TBODY></TABLE><!-- CS_PAGE_BREAK -->
<P><!-- CS_PAGE_INDEX --></P>
<H3>The Problems With Unit Testing a Struts Application</H3>
<H4>What is Struts?</H4>
<P>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 <A 
href="http://struts.apache.org/">struts.apache.org</A> to learn more about 
Struts.</P>
<P>Below are a simple Struts configuration file and its related 
<CODE>Action</CODE> and <CODE>Form</CODE>, as well as its JSP file.</P><PRE><CODE>struts-config.xml

&lt;?xml version="1.0" encoding="ISO-8859-1" ?&gt;
&lt;!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"&gt;
&lt;struts-config&gt;

  ...
  &lt;form-beans&gt;
    &lt;form-bean 
              name="simpleForm"
              type="unittest.struts.SimpleForm"/&gt;
  &lt;/form-beans&gt;

  &lt;action-mappings&gt;
    &lt;action    
              path="/strutsTest"
              type="unittest.struts.SimpleAction"
              name="simpleForm"
              scope="request"&gt;
     &lt;forward 
              name="succeed" 
              path="/result.jsp"/&gt;
    &lt;/action&gt;
  &lt;/action-mappings&gt;
  ...

&lt;/struts-config&gt;


//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

&lt;%@ page contentType=
                "text/html; charset=UTF-8"%&gt;
&lt;%@ taglib uri="/WEB-INF/struts-bean.tld" 
           prefix="bean" %&gt;

&lt;html&gt;
&lt;body&gt;
&lt;form name="form1"&gt;
  The name is &lt;input type="text" name="name" 
          value='&lt;bean:write name="name"/&gt;'/&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</CODE></PRE>
<H4>The Problems</H4>
<P>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:</P>
<UL>
  <LI>
  <P><B>Using Cactus to test a single <CODE>Action</CODE> class</B></P>
  <P>It is possible to use Cactus to unit test the <CODE>execute()</CODE> method 
  in the <CODE>Action</CODE> class. Test case writers can continue to use the 
  mock object and override approaches. However, they have to construct the 
  <CODE>ActionMapping</CODE> object by themselves. <CODE>ActionMapping</CODE> 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.</P>
  <P>Below is a test case for <CODE>SimpleAction</CODE>.</P><PRE><CODE>//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);
      }
   }
} </CODE></PRE>
  <LI>
  <P><B>Using Cactus for an end-to-end test</B></P>
  <P>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.</P></LI></UL>
<P>Both of these approaches have serious limitations. The first approach uses 
traditional unit test technologies, but has limited scope. It can only test the 
<CODE>Action</CODE> 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.</P>
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>
  <TBODY>
  <TR>
    <TD>
      <P class=secondary><!-- CS_PAGE_INDEX --></P></TD>
    <TD>
      <P class=secondary align=right><A 
      href="http://www.onjava.com/lpt/a/<!--CS_NEXT_REF-->"></A></P></TD></TR></TBODY></TABLE><!-- CS_PAGE_BREAK -->
<P><!-- CS_PAGE_INDEX --></P>
<H4>A Simple Solution</H4>
<P>Why not combine the two approaches together? Since Struts has done the job of 
constructing the <CODE>ActionMapping</CODE> 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 
<CODE>execute()</CODE> method in the <CODE>Action</CODE> class that is called by 
Struts. Test case writers can make use of this join point to prepare the 
<CODE>ActionForm</CODE> and use traditional unit test technologies to prepare an 
<CODE>Action</CODE> class that uses external interfaces.</P>
<P>The idea is to extend the Cactus framework's "in-container" part to interact 
with the test case <B>two</B> times in the web container. One is called by the 
Cactus-specific servlet, <I>ServletRedirector</I>, 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. </P>
<H3>Introducing StrutsUT</H3>
<P>The solution presented here, <A 
href="http://www.onjava.com/onjava/2004/09/22/examples/strutsut.jar">StrutsUT</A>, 
provides such an extension to help unit test Struts applications. Here's how it 
works:</P>
<OL>
  <LI>
  <P>A client-side test runner creates the test case instance and initiates it 
  by calling the <CODE>begin()</CODE> method. For each test point 
  <CODE><I>XXX</I></CODE> in the test case, it calls the 
  <CODE>begin<I>XXX</I>()</CODE> method to prepare request parameters and/or 
  request headers.</P>
  <LI>
  <P>The client sends the request to the server-side Cactus redirect 
servlet.</P>
  <LI>
  <P>The redirect servlet creates the test case instance on the server side 
  according to the information from request, and assigns the 
  <CODE>HttpServletRequest</CODE>, <CODE>HttpServletResponse</CODE>, and 
  <CODE>HttpSession</CODE> to the test case public fields.</P>
  <LI>
  <P>The redirect servlet calls the <CODE>setUp()</CODE> method in the test case 
  to satisfy the test precondition and calls <CODE>test<I>XXX</I>()</CODE> to 
  launch the test process.</P>
  <LI>
  <P>The request is redirected to the Struts <CODE>RequestProcessor</CODE>.</P>
  <LI>
  <P><CODE>RequestProcessor</CODE> uses the same test case instance and calls 
  <CODE>prepareFrom<I>XXX</I>()</CODE> and 
  <CODE>prepareAction<I>XXX</I>()</CODE> to prepare the <CODE>ActionForm</CODE> 
  and <CODE>Action</CODE> instance.</P>
  <LI>
  <P>The <CODE>RequestProcessor</CODE> calls the <CODE>execute()</CODE> method 
  in <CODE>Action</CODE>.</P>
  <LI>
  <P>The <CODE>RequestProcessor</CODE> calls <CODE>endAction<I>XXX</I>()</CODE> 
  method in the test case to do any necessary verification and prepare the next 
  join point, if needed.</P>
  <LI>
  <P>The Struts framework finishes the remaining operations and returns the 
  control flow.</P>
  <LI>
  <P>The Cactus redirect servlet calls the <CODE>tearDown()</CODE> method in the 
  test case to clear the test environment.</P>
  <LI>
  <P>The Cactus redirect servlet finishes the test case invocations.</P>
  <LI>
  <P>The Cactus redirect servlet returns the response to client-side test 
  runner.</P>
  <LI>
  <P>The client-side test runner calls the <CODE>end<I>XXX</I>()</CODE> method 
  in the test case to verify the response for each test point 
  <CODE><I>XXX</I></CODE>, and calls the <CODE>end()</CODE> method to clear the 
  status of the test case.</P></LI></OL>
<P>Figure 2 shows the StrutsUT test case execution flow. </P>
<P><IMG height=240 alt="Figure 2" 
src="http://www.onjava.com/onjava/2004/09/22/graphics/architecture.gif" 
width=550><BR><EM>Figure 2. StrutsUT test case execution flow</EM></P>
<P>With StrutsUT, test case writers now can do more in the test case: </P>
<UL>
  <LI>
  <P>Use <CODE>prepareForm<I>XXX</I>()</CODE> method to prepare the 
  <CODE>ActionForm</CODE>, which will be the argument the <CODE>execute()</CODE> 
  method in the <CODE>Action</CODE> class.</P>
  <LI>
  <P>Use the <CODE>prepareAction<I>XXX</I>()</CODE> method to prepare the 
  <CODE>Action</CODE> instance to be called. </P>
  <LI>
  <P>Use the <CODE>endAction<I>XXX</I>()</CODE> method to do any necessary 
  verification and prepare the next join point, if needed, after calling 
  <CODE>Action</CODE>'s <CODE>execute()</CODE> method.</P></LI></UL>
<P>Like the extra methods in Cactus' 
<CODE>ServletTestCase</CODE>--<CODE>begin()</CODE>, 
<CODE>begin<I>XXX</I>()</CODE>, <CODE>end<I>XXX</I>()</CODE>, 
<CODE>end()</CODE>, <CODE>setUp()</CODE>, and <CODE>tearDown()</CODE>--it is not 
mandatory to provide these extra methods. Use them when needed.</P>
<P>There are two implementations in StrutsUT to satisfy the idea described 
above.</P>
<H3>The StrutsUT Traditional Solution</H3>
<P>In order to insert such a join point within the control flow of Struts, it is 
necessary to extend Struts' central controller, <CODE>RequestProcessor</CODE>, 
to interact with the test case. We also have to extend Cactus' test case base 
class, <CODE>ServletTestCase</CODE>, 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.</P>
<P>StrutsUT replaces the Struts central controller, 
<CODE>RequestProcessor</CODE>, with a subclass called 
<CODE>StrutsUnitTestRequestProcessor</CODE>, and uses 
<CODE>StrutsServletTestCase</CODE> to replace Cactus' 
<CODE>ServletTestCase</CODE> as the test case base class.</P>
<H4>A Simple Test Case</H4><PRE><CODE>// 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);
      }
   }
} </CODE></PRE>
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>
  <TBODY>
  <TR>
    <TD>
      <P class=secondary><!-- CS_PAGE_INDEX --></P></TD>
    <TD>
      <P class=secondary align=right><A 
      href="http://www.onjava.com/lpt/a/<!--CS_NEXT_REF-->"></A></P></TD></TR></TBODY></TABLE><!-- CS_PAGE_BREAK -->
<P><!-- CS_PAGE_INDEX --></P>
<H4>How to Write the Test Case</H4>
<P 
style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-SIZE: 1px; PADDING-BOTTOM: 0px; MARGIN: 0px; PADDING-TOP: 0px"></P>
<P>Here are the key points of how to write the test case.</P>
<UL>
  <LI>
  <P>Each test case should extend from <CODE>StrutsServletTestCase</CODE>.</P>
  <LI>
  <P>Define a <CODE>public ActionForm 
  prepareForm<I>XXX</I>(ActionMapping)</CODE> method if you want to prepare the 
  <CODE>ActionForm</CODE> instance.</P>
  <LI>
  <P>Define a <CODE>public Action prepareAction<I>XXX</I>(ActionMapping)</CODE> 
  method if you want to prepare the Action instance.</P>
  <LI>
  <P>Define a <CODE>public ActionResult endAction<I>XXX</I>()</CODE> method if 
  you want to verify the mock object and/or introduce the next join point. The 
  return value of this method is an <CODE>ActionResult</CODE> object that 
  contains a <CODE>String</CODE> 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 <CODE>ActionResult</CODE> contains the string <CODE>YYY</CODE>, the 
  Struts framework will try to call <CODE>prepareForm<I>YYY</I>()</CODE> and 
  <CODE>prepareAction<I>YYY</I>()</CODE> before calling the next 
  <CODE>Action</CODE>, and call <CODE>endAction<I>YYY</I>()</CODE> after the 
  <CODE>Action</CODE> execution. The next example gives you a more complicated 
  scenario and test case. There are two predefined <CODE>ActionResult</CODE> 
  instances: <CODE>TERMINATE</CODE> and <CODE>CONTINUE</CODE>. 
  <CODE>TERMINATE</CODE> simply terminates the process of Struts and return the 
  control flow. <CODE>CONTINUE</CODE> or <CODE>null</CODE> will continue the 
  Struts operation as usual.</P></LI></UL>
<H4>A More Complicated Example</H4>
<P>Change the Struts configuration file to make the <CODE>Action</CODE> forward 
more complicated.</P><PRE><CODE>//struts-config.xml

    ...
    &lt;action  
              path="/strutsTest"
              type="unittest.struts.SimpleAction"
              name="simpleForm"
              scope="request"&gt;
      &lt;forward 
              name="succeed"              
        <B><I>path="/anotherTest.do"/&gt;
    &lt;/action&gt;

    &lt;action    
              path="/anotherTest"
              type="unittest.struts.AnotherAction"&gt;
      &lt;forward 
              name="succeed"              
              path="/result.jsp"/&gt;
    &lt;/action&gt;  </I></B>
    ...

&lt;/struts-config&gt;</CODE></PRE>
<P>Introduce an extra <CODE>Action</CODE> into the scenario:</P><PRE><CODE>//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;
   }
} </CODE></PRE>
<P>Write a new test case to test this complicated scenario:</P><PRE><CODE>//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
   <B><I>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;
   }</I></B>
   ...

   //verify the mock object after the execution 
   //of action
   public ActionResult 
                    endActionStrutsTestAction() {
      controller.verify();
      //Introduce next join point "AnotherAction"
      return new <B><I>ActionResult("AnotherAction")</I></B>;
   }    

   //compare the result html documents
   public void endStrutsTestAction(
                          WebResponse response) {
      try {
         WebForm form = response.getForms()[0];
         <B><I>assertEquals(
                 "DennisGreatLittle", 
                 form.getParameterValue("name"));</I></B>
      } catch (Exception e) {
         fail("Unexpected exception: " + e);
      }
   }
} </CODE></PRE>
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>
  <TBODY>
  <TR>
    <TD>
      <P class=secondary><!-- CS_PAGE_INDEX --></P></TD>
    <TD>
      <P class=secondary align=right><A 
      href="http://www.onjava.com/lpt/a/<!--CS_NEXT_REF-->"></A></P></TD></TR></TBODY></TABLE><!-- CS_PAGE_BREAK -->
<P><!-- CS_PAGE_INDEX --></P>
<H3>StrutsUT AspectJ Solution</H3>
<P>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: 
<CODE>StrutsTestCaseAspect</CODE>. </P>
<P>Refer to the <A href="http://www.onjava.com/lpt/a/5210#resources">References 
section</A> at the end of the article to learn more about AspectJ. </P><PRE><CODE>// 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())
    &amp;&amp; this(testcase);
    
  pointcut teardown(ServletTestCase testcase) 
  : execution(protected void org.apache.cactus.ServletTestCase+.tearDown())
    &amp;&amp; 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))
    &amp;&amp; args(mapping, form, request, response);
    
  after(ServletTestCase testcase) 
  : setup(testcase) 
    &amp;&amp; (!cflowbelow(setup(ServletTestCase))) 
    &amp;&amp; (!within(org.jingle.unittest.struts.StrutsServletTestCase+)) {
       ... //add extra info about the test point and test case instance
  }
    
  before(ServletTestCase testcase)
  : teardown(testcase) 
    &amp;&amp; (!cflowbelow(teardown(ServletTestCase))) 
    &amp;&amp; (!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) 
    &amp;&amp; (!cflowbelow(actionExecute(ActionMapping, 
                                  ActionForm, 
                                  HttpServletRequest, 
                                  HttpServletResponse))) 
    &amp;&amp; if (request.getAttribute(
         StrutsTestCaseConstants.WRAPPER_ACTION_ATTR_NAME) == null) {
      ... //introduce the join points before and after the execution()
          //in Action
  }
}</CODE></PRE>
<P>There are three pointcuts defined in the aspect: <CODE>setup</CODE>, 
<CODE>teardown</CODE>, and <CODE>actionExecute</CODE>. </P>
<UL>
  <LI>
  <P>The <CODE>setup</CODE> pointcut will be captured after the 
  <CODE>setUp()</CODE> method in the test case executes, to save extra 
  information about the test point and test case instance.</P>
  <LI>
  <P>The <CODE>teardown</CODE> pointcut will be captured before 
  <CODE>tearDown()</CODE>'s method execution in the test case, to clear any 
  extra information.</P>
  <LI>
  <P>The <CODE>actionExecute</CODE> pointcut will be captured during the 
  <CODE>execute()</CODE> method's execution in the <CODE>Action</CODE> class to 
  introduce join points.</P></LI></UL>
<H4>A Simple Test Case</H4>
<P>The <CODE>AspectsimpleStrutsTest</CODE> is almost the same as 
<CODE>SimpleStrutsTest</CODE> used in the traditional solution, except that it 
extends the <CODE>ServletTestCase</CODE> defined in the Cactus framework, 
instead of <CODE>StrutsServletTestCase</CODE> (provided by StrutsUT).</P><PRE><CODE>// AspectSimpleStrutsTest.java
package unittest.struts;

...
public class AspectSimpleStrutsTest extends ServletTestCase {
    ... //same as SimpleStrutsTest
}</CODE></PRE>
<H4>How to Write the Test Case</H4>
<P>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 <CODE>ServletTestCase</CODE> defined in Cactus framework.</P>
<P>The more complicated test case in the AspectJ solution, 
<CODE>AspectAnotherStrutsTest</CODE>, is also available in the sample code 
package.</P>
<H3>How to Install StrutsUT</H3>
<P>Here are the steps to install StrutsUT:</P>
<UL>
  <LI>
  <P>Prepare the web container. It can be Tomcat, JBoss, or another web 
  container product.</P>
  <LI>
  <P>Install the Struts framework into your web container.</P>
  <LI>
  <P>Install Cactus into your web container.</P>
  <LI>
  <P>Copy <I>StrutsUT.jar</I> into your web application <I>lib</I> directory: 
  <CODE>%CONTEXT_PATH%/WEB-INF/lib</CODE>.</P>
  <LI>
  <P>If you're using mocks, copy <I>easymock.jar</I> into the web application 
  <I>lib</I> directory.</P>
  <LI>
  <P>If you're using the StrutsUT traditional solution, modify the Struts 
  configuration file (it is most likely named <I>struts-config.xml</I>) in 
  <CODE>%CONTEXT_PATH%/WEB-INF</CODE>. Search for the following lines in the 
  file:</P><PRE><CODE>&lt;controller processorClass="org.apache.struts.action.RequestProcessor"
&lt;/controller&gt;</CODE></PRE>
  <P>And replace that with:</P><PRE><CODE>&lt;controller processorClass="org.jingle.unittest.struts.StrutsUnitTestRequestProcessor"
&lt;/controller&gt;</CODE></PRE>
  <P>If you're using the Tiles plug-in with Struts, replace the following 
  lines:</P><PRE><CODE>&lt;controller
    processorClass="org.apache.struts.tiles.TilesRequestProcessor"
&lt;/controller&gt;</CODE></PRE>
  <P>with:</P><PRE><CODE>&lt;controller
    processorClass="org.jingle.unittest.struts.TilesUnitTestRequestProcessor"
&lt;/controller&gt;</CODE></PRE>
  <LI>
  <P>If you're using the StrutsUT AspectJ solution, extract 
  <I>StrutsTestCaseAspect.java</I> from the StrutsUT .jar file and compile with 
  your source code and test cases using the AspectJ compiler.</P></LI></UL>
<P>Now, the test case is ready to be launched by the JUnit test runner.</P>
<H3>Conclusion</H3>
<P>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.</P>
<H3><A name=resources>References</A></H3>
<UL>
  <LI><A 
  href="http://www.onjava.com/onjava/2004/09/22/examples/strutsut.jar">Download 
  StrutsUT</A> 
  <LI><A 
  href="http://www.onjava.com/onjava/2004/09/22/examples/samples.jar">Download 
  the sample code in this article</A> 
  <LI><A href="http://www.junit.org/index.htm">JUnit</A> 
  <LI><A href="http://www.easymock.org/">EasyMock</A> 
  <LI><A href="http://jakarta.apache.org/cactus/index.html">Cactus</A> 
  <LI><A href="http://struts.apache.org/">Struts</A> 
  <LI><A href="http://httpunit.sourceforge.net/">HttpUnit</A> 
  <LI><A href="http://www.sys-con.com/story/print.cfm?storyid=45834">Using 
  Apache Cactus</A> 
  <LI><A href="http://eclipse.org/aspectj">AspectJ</A> </LI></UL>
<P><I><A href="http://www.onjava.com/pub/au/2000">Lu Jian</A> is a senior Java 
architect/developer with 4 years of Java development experience. </I></P>
<HR noShade SIZE=1>

<P>Return to <A href="http://www.onjava.com/">ONJava.com</A>.</P>
<P><FONT face=verdana,arial,helvetica size=1>Copyright ?2004 O'Reilly Media, 
Inc.</FONT></P></BODY></HTML>