Cactus: server-side unit testing

Description

This section on Cactus was taken mostly from the Cactus web site . Cactus is a subproject of the Apache Jakarta Commons project. We would like to express our thanks to the Apache Software Foundation and to Vincent Massol, the writer and project maintainer for letting us reproduce it here. We would also like to thank OCTO Technology for sponsoring the Cactus project.

Tested on Apache-Cactus! Cactus is a simple test framework for unit testing server-side java code. Logo designed by Lyonel Thouvenot.

Cactus is a simple test framework for unit testing server-side java code. It uses JUnit and extends it. It's primary goal is to be able to unit test server side java methods which use Servlet objects such as HttpServletRequest, HttpServletResponse, HttpSession, ...

The Cactus framework is not a functional test framework which views your server classes as a black box and only tests the returned results from externally calling your application (see HttpUnit for this). Both frameworks are complementary.

Current scope and status of Cactus

MVC Testing

A server side architecture is generally comprised of 3 domains : the Model , the View and the Controller. This is known as the "Model 2" MVC architecture. The challenge is to be able to unit test the MVC architecture. Cactus uses and extend JUnit for this purpose.

However, unit testing the View part is not relevant. The View is the part that is seen by the end user and it can be effectively tested using a functional test framework (such as HttpUnit).

To summarize, unit testing the MVC architecture is done using the Cactus framework and functional testing of the MVC architecture is done by functionally testing the View part using a functional test framework.

Current scope and status of Cactus

Here is the current scope and status of Cactus (02/05/2001):

Current scope and status of Cactus.

Installation

Step 1: Installation of Cactus in your application to test

Cactus works by calling a Redirector (either Servlet Redirector or JSP Redirector, both part of Cactus) that you need to put where both the server-side code that you need to test and your test code are located. In other words, you need to do the following:

  1. Put the commons-cactus.jar file in the WEB-INF/lib directory of the webapp that you are testing (If your servlet engine does not support web applications, just make sure that it is in the CLASSPATH of your servlet engine).

  2. Modify your web.xml file to include a mapping for the Redirector Servlet (If your servlet engine does not support web applications, you won't have any web.xml file. You'll need to edit your servlet engine configuration file and find out how to map a URL to a Servlet). This mapping contains :

    • the name of the Redirector Servlet class,

    • the URI that will be used to call the Redirector Servlet. This needs to match the URL specified in the cactus.properties configuration file (see configuration),

    • configuration data that can be retrieved using the config implicit object.

  3. Modify your web.xml file to include a mapping for the Redirector JSP. This mapping contains

    • the name of the Redirector JSP page,

    • the URI that will be used to call the Redirector JSP. This needs to match the URL specified in the cactus.properties configuration file (see configuration).

    • configuration data that can be retrieved using the config implicit object.

  4. Copy the Cactus Redirector JSP file called redirector.jsp in the document root of your test webapp.

The steps 3 and 4 above are only necessary if you wish to use the Redirector JSP for your tests and thus have access to the pageContext and out implicit objects (for testing custom tag libraries for example).

For example, if you have the following configuration:

  • Your webapp is called "mywebapp".

  • The URLs specified in your cactus.properties are "http://localhost:8080/mywebapp/ServletRedirector" for the cactus.servletRedirectorURL property and "http://localhost:8080/mywebapp/JspRedirector" for the cactus.jspRedirectorURL property

  • The Cactus Redirector JSP file redirector.jsp is put in a subdirectory named test/ in your webapp document root.

then your web.xml file should look like:

  ...
<web-app>

    <servlet>
       <servlet-name>ServletRedirector</servlet-name>
       <servlet-class>org.apache.commons.cactus.server.ServletTestRedirector</servlet-class>
       <init-param>
         <param-name>param1</param-name>
         <param-value>value1 used for testing</param-value>
       </init-param>
    </servlet>

    <servlet>
      <servlet-name>JspRedirector</servlet-name>
      <jsp-file>/test/redirector.jsp</jsp-file>
      <init-param>
        <param-name>param1</param-name>
        <param-value>value1 used for testing</param-value>
      </init-param>
    </servlet>

    <servlet-mapping>
      <servlet-name>ServletRedirector</servlet-name>
      <url-pattern>/ServletRedirector</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
      <servlet-name>JspRedirector</servlet-name>
      <url-pattern>/JspRedirector</url-pattern>
    </servlet-mapping>

