2011/08/05 - Jakarta Cactus has been retired.

For more information, please explore the Attic.

Servlet Sample Walkthrough

Cactus is distributed with a small sample web application, generally referred to as the Servlet Sample. You can find this application in the directory samples/servlet in the main distribution. While the servlet sample is intended to provide insight in how to use Cactus for writing tests, it also nicely demonstrates the use of the Ant integration provided by Cactus.

In this section, we will walk through the build file of the servlet sample step by step, explaining how the Cactus tests are integrated into the overall build of the application. I recommended that you look at the servlet sample build and play with it after or while reading this.

The actual build file of the sample might differ in a few details from the snippets shown here.

Starting Off with a Plain Build

The servlet sample is a simple web application that contains a servlet, some JSP tags, a JSP page and a servlet filter. For the rest of this document we're going to work with the version of the servlet sample for J2EE 1.3.

The build file without any Cactus tests is straightforward. As usual, the first thing is that a couple of properties are set up:

<project name="Cactus Servlet Sample" default="dist" basedir=".">

  <property file="build.properties" />
  <property file="${user.home}/build.properties" />

  <property name="project.name.text" value="Cactus Servlet Sample"/>
  <property name="project.name.file" value="sample-servlet"/>
  <property name="project.version" value="@version@"/>

  <property name="project.prefix" value="jakarta-"/>

  <property name="year" value="@year@"/>
  <property name="debug" value="on"/>
  <property name="optimize" value="off"/>
  <property name="deprecation" value="off"/>

  <!-- Directory layout -->
  <property name="src.dir" location="src"/>
  <property name="src.java.dir" location="${src.dir}/java"/>
  <property name="src.webapp.dir" location="${src.dir}/webapp"/>
  <property name="target.dir" location="target"/>
  <property name="target.classes.dir" location="${target.dir}/classes"/>
  <property name="target.classes.java.dir"
      location="${target.classes.dir}/java"/>
  <property name="dist.dir" location="dist"/>

  <!-- Required libraries -->
  <property name="servlet.jar" location="lib/servlet.jar"/>
  <property name="jstl.jar" location="lib/jstl.jar"/>
  <property name="standard.jar" location="lib/standard.jar"/>
  <path id="project.classpath">
    <pathelement location="${servlet.jar}"/>
    <pathelement location="${jstl.jar}"/>
    <pathelement location="${standard.jar}"/>
  </path>

Next, we check whether the required libraries are actually available. This is done in the init target. In this case, we just check whether the corresponding properties point to existing files. Alternatively, we could be checking whether the JARs also contain some class we need. We also set the time stamp.

  <!-- Initialize the build. Must be called by all targets -->
  <target name="init">
    <condition property="properties.ok">
      <and>
        <available file="${servlet.jar}"/>
        <available file="${jstl.jar}"/>
        <available file="${standard.jar}"/>
      </and>
    </condition>
    <fail unless="properties.ok">Missing property...</fail>
    <tstamp/>
  </target>

The first real thing we'll do in the build is to compile the application classes.

  <!-- Compile the Java source -->
  <target name="compile" depends="init"
      description="Compile the application classes">
    <mkdir dir="${target.classes.java.dir}"/>
    <javac destdir="${target.classes.java.dir}"
        debug="${debug}" optimize="${optimize}"
        deprecation="${deprecation}">
      <src path="${src.java.dir}"/>
      <classpath refid="project.classpath"/>
    </javac>
  </target>

What's left is just the generation of the web-application archice (WAR) for deployment. We do that using the builtin <war> task.

  <!-- Create the war file -->
  <target name="war" depends="compile"
      description="Generate the runtime war">
    <war warfile="${target.dir}/${project.name.file}.war"
        webxml="${src.webapp.dir}/WEB-INF/web.xml">
      <fileset dir="${src.webapp.dir}"/>
      <classes dir="${target.classes.java.dir}"/>
      <lib file="${jstl.jar}">
      <lib file="${standard.jar}">
    </war>
  </target>

After that, we might want to copy the generated WAR file to the distribution directory. In a somewhat more sophisticated application we'd also copy things like the API documentation or a user's guide to the distribution directory in this target. We also make the dist target depend on the clean target, so that distribution builds are always a full rebuild.

  <target name="dist" depends="clean, war"
      description="Generate the distributable files">
    <copy file="${target.dir}/${project.name.file}.war"
        todir="${dist.dir}"/>
  </target>

  <target name="clean"
      description="Remove all generated files">
    <delete dir="${target.dir}"/>
    <delete dir="${dist.dir}"/>
  </target>

