December 2003
[ vmassol ] 16:11, Sunday, 21 December 2003

Cactus v2 architecture

Rationale

Why a new architecture? Several reasons:

  • The existing architecture is restricted to testing Servlet components (and its variations: Taglibs, Filters, JSP). We've tried to create an SPI so that implementations for other containers can be written but it is not possible with the current architecture.
  • We'd like to make Cactus the de facto tool for performing integration unit testing (aka in-container testing) for any type of component in any type of container. See figure 1.
  • We'd like to make it easy for others to create Cactus extensions. This is not currently possible with the existing architecture.
  • We'd like to maximize the reusability of other testing tools. For example, instead of implementing in Cactus the HTTP layer that calls the server side, we'd like the user to use his favorite tool (e.g. HttpUnit). For unit testing Message-Driven Beans, the user will be able to use his favorite JMS injector (e.g. Commons Messenger), etc. This will allow leveraging all the features in those tools (for example, support for HTTPS in HttpUnit, support for Cookie handling, etc).
  • We'd like to standardize on a server side interception mechanism instead of inventing our own.
Figure 1: Scope of Cactus v2
Figure 1: Scope of Cactus v2

Architecture choices

These are the high-level architecture choices on which Cactus v2 will be built:

  • An AOP framework for server-side interception. There are 2 possible contenders: AspectJ or AspectWerkz. We are currently favorizing AspectWerkz because it allows to write test cases in Java. AspectJ extends the Java language and requires strong tool support. It will change with the advent of JDK 1.5 as the JDK will become meta-data compatible and Aspect will probably take advantage of this. However, it will take several years before everyone is on JDK 1.5 and we need a solution before this.
  • Cactus v2 will continue to be a JUnit extension. A Cactus v2 test case will be a JUnit test case and thus any JUnit test runner will work (provided the server has been started and the components and tests deployed).

High level architecture

The Cactus system is composed of 3 parts (see figure 2):

  • A Cactus test case, which is a combination of a JUnit test case (with testXXX() methods executed on the client side) and aspects used on the server side to perform interception and/or validation on the server side (see figure 3). More specifically, 3 typical uses cases for these aspects are:
    • Intercept the call to the component under test and redirect the flow of execution to a specific method to unit test it,
    • Prevent the flow of execution to call some subsystem. For example, stop the flow of execution before it goes to the database and instead return canned values.
    • Perform asserts to verify server-side expectations. For example, verify that the Servlet HTTP Session contains such and such values after executing such method, verify that the Database connection is closed as many times as it is open for such and such use case, verify that the number of SQL queries is below such number (e.g. less than 10 SQL queries per use case), etc. These are server-side expectations.
  • A Cactus runner to execute Cactus tests automatically. This involves starting the container, deploying the application and tests in the container, starting the tests and stopping the container.
  • A Cactus framework to support starting a test case on the client side and continuing it on the server side, and also to support transferring test results from the server side to the client side so that results can be displayed in the executing JUnit test runner. This framework also contains helper aspects and classes to help write test cases.
Figure 2: High-level architecture
Figure 2: High-level architecture

Cactus test case example

Here is an example of a typical Cactus test case using AspectWerkz 0.9. Please note that this example is a work in progress and is non-functional at this stage. We're also working towards simplifying the syntax for test case writers:

Figure 3: Cactus test case sample
package org.apache.cactus.sample.servlet;

import java.util.Hashtable;

import javax.servlet.http.HttpServletRequest;

import org.codehaus.aspectwerkz.attribdef.Pointcut;
import org.codehaus.aspectwerkz.attribdef.aspect.Aspect;
import org.codehaus.aspectwerkz.joinpoint.JoinPoint;
import org.codehaus.aspectwerkz.joinpoint.MethodJoinPoint;

import com.meterware.httpunit.GetMethodWebRequest;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebRequest;
import com.meterware.httpunit.WebResponse;