</web-app>
      

Step 2 : Installation of your test code

You also need to put the test classes that you have written (see the using section) on the server side, along with the code to test. Put these classes in your WEB-INF/classes or WEB-INF/lib (If your servlet engine does not support web applications, just make sure that it is in the CLASSPATH of your servlet engine).

Step 3: The server side CLASSPATH

Here is the list of files that need to be in your server CLASSPATH :

  • the classes under test. These would normally be put under your webapp WEB-INF/classes directory,

  • the test classes. These would normally be put under your webapp WEB-INF/classes directory,

  • the cactus jar (commons-cactus.jar). It should normally be put under your webapp WEB-INF/lib directory. However if you use Cactus to test several webapps you might want to factor it out by putting it in the server's global CLASSPATH,

  • the JUnit jar (junit.jar). It should normally be put under your webapp WEB-INF/lib directory. However if you use it for several other webapps you might want to factor it out by putting it in the server's global CLASSPATH,

Step 4: Client-side installation with JUnit

A Cactus suite of tests is started using JUnit test runners (see the using section). You need to put the following files in your client-side CLASSPATH :

  • The JUnit jar file,

  • The Cactus jar : commons-cactus.jar,

  • Your test classes. This is needed because the beginXXX() and endXXX() methods are executed on the client side (see the using section for explanations on how to write tests using Cactus and see the architecture section for an explanation on how Cactus works)

Step 5 : Configure Cactus

You need to configure Cactus before using it the first time.

Configuration Configuring Cactus is very simple. You simply need to provide a file named cactus.properties that you will put in your CLASSPATH (meaning you need to put the directory where this file is located in your CLASSPATH, not the file itself !).

Moreover, in order for Cactus to work, you need to register a generic servlet (called the Redirector servlet) and/or a generic JSP (called the Redirector JSP) in your web.xml file if you are using a web application or in your specific Servlet engine configuration file if it does not support web applications (see the installation section for help on modifying your web.xml file).

You need the Redirector JSP only if you want to make unit tests that need to access JSP implicit objects (PageContext and JspWriter).

cactus.properties

You need to se the following properties in your Cactus configuration file (cactus.properties):

Property Name cactus.servletRedirectorURL
Description URL to which the Redirector Servlet is mapped to
Example http://localhost:8080/test/ServletRedirector

Property Name cactus.jspRedirectorURL
Description URL to which the Redirector JSP is mapped to
Example http://localhost:8080/test/JspRedirector

Sample cactus.properties file:
# Configuration file for Cactus.

# Each project using Cactus need to have such a file put in the CLASSPATH
# (Meaning the directory containgin this file should be in the CLASSPATH, not
# the file itself of course ... :) )

# Defines the URLs that will be used by Cactus to call it's redirectors
#(Servlet and JSP). You need to specify in these URLs the webapp context
# that you use for your application. In the example below, the context is
# "test".

cactus.servletRedirectorURL = http://localhost:8080/test/ServletRedirector
cactus.jspRedirectorURL = http://localhost:8080/test/JspRedirector  
         

Example

This section only focuses on explaining the principle of Cactus unit testing by showing how to write a servlet test case on a very simple example.

Simple Servlet Sample

For the sake of simplicity let's imagine we have a servlet called SimpleServlet which has a single method :

  • registerUser() : Gets the user name from a HTTP request parameter, save it in the HTTP Session and send a Cookie back to the client with the user name in it. The returned HTML prints the name that was retrieved from the HTTP request's parameter.

Step 1: The SimpleServlet's code

Here is the code to unit test:
 
package sample;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class SimpleServlet extends HttpServlet
{
  public void doGet(HttpServletRequest theRequest, HttpServletResponse theResponse)
    throws IOException
 {
    PrintWriter pw = theResponse.getWriter();
    theResponse.setContentType("text/html");
    pw.print("<html><head/><body><h1>Your name is " + 
    registerUser(theRequest, theResponse) + "</h1></body></html>");
 }

 public String registerUser(HttpServletRequest theRequest, HttpServletResponse theResponse)
 {
   // Get the user name from the HTTP request's parameter
   String name = theRequest.getParameter("USER_NAME");
   // Save it in the session
   HttpSession session = theRequest.getSession();
   session.putValue("NAME", name);

   // And return a cookie
   Cookie cookie = new Cookie("ID", name);
  theResponse.addCookie(cookie);

  return name;
 }
}
         

