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 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>
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.
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.
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:
JspTestCase
, the file
jspRedirector.jsp
needs to be included in the web
application, and it needs to be named in the deployment
descriptor.
<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.
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:
webapps
directory, so that it will be deployed when Tomcat is started up.
cactus.contextURL
and the redirector mappings are
automatically passed to the test runner.