import junit.framework.TestCase;

public class TestSampleServletAspectWerkz extends TestCase
{
     /**
      * Intercepts Servlet's doXXX calls and instead redirect the flow of
      * execution to the {@link SampleServlet#getRequestParameters} method to
      * unit test.
      * 
      * @Aspect
      */
     public static class GetRequestParametersTestAdvice extends Aspect
     {
          /**
           * @Execution * *..SampleServlet.do*(..)
           */
          Pointcut interceptServlet;
          
          /**
           * @Around interceptServlet
           */
          public Object catchGetRequestParameters(JoinPoint joinPoint) 
              throws Throwable
          {
               MethodJoinPoint jp = (MethodJoinPoint) joinPoint;
               SampleServlet servlet = (SampleServlet) jp.getTargetInstance();
               Hashtable params = servlet.getRequestParameters(
                   (HttpServletRequest) jp.getParameters()[0]);
               assertNotNull(params.get("param1"));
               assertNotNull(params.get("param2"));
               assertEquals("value1", params.get("param1"));
               assertEquals("value2", params.get("param2"));
               return null;
           }
      }
 
     /**
      * Test {@link SampleServlet#getRequestParameters} by calling the server 
      * side using HttpUnit. On the server side, our aspect will kick in and
      * the {@link GetRequestParametersTestAdvice#catchGetRequestParameters} 
      * test method will be called to unit test our method.    
      */
     public void testGetRequestParameters() throws Exception
     {
          WebConversation conversation = new WebConversation();
          WebRequest request = new GetMethodWebRequest(
              "http://localhost:8080/test/SampleServlet?param1=value1&param2=value2");
          WebResponse response = conversation.getResponse(request);
      }    
}

We would like to be able to write the following (not yet supported by AspectWerkz but we've had commitment from the AW team that they will make modifications to support it! :-)). The difference with the previous sample is the removal of the inner aspect class + the typed poincut interception.

Figure 4: Ideal Cactus test case sample
package org.apache.cactus.sample.servlet;

import java.util.Hashtable;

import javax.servlet.http.HttpServletRequest;

import org.codehaus.aspectwerkz.attribdef.Pointcut;
import org.codehaus.aspectwerkz.joinpoint.JoinPoint;
import org.codehaus.aspectwerkz.joinpoint.MethodJoinPoint;

import com.meterware.httpunit.GetMethodWebRequest;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebRequest;
import com.meterware.httpunit.WebResponse;

import junit.framework.TestCase;

public class TestSampleServletAspectWerkz extends TestCase
{
     /**
      * @Execution * *..SampleServlet.do*(..)
      * @And @Target(SampleServlet)
      * @And @Args(HttpServletRequest)
      */
     Pointcut interceptServlet;
         
     /**
      * @Around interceptServlet
      */
     public void catchGetRequestParameters(SampleServlet servlet,
         HttpServletRequest request) throws Throwable
     {
          Hashtable params = servlet.getRequestParameters(request);
          assertNotNull(params.get("param1"));
          assertNotNull(params.get("param2"));
          assertEquals("value1", params.get("param1"));
          assertEquals("value2", params.get("param2"));
      }
 
     /**
      * Test {@link SampleServlet#getRequestParameters} by calling the server 
      * side using HttpUnit. On the server side, our aspect will kick in and
      * the {@link #catchGetRequestParameters} test method will be called 
      * to unit test our method.    
      */
     public void testGetRequestParameters() throws Exception
     {
          WebConversation conversation = new WebConversation();
          WebRequest request = new GetMethodWebRequest(
              "http://localhost:8080/test/SampleServlet?param1=value1&param2=value2");
          WebResponse response = conversation.getResponse(request);
      }    
}

Detailed design

The detailed design of Cactus v2 is shown in figure 5 below.

Figure 5: Detailed design
Figure 5: Detailed design

It works as follows:

  1. The Cactus tests are started by a JUnit Test Runner (any JUnit Test Runner).
  2. The Cactus framework intercepts the JUnit call to the test case runBare() method. It checks if a listener socket has been set up. If not it sets up one. It passes the test name to it (so that the server side can later on find out what test is currently being executed),
  3. It calls the test case testXXX() method. In this method the test case writer has written the logic to call the server side (using any existing framework; for example HttpUnit for calling an HTTP service),
  4. The flow of execution reaches the application to test on the server side,
  5. Somewhere during the execution of the application, the test aspect defined by the test case writer kicks in. Before that aspect is executed, the Cactus framework intercepts it,
  6. The Cactus server side interceptor then calls the listener socket set up in step 2 to get the name of the test being executed (the test that was started on the client side). It checks if the aspect matches the current test,
  7. If the aspect matches, its advice is executed, performing whatever logic the test case writer has put in it,
  8. Before the call returns to the client side, the Cactus server side interceptor calls the socket listener to pass to it the server side test result (it passes to it any exception raised on the server side; for example AssertionFailedError exceptions),
  9. After the testXXX() method finishes its execution and before the test result is communicated to the JUnit Test Runner, the Cactus client side interceptor verifies if any error has been reported by the server side execution. If so, it rethrows the server side exception to the JUnit Test Runner. Otherwise it lets the result of testXXX() bubble up to the JUnit Test Runner.

Some additional comments/ideas:

  • If one of the catchXXX() methods is not called, it should result in an AssertionFailedError being raised. This is to prevent not executing server-side test code without knowing it. As we are using interception, I guess it's easy to make a mistake when defining the join point and thus we need this safeguard.

Challenges

The following challenges await us:

  • Being able to make the Cactus test case easy to write for test case writers,
  • Make the execution of Cactus test case easily executable. Runtime code weaving would be nice but is not supported by old JVMs. We will probably have a mixed model as is being supported by AspectWerkz.
  • Find out if integration with Chad's VirtualMock is possible/desirable.

Please challenge us to improve our design! :-)