Defining the Cactus Ant tasks

Now we're ready to start integrating Cactus tests into the build. The first thing we need to do for that is to define the Cactus tasks, so that they can be used in the build file.

The prerequisite for defining the Cactus tasks is to make the Cactus JARs accessible to Ant. This can be done in a number of ways, but here we're going to assume that they are stored in a lib directory of the project. We then define properties representing the individual JARs, so that they can be overridden by the user. And we build a reusable classpath using the Ant <path> type.

  <!-- Libraries required for the Cactus tests -->
  <property name="aspectjrt.jar" location="lib/aspectjrt.jar"/>
  <property name="cactus.jar" location="lib/cactus.jar"/>
  <property name="cactus.ant.jar" location="lib/cactus.ant.jar"/>
  <property name="commons.httpclient.jar"
      location="lib/commons.httpclient.jar"/>
  <property name="commons.logging.jar"
      location="lib/commons.logging.jar"/>
  <property name="httpunit.jar" location="lib/httpunit.jar"/>
  <property name="junit.jar" location="lib/junit.jar"/>
  <property name="nekohtml.jar" location="lib/nekohtml.jar"/>
  <path id="cactus.classpath">
    <path refid="project.classpath"/>
    <pathelement location="${aspectjrt.jar}"/>
    <pathelement location="${cactus.jar}"/>
    <pathelement location="${cactus.ant.jar}"/>
    <pathelement location="${commons.httpclient.jar}"/>
    <pathelement location="${commons.logging.jar}"/>
    <pathelement location="${junit.jar}"/>
  </path>

Once this is done, we can proceed with the actual definition of the Cactus tasks, using the Ant <taskdef> task.

  <taskdef resource="cactus.tasks"
      classpathref="cactus.classpath"/>

By using the cactus.tasks property file included in the cactus-ant.jar library, we can define all Cactus tasks in one go, without needing to know the names of the individual task classes.

Compiling the Test Code

Next we need to compile the test case classes. In the servlet sample, these are located in the directory src/test-cactus. After adding the definition of the two properties src.cactus.dir and test.classes.cactus.dir at the top of the build file, we can add a target for the test compilation:

  <!-- Compiles the Cactus test sources -->
  <target name="compile.cactus" depends="compile.java">
    <mkdir dir="${target.classes.cactus.dir}"/>
    <javac destdir="${target.classes.cactus.dir}"
        debug="${debug}" optimize="${optimize}"
        deprecation="${deprecation}">
      <src path="${src.cactus.dir}"/>
      <classpath>
        <path refid="cactus.classpath"/>
        <pathelement location="${httpunit.jar}"/>
        <pathelement location="${nekohtml.jar}"/>
        <pathelement location="${target.classes.java.dir}"/>
      </classpath>
    </javac>
  </target>

  <target name="compile" depends="compile.java, compile.test">
  </target>

Note that we renamed the target to compile the application classes from compile to compile.java, and added a wrapper target compile that depends on both compile.java and compile.test.

Cactifying the Web Application

In order to be able to run the Cactus tests, you have to deploy a cactified web application to the target container. With cactified, we generally refer to a web-application that has been enhanced with the elements required for Cactus tests to work. The minimum requirements are as follows:

  • It contains all the libraries that Cactus needs on the server side (see the Classpath Guide for details).
  • At least one test redirector is defined in the deployment descriptor of the web-application.
  • If you are using JspTestCase, the file jspRedirector.jsp needs to be included in the web application, and it needs to be named in the deployment descriptor.
Of course, the cactified web-application also needs to contain your test classes. The Cactus tasks for Ant support these requirements with the tasks <cactifywar> and <webxmlmerge>. While the former is specific to Cactus and probably does all you need in most cases, the latter is more generic and lower-level. In this example, we'll use <cactifywar> because it's more powerful and easier to use. For the cactification, we add a target called test.prepare, on which the test target will depend. This target itself depends on the application WAR being built (the war target), and the Cactus test cases being compiled (the compile.cactus target):
<target name="test.prepare" depends="war, compile.cactus">
  
  <!-- Cactify the web-app archive -->
  <cactifywar srcfile="${target.dir}/${project.name.file}.war"
      destfile="${target.dir}/test.war">
    <classes dir="${target.classes.cactus.dir}"/>
    <lib file="${httpunit.jar}"/>
  </cactifywar>

