JaVa
   

Cactus Architecture

To generate the implicit objects available in a Web server, Cactus needs to replicate the request-response cycle of HTTP communication. To do this (and also to allow verification of the response returned by the Web server), Cactus test cases are fully executed in two instances that pretend to run as one—the first on the client side and the second on the server side. The framework authors made this decision to simplify the creation of test cases: the alternative would be to have one class for the server and one for the client. This process operates so smoothly that it often appears that Cactus test cases are standard JUnit test cases. Occasionally, test execution can produce unexpected behavior because, although the two objects operate as one, they do not actually share a common state. However, with caution and a solid understanding of the Cactus architecture, writing test cases that execute in the container is a snap. In this section, we'll give you an in-depth look at the various pieces of a Cactus test case and show you how Cactus goes about executing a test case (including which steps happen where).

Extending the Appropriate Class

Cactus tests are JUnit tests. Their execution is started using JUnit's TestRunners outside the container, and in format they follow the basic JUnit test case. There are a couple of key differences, however. The most obvious is that Cactus tests extend one of the three types of Cactus TestCases (ServletTestCase, FilterTestCase, or JspTestCase). These base classes provide hooks into the Cactus framework for remote execution and specify several instance variables (such as request and response) that correspond to objects that are readily available to the redirector during execution. The presence of these objects allows you to test methods that depend on these portions of the API in question.

Beginning Execution

A Cactus test is started from a usual JUnit test runner; following is a diagram of the first stage of Cactus test case execution.

Java Click To expand

An instance of the test case is instantiated, and its test method is selected by the name passed into the constructor.

The beginXXX() Method

Based on the name of the test method (a method named testXXX()), Cactus will look for and execute a method in the TestCase named beginXXX() as shown in the following illustration. For example, if Cactus were executing the testFoo() method of your test case, it would execute your beginFoo() method during this step. The beginXXX() method must accept one argument—an org.apache.cactus.WebRequest. The WebRequest represents a client-side request that the ServletTestCase will send to the redirector servlet, and that will eventually (in a modified form) be copied into the test case's request instance variable. The beginXXX() method serves primarily to add parameters and other information to this request object. Because beginXXX() is executed in a different copy of the test case from the one that runs on the server, its only method of communicating with the server-side test execution code is through the WebRequest.

Java Click To expand

Adding Information to the Request

The WebRequest defines several methods that set up the state of the request. The Cactus API documentation contains more detailed descriptions of these methods; however, they are worth outlining here. addCookie(), addHeader(), and addParameter() add request cookies, headers, and parameters, respectively. These can be retrieved from the request variable during server-side execution with the methods defined in the HttpServletRequest interface. setMethod() sets the HTTP method (GET or POST) that the request will use. setAutomaticSession() determines whether a session will automatically be created for this request; the default value is true. If beginXXX() calls setAutomaticSession(false), the session instance variable in the server-side TestCase (if any) called with this request will be null. setURL() sets the simulated URL of the HTTP request. If the code under test does URL processing, setURL() allows for a mock URL. The following example illustrates how to add setup information for a specific testXXX method to the request that will be sent to the server.

public void beginGoOnVisionQuest(WebRequest request) {
 /* The URL being constructed is: http://nationalparks.org/organ-pipe-monument/long_walk.do
 */
 request.setURL("nationalparks.org",
 "/organ-pipe-monument",
 null,
 "/long_walk.do",
 null);
 /*multiple values for same key*/
 request.addParameter("SPIRIT_GUIDES", "Coyote");
 request.addParameter("SPIRIT_GUIDES", "Snake");
 request.addCookie("PREPARED", "true");
}


Java Start Sidebar
A Possible Trap

The beginXXX() method seems like an ideal place to do method-specific setup above and beyond request parameters. However, the following beginXXX() method will not work:

Java End Sidebar
public void beginCyborgAttack{
 someImportantInstanceVariable = someSpecificSetupMethod();
}


If someImportantInstanceVariable is referenced in testCyborgAttack, a NullPointerException will result because beginCyborgAttack() executes in a different copy of the test case. If a test method requires specific setup, you should use the following code instead:

public void testCyborgAttack{
 someImportantInstanceVariable = someSpecificSetupMethod();
 //..rest of testMethod
}


Calling the Redirector Servlet