Step 2: Unit testing strategy

Notice that we mentionned we were going to test the registerUser() method but nothing was said about the doGet() method ... Indeed, unit testing the doGet() (or doPost()) is not completely relevant. This kind of test is actually more a functional test (you can use a framework such as HttpUnit for doing this). However, for some simple cases it is not always easy to differentiate between unit test and functional test, you might want the test to be part of your unit test suite or it is a simple case that do not need to set up a functional test framework. For these cases, Cactus support asserting the output stream returned from your servlet, so it is possible to unit test the doGet() type methods, as demonstrated below. However, you should try to not put any logic in your doGet and doPost() methods. You should rather delegate any code logic to other methods (such as the registerUser() method is the sample below). It is called the facade pattern. If you do it this way, you can easily unit test your logic code methods one by one.

Step 3: The test code

 
public class TestSimpleServlet extends ServletTestCase
{
 //standard JUnit code.

  /**
   * Sets the HTTP request parameter that will be available in the test
   * method.
   */
  public void beginRegisterUser(ServletTestRequest theRequest)
  {
    theRequest.addParameter("USER_NAME", "Vincent");
  }

  /**
   * Unit test the registerUSer method.
   */
  public void testRegisterUser()
  {
    // Instantiate the class to test
    SimpleServlet servlet = new SimpleServlet();
    // Call the method to test
    String name = servlet.registerUser(request, response);
    // Verify that it returns the correct name
    assertEquals("Vincent", name);
    // Verify that the name has been put in the session
    assertEquals("Vincent", (String)session.getValue("NAME"));
  }

 /**
  * Verify that a cookie has been returned
  */
  public void endRegisterUser(HttpURLConnection theConnection)
  {
    Hashtable cookies = AssertUtils.getCookies(theConnection);
    Vector list = (Vector)cookies.get("ID");
    assert(list.size() == 1);

    ClientCookie cookie = (ClientCookie)list.elementAt(0);
    assertEquals("ID", cookie.getName());
    assertEquals("Vincent", cookie.getValue());
  }

 /**
  * Test the output stream returned by the <code>doGet()</code> method.
  */
  public void testDoGet()
  {
    SimpleServlet servlet = new SimpleServlet();
    servlet.doGet(request, response);
  }

 /**
  * Test the output stream returned by the <code>doGet()</code> method.
  */
  public void endDoGet(HttpURLConnection theConnection)
  {
   assertEquals("<html><head/><body><h1>Your name is Vincent</h1></body></html>",
   AssertUtils.getResponseAsString(theConnection));
  }
} 
       

Notice that in the endRegisterUser() and endDoGet() test methods we use methods from a helper class provided by Cactus to help retrieve cookies sent back from the servlet and to help assert the returned servlet output stream.

For unanswered questions, see the section on using Cactus and the FAQ.

Architecture

Assumptions that lead to the current Cactus design

  • The tests need to run on the server side. Indeed in a component model, the components have a closed relationship with their container : they benefit from the container's services, ... and thus they cannot easily be run outside the container. Simulating a container is too much work and leads to building a complete new container if all features need to be supported.

  • We need to be able to launch the tests that are located on the server side. The solution is to use a proxy class that is located on the server side. There are 2 possible implementations : (1) use a servlet which starts the tests or (2) use an EJB. I have chosen a servlet because it is the simplest solution and not all servlet engine also support EJBs.

  • We need to call this servlet. Again 2 solutions : (1) open a browser and call the servlet URL or (2) write some java code that uses the URLConnection class to automatically call the servlet. This is what I call the client side part of Cactus. Of course the best solution is (2) for the following reasons :

    • The tests can be automated, i.e. it is possible to do automatic regression tests. See the eXtreme Programming philosophy for better understanding this. The rationale is : everytime you modify some piece of code, you run all the tests again to make sure everything is still working.

    • It is possible to verify the results returned by the Servlet : cookies, headers, ... By calling directly the Servlet URL in a browser, it is not possible to do so.

    • In order to formalize test suites and have a sexy GUI interface there were 2 solutions : (1) make my own framework and GUI or (2) use a well-known, everywhere used framework. Of course, I have chosen this second solution : all the tests are started using JUnit.