</target>

So what the <cactifywar> task does here, is to open the WAR file specified by the srcfile attribute, and write the cactified WAR file to the file specified by the destfile attribute.

The Cactus test redirectors are automatically injected into the deployment descriptor of the destination archive. By the way, the task will examine the version of the web-app DTD used in the source file, which determines the servlet API version in use (if no DOCTYPE declaration is found, it assumes servlet API 2.2). The filter test redirector will only be inserted if the servlet API version is 2.3 (or later). The file jspRedirector.jsp will also be added automatically (to the root of the web-application).

<cactifywar> will also try to add the Cactus libraries required on the server-side to the destination archive. For this to work, these libraries need to be on the classpath of the task. Because we've done that when defining the task (see above), there shouldn't be problems here. If the task has problems locating the required JARs, a warning will be logged.

An important fact to note about the <cactifywar> task is that it extends the builtin Ant task <war>. That's why we can add the nested <classes> and <lib> elements in the example above. We do this to add the actual test classes to the cactified WAR, as well as the HttpUnit JAR needed for some of the tests.

Running the Cactus Tests

Now to the probably most important part: running the Cactus tests. The critical point here is that a J2EE container must be running while the tests are executed, and that the web-application under test must have been successfully deployed. If that is the case, you can simply run the tests like normal JUnit tests, and provide a couple of system properties that tell Cactus how to connect to the server (see the Configuration Guide for details).

However, you will probably want to automate the deployment of the cactified WAR, and maybe also the startup and shutdown of the container. This can be done with some Ant scripting in combination with the <runservertests> task provided by Cactus. But Cactus also provides a higher-level abstraction for running the tests with the <cactus> task.

The <cactus> task extends the optional Ant task <junit>, adding support for in-container tests and hiding some of the details such as the system properties used for configuring Cactus:

<target name="test" depends="test.prepare"
    description="Run the tests on the defined containers">

  <!-- Run the tests -->
  <cactus warfile="${target.dir}/test.war" fork="yes"
      failureproperty="tests.failed">
    <classpath>
      <path refid="project.classpath"/>
      <pathelement location="${httpunit.jar}"/>
      <pathelement location="${nekohtml.jar}"/>
      <pathelement location="${target.classes.java.dir}"/>
      <pathelement location="${target.classes.cactus.dir}"/>
    </classpath>
    <containerset timeout="180000">
      <tomcat4x if="cactus.home.tomcat4x"
          dir="${cactus.home.tomcat4x}" port="${cactus.port}"
          output="${target.testreports.dir}/tomcat4x.out"
          todir="${target.testreports.dir}/tomcat4x"/>
    </containerset>
    <formatter type="brief" usefile="false"/>
    <formatter type="xml"/>
    <batchtest>
      <fileset dir="${src.cactus.dir}">
        <include name="**/Test*.java"/>
        <exclude name="**/Test*All.java"/>
      </fileset>
    </batchtest>
  </cactus>

</target>

In this example, we specify the "WAR under test" using the warfile attribute. This must point to an already cactified WAR. The task will extract information about the mappings of the test redirectors from the deployment descriptor of the web-application, and automically setup the corresponding system properties.

Next, we add a nested <containerset> element, which allows us to specify one or more containers against which the tests will be executed. Here we only test against Apache Tomcat 4.x. We specify the installation directory of Tomcat using the dir attribute, as well as the port to which the container should be bound using the port attribute.

What happens behind the scenes is this:

  1. Tomcat is installed to a temporary directory using a minimal configuration.
  2. The specified WAR is copied into the Tomcat webapps directory, so that it will be deployed when Tomcat is started up.
  3. Tomcat is started. The task assumes that the startup is complete as soon as HTTP requests to the test web-application are successful.
  4. Now the tests are executed. The required system properties such as cactus.contextURL and the redirector mappings are automatically passed to the test runner.
  5. After the tests have terminated (successfully or not), Tomcat is shut down.
If we had defined more than one container in the <containerset> element, this procedure would be repeated for every container in the list.