After beginXXX() completes, the Cactus TestCase opens an HTTP connection to the redirector servlet as shown in the next illustration. In addition to sending any request parameters added in, beginXXXCactus sends some internal parameters that specify which test (and which test method) to execute on the server.

Java Click To expand

Server-Side Setup

When the redirector servlet receives the request, it inspects the internal parameters and instantiates a new copy of the correct test case on the server as shown in the following illustration. Once the class is instantiated, the redirector servlet uses reflection to copy the standard servlet variables (request, response, and so on) into the new test case instance. These variables are wrapped versions of the variables available to the redirector servlet and are covered in detail in the next section.

Java Click To expand

The Implicit Objects

A major feature of Cactus is the presence of implicit objects as public member variables of its TestCases. Server-side methods can pass these variables as parameters to tested methods that require them. Available to all three types of Cactus TestCases (ServletTestCase, FilterTestCase, and JspTestCase) are the following implicit objects: request, response, and config. These variables are initialized just prior to the calling of setUp() and are available only in the copy of the test case that runs on the server. (They contain null on the client.) Each implicit object is either the exact version or a thinly wrapped version of the same object available to the redirector servlet. The reasons for the wrapping are explored under the individual section.

request

The request variable implements HttpServletRequest. Its actual class is org.apache.cactus.server .HttpServletRequestWrapper, which delegates almost all its calls to the original HttpServletRequest. This means that the wrapper contains all of the request parameters that were specified in beginXXX(). The only exceptions are the methods that return information about the URL. These methods return the simulated URL specified with WebRequest.setURL in beginXXX(). The request variable also returns a Cactus-wrapped version of a RequestDispatcher from getRequestDispatcher() to avoid problems resulting from the mocked URL.

response

This variable is set to the actual response object passed into the redirector servlet's doGet()/doPost() method.

config

This variable contains a wrapped version of the ServletConfig object returned by a call to getServletConfig() in the redirector. The wrapper delegates almost all its methods; the key difference is its handling of initialization parameters. In addition to being able to return init parameters of the redirector servlet (as mapped in the deployment descriptor), you can set further init parameters with setInitParameter(). The values set with this method take precedence over the values of the redirector servlet.

The config object also returns a wrapped ServletContext from getServletContext(). The ServletContext wrapper handles forwards and includes correctly (additional logic is needed to cope with the simulated URL of the HttpServletRequestWrapper); it also saves all calls to its logging methods so that they can be easily retrieved and inspected by test code.

Server-Side Execution

Once the server-side test case has been fully initialized, server-side execution begins as shown in the following illustration. The setUp(), testXXX(), and tearDown() methods are executed at this point. (Note that these are executed only in the copy of the test case that runs on the server.) The results of the test (including errors or failure, if any) are captured and stored in an app scope variable.

Java Click To expand

setUp() and tearDown()

The setUp() and tearDown() methods mark the beginning and end of server-side execution, respectively. The use of these methods is analogous to their use in regular JUnit. They are not specific to any test method and are the ideal place to construct a test fixture. All the implicit objects are available in setUp() and tearDown(). In the following example, setUp() sets an init parameter on the config object to simulate the expected initialization parameters for the FooServlet. Then setUp() creates an instance of FooServlet, and it is initialized with the modified config object. Notice that this is done manually by the setUp() method; ordinarily, the servlet engine would handle this housekeeping chore. Here, the "unit" nature of Cactus becomes more apparent. The framework does not invoke servlets; it merely provides access to the container:

public void setUp() {
 config.setInitParameter("loginPage","/login.jsp");
 servlet = new LoginServlet();
 servlet.init(config);
}


testXXX()

The test method, as in JUnit, defines the meat of the testing logic. In the following example, it consists of passing two of the implicit objects to the servlet under test and then running assertions against other implicit objects to verify that they were modified correctly:

public void testDoGet() throws Exception{
 servlet.doGet(request, response);
 String value = (String)session.getAttribute("name");
 assertEquals("request param not mapped into session", "siegfried", value);
}


However, the implicit objects do not need to be used. The test method can verify any code that relies on container services. Imagine the following static method, which performs a JNDI lookup:

public static XAConnection connectionLookup() throws Exception {
 InitialContext ctx = new InitialContext();
 XADataSource src = (XADataSource)ctx.lookup("java/comp/env/webDatabase");
 return src.getXAConnection();
}


This method could be tested in a Cactus test case as follows:

public void testJNDILookup()throws Exception{
 javax.sql.XAConnection conn = JNDILookup.connectionLookup();
 assertNotNull(conn);
}


Results Collection and Postprocessing

Once the test has finished executing, the copy of the test on the client makes another request to the redirector servlet asking for the results, as shown in the next illustration. The redirector servlet pulls the results from the app scope variable where they were stored and sends them to the client. If any exceptions or failures were stored in the results, they are rethrown so the JUnit test runner will log them normally. If the result was successful, a final endXXX() method is executed on the client.

Java Click To expand

The endXXX() takes a WebResponse parameter. The WebResponse object contains response data written to the client, if any. The endXXX() method can perform assertions against contents of the response using the methods of the WebResponse object. Test execution is complete after endXXX() returns. Note that there are actually two types of WebResponse objects—we will cover these in the next section.

endXXX()

The final (optional) method that ServletTestCase expects subclasses to define is endXXX(). This method is called only after the results of the server-side execution have been retrieved from the server. If the results do not contain any failures or errors, endXXX() is called. This method is used to verify the final output sent by the server. This can be useful for cases where to code under test generates a full HTML response, as well as cases (such as custom tags) where only a small amount of data is written to the client. Needless to say, the servlet implicit objects are not available in endXXX(); nor are any variables that were set during server-side execution. You can define endXXX() with one of two signatures, which differ based on the type of the single parameter they accept. The two possible parameter types are org.apache.cactus.WebResponse and com.meterware.httpunit.WebResponse. Each one provides different assertion capabilities, detailed in the following subsections.

Basic Assertions: Cactus's WebResponse

The Cactus version of WebResponse supports simple assertions. It allows the transformation of the server's response into a searchable String with getText() or into an array of Strings with getTextAsArray(). It also provides facilities for retrieving and performing assertions on cookies returned with the response by the server; this can be done by using getCookies() and getCookie(String). In the following example, the endFoo() method checks whether the response contains the String "siegfried":

public void endFoo(WebResponse response) {
 assertEquals("siegfried not found in response", "siegfried", response.getText());
}


Complex Assertions: HttpUnit's WebResponse

The alternate signature for endXXX is endXXX(com.meterware.httpunit.WebResponse). HttpUnit's WebResponse class supports significantly more sophisticated assertions but operates primarily on HTML documents. For plain-text or XML responses, for example, you are probably better off with Cactus's WebResponse. Using the two frameworks together allows easy access to these powerful assertion facilities without tying test development to the black-box model of HttpUnit. Using the HttpUnit WebResponse class is a piece of cake. The HttpUnit API reference details the available methods, but a few examples here will illustrate their general capabilities. getLinks() returns an array of WebLink objects, which provide accessors for their target URLs. getDOM() returns the org.w3c.dom.Node corresponding to the underlying HTML document, and getText() returns a String containing the raw HTML. In order to access these features, the content type of the response must be set to "text-html"; however, a suggestion has been submitted to the HttpUnit team to allow this to be assumed if necessary (although the response will still have to contain HTML).

An Example of endXXX() Using HttpUnit

The following listing contains the source of a simple servlet that prints a link into the response object.

public class HappyServlet extends javax.servlet.http.HttpServlet {
 public void doGet(HttpServletRequest request,
 HttpServletResponse response)throws IOException{
 response.setContentType("text/html");
 PrintWriter writer = response.getWriter();
 writer.println("<a href='http://www.happypuppy.com/'>");
 writer.println("I'm so happy!</a>"); }
}


The following listing contains the relevant methods of a Cactus test that verifies the output of our HappyServlet by using HttpUnit.

private HappyServlet servlet;
public void setUp()throws Exception{
 servlet = new HappyServlet();
 servlet.init(config); }
/** Test of doGet method, of class xptoolkit.cactus.HappyServlet. */
public void testDoGet()throws Exception{
 System.out.println("testDoGet");
 servlet.doGet(request, response);
}
public void endDoGet(com.meterware.httpunit.WebResponse response)
 throws Exception{
 WebLink link = response.getLinkWith("I'm so happy!");
 assertNotNull(link);
}


JaVa
Comments