How it works

Architecture overview.

The process is as follows for each XXX test method of your TestYYY test class:

  1. JUnit calls your the TestYYY.runTest() method (inherited from ServletTestCase). This later looks for a beginXXX(ServletTestRequest) method. If one is found, it executes it. This is executed on the client side (i.e. not in a server engine). The ServletTestRequest parameter passed to the beginXXX() method is used to set the HTTP headers, the HTTP parameters, ... that will be sent in step 2 to the Redirector proxy.

  2. The TestYYY.runTest() method then opens an HTTP connection to the Redirector proxy. All the parameters set up in the beginXXX() method are put in the HTTP request (HTTP headers, HTTP parameters, ...)

  3. The Redirector proxy acts as a proxy for your TestYYY test class, but on the server side. It means that TestYYY is instantiated twice : once on the client side and once on the server side. The client side instance is used for executing the beginXXX() and endXXX() methods (see steps 1 and 8) and the server side instance is used for executing the testXXX() methods (see step 4). The Redirector proxy does the following :

    • creates an instance of TestYYY using reflection. It sets by reflection the Cactus implicit objects (see the section below on Servlet Redriector Proxy and JSP Redirector Proxy for a list of available objects).

    • creates instances of Cactus wrappers for some server objects (HttpServletRequest, ServletConfig, ServletContext, ...). This is to be able to to override some methods in order to return simulated values. For example, the Cactus framework can simulate an URI (i.e. act as if this URI was called instead of the Redirector proxy URI). Thus, the getServerName(), getServerPort(), getRequestURI(), ... methods return values based on the simulated URI (if there is any defined by the user).

    • creates an HTTP Session if the user has expressed the wish (using the ServletTestRequest.setAutomaticSession(boolean) code in the beingXXX() method. By default a session is always created) and it fills by reflection the session implicit object.

  4. The TestYYY.setUp(), TestYYY.testXXX() and tearDown() methods are executed (in that order). They are called by the Redirector proxy using reflection.

  5. Your TestYYY.testXXX() method calls your server side code, executing the test and using the JUnit asserts to assert the result (assert(), assertEquals(), fail(), ...)

  6. If the test fails, your TestYYY.testXXX() methods throws exceptions to the Redirector proxy.

  7. If an exception has been raised, the Redirector proxy returns the information about the exception (it's name, class, stack trace) back to the client side. It will then be printed by JUnit in it's Test Runner console.

  8. If no exception occurred, the TestYYY.runTest() method looks for an endXXX(HttpURLConnection) method and executes it if found. At this stage, you have the opportunity to check returned HTTP headers, Cookies and the servlet output stream in the endXXX() method, again using JUnit asserts and helper utility classes provided by Cactus (see the sample application).

Redirector Proxies

Cactus provides 2 implementation for the Redirector Proxy:

  • A Servlet Redirector. This redirector is a servlet that should be used for unit testing servlet methods. It provides the following implicit objects : request, response, session and config.

  • A JSP Redirector. This redirector is a JSP page that should be used for unit testing server code that need access to the following objects : pageContext and out. These objects are provided in addition to all the objects provided by the Servlet Redirector. It can be useful for testing simple JSP custom Tag libraries.

Testing custom JSP Tag libraries is still in beta. I am still not sure of the usefulness of this ... I think unit testing tag libraries is much less relevant than doing functional tests, but this is an open subject ...

Servlet Redirector Proxy

Servlet Redirector Proxy

The client side opens 2 HTTP connections to the Servlet redirector. Once to execute the tests and retrieve the servlet output stream and a second time to get the test result. This is to be able to get the exception data (message, stack trace, ...) if the test failed. The test results are stored in a servlet-context-wide scope variable which is retrieved on the second HTTP connection.

JSP Redirector Proxy

JSP Redirector Proxy

The client side opens 2 HTTP connections. Once to the Redirector JSP to execute the tests and retrieve the JSP output stream and a second time to the Servlet Redirector to get the test result. This is to be able to get the exception data (message, stack trace, ...) if the test failed. The test results are stored in a servlet-context-wide scope variable which is retrieved on the second HTTP connection.