Disclaimer: Please also note that, at this point in time, this architecture and ideas are only mine and do not represent (yet!) the official view of the Cactus project. I am proposing it to the Cactus project members.

[ vmassol ] 12:46, Monday, 1 December 2003

For French people or anyone visiting Paris at that time, I'll be doing some book signing for JUnit in Action on the 20th of December 2003, at the Le Monde en "Tique" bookshop. From 15:00 to 18:00.

If you're interested to discuss a bit about unit testing, JUnit, TDD, Cactus, Maven, Ant, open source, Apache/Jakarta, etc come and join me!

For those in other countries, sorry! I don't think Manning will pay for the trip. That's unless JUnit in Action becomes a bestseller of course! You know what you have to do! :-)

[ vmassol ] 12:29, Monday, 1 December 2003

Currently the Cactus project is a framework to help unit test J2EE components (and mostly Servlet/JSP/Taglib).

I'd like to expand its goal and make it a framework for building in-container testing solutions. Cactus would still offer an implementation for J2EE component testing but it will also open up an API for plugging other implementations. Some ideas are shown on the diagram below.

cactus_new_vision.jpg

For this to happen, the core helper classes will have to be separated from the HTTP protocol implementation and the existing Cactus TestCases. 2 SPIs will appear:

  • one for plugging in different protocol implementations (RMI, JMS, etc). Currently Cactus provides the HTTP implementation.
  • one for plugging in custom test case implementations (still looking for a good name for these). Currently Cactus provides the ServletTestCase, FilterTestCase and JspTestCase.

Moreover, the Cactus integration modules (aka front-ends) will also need to provide clearly-defined extension points to help automate the whole process of starting the container, deploying components, running the tests and shutting down the container.

I'm currently working on the Cactus code to make the 2 SPIs surface. The first test drive of these new SPIs will be to implement support for EJB TestCases.