|
|
Cactus v2 architecture proposal
[
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
|
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
|
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
{
public static class GetRequestParametersTestAdvice extends Aspect
{
Pointcut 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;
}
}
public void testGetRequestParameters() throws Exception
{
WebConversation conversation = new WebConversation();
WebRequest request = new GetMethodWebRequest(
"http://localhost:8080/test/SampleServlet?param1=value1¶m2=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
{
Pointcut 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"));
}
public void testGetRequestParameters() throws Exception
{
WebConversation conversation = new WebConversation();
WebRequest request = new GetMethodWebRequest(
"http://localhost:8080/test/SampleServlet?param1=value1¶m2=value2");
WebResponse response = conversation.getResponse(request);
}
}
|
Detailed design
The detailed design of Cactus v2 is shown in figure 5 below.
Figure 5: Detailed design
|
It works as follows:
-
The Cactus tests are started by a JUnit Test Runner (any JUnit Test Runner).
-
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),
-
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),
-
The flow of execution reaches the application to test on the server side,
-
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,
-
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,
-
If the aspect matches, its advice is executed, performing whatever logic the test case
writer has put in it,
-
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),
-
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.
I somewhat disagree with the Aspect Oriented proposal. Is there any way of getting to this architecture while avoiding that (at least until AOP is integrated into the Java standard.
|