7.3 Setting Up a Stable Build Environment

7.3.1 Problem

You want to configure your environment to support test-first development with Cactus, Tomcat, and Ant.

7.3.2 Solution

Create an Ant buildfile to automatically build, start Tomcat, deploy to the server, execute your web application's test suite, and stop Tomcat.

7.3.3 Discussion

Setting up an Ant buildfile to properly handle Cactus tests is nontrivial and deserves some explanation A successful environment allows developers to make and test small code changes quickly, and requires a server that supports hot deploying. The ability to hot deploy a modified web application is critical for test-first development because it takes too long to restart most servers. Tomcat provides a built-in web application called manager that supports hot deploying. For more information on Tomcat see Chapter 10.

Figure 7-2 shows a graphical view of the Ant buildfile we are creating. Setting up a stable and easy-to-use environment is imperative for server-side testing. For example, typing ant cactus prepares the development environment, compiles all out-of-date files, creates a new WAR file, starts Tomcat (if it isn't already started), removes the old web application (if it exists), deploys the updated web application, and invokes the Cactus test suite. The developer does not have to worry about whether the server is started. Ant takes care of the details, allowing developers to concentrate on writing testable code. If the tests are too hard to execute, then developers will not write them.

Figure 7-2. Graphical view of the Ant buildfile
figs/jexp_0702.gif

The properties defined below set up information used throughout the buildfile. For example, the properties username.manager and username.password are needed to login to Tomcat's manager application for deploying and removing web applications while Tomcat is running, a concept known as hot deploying. If the username or password changes, we only have to change it here.

<property environment="env"/>
<property name="dir.build" value="build"/>
<property name="dir.src" value="src"/>
<property name="dir.resources" value="resources"/>
<property name="dir.lib" value="lib"/>
<property name="url.manager" value="http://localhost:8080/manager"/>
<property name="username.manager" value="javaxp"/>
<property name="password.manager" value="secret"/>
<property name="host" value="http://localhost"/>
<property name="port" value="8080"/>
<property name="webapp.context.name" value="xptest"/>
<property name="servlet.redirector" value="ServletRedirector"/>
<property name="cactus.service" value="RUN_TEST"/>
<property name="jsp.redirector" value="JspRedirector"/>
<property name="filter.redirector" value="FilterRedirector"/>

The classpath shown below is used to compile the web application, and is used as the client-side classpath when executing Cactus.

<path id="classpath.project">
  <pathelement location="${dir.build}"/>
  <pathelement location="${env.CACTUS_HOME}/lib/aspectjrt-1.0.5.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/cactus-1.4.1.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/commons-logging-1.0.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/httpclient-2.0.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/httpunit-1.4.1.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/junit-3.7.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/log4j-1.2.5.jar"/>
  <pathelement location="${env.CACTUS_HOME}/lib/servletapi-2.3.jar"/>[3]
</path>

[3] Cactus 1.4 and higher now ships with the Servlet 2.3 API.

The next target sets the property is.tomcat.started if Tomcat is running. The property is.webapp.deployed is set if Tomcat is started and there is an instance of our web application currently installed. The Ant http conditional subtask returns a response code, and if that response code indicates some sort of success, we can assume the application is deployed. The undeploy target uses the is.webapp.deployed property to determine if an old copy of the web application should be removed:

<target name="init">
  <condition property="is.tomcat.started">
    <http url="${host}:${port}"/>
  </condition>
  <condition property="is.webapp.deployed">
    <and>
      <isset property="is.tomcat.started"/>
      <http url="${host}:${port}/${webapp.context.name}"/>
    </and>
  </condition>
</target>

The code is compiled with this target:

<target name="compile" depends="prepare" 
        description="Compile all source code.">
  <javac srcdir="${dir.src}" destdir="${dir.build}">
    <classpath refid="classpath.project"/>
  </javac>
</target>

Next, your buildfile should have a target to generate the WAR file:

<target name="war" depends="compile">
  <war warfile="${dir.build}/${webapp.context.name}.war" 
      webxml="${dir.resources}/web.xml">
    <classes dir="${dir.build}">
      <include name="com/oreilly/javaxp/cactus/**/*.class"/>
    </classes>
    <lib dir="${env.CACTUS_HOME}/lib">
      <include name="aspectjrt-1.0.5.jar"/>
      <include name="cactus-1.4.1.jar"/>
      <include name="commons-logging-1.0.jar"/>
      <include name="httpunit-1.4.1.jar"/>
      <include name="junit-3.7.jar"/>
    </lib>
    <fileset dir="${dir.resources}">
      <include name="*.jsp"/>
      <include name="*.html"/>
    </fileset>
  </war>
</target>

Cactus tests need a few support classes on the web application's classpath. The simplest way to ensure that these files are on the web application's classpath is to include them in the WEB-INF/lib directory. Using the lib subtask accomplishes this goal. Finally, if you are testing JSPs, then you need to copy the jspRedirector.jsp[4] file to the root of your web applicationotherwise, do not worry about it.

[4] This file is located under the CACTUS_HOME/sample-servlet/web. For convenience, this file has been copied to our project's resources directory.

Since we are using Tomcat's manager web application to deploy, Tomcat must be started. In order to achieve test-first development, we created a new Ant task to start Tomcat. We need our build process to patiently wait until the server is started before trying to deploy.

<target name="start.tomcat">
  <taskdef name="starttomcat"
      classname="com.oreilly.javaxp.tomcat.tasks.StartTomcatTask">
    <classpath>
      <path location="${dir.lib}/tomcat-tasks.jar"/>
    </classpath>
  </taskdef>

  <starttomcat
      testURL="${host}:${port}"
      catalinaHome="${env.CATALINA_HOME}"/>
</target>

Before deploying, the old instance of the web application must be removed, if it exists. First, the init target is called to see if the web application has been deployed. If so, then Tomcat's RemoveTask is used to remove the old instance.

The RemoveTask fails if the web application is not installed (previously deployed), causing the build to stop. Depending on your needs, this may or may not be what you expect. In our case, it's not what we expect, and is why we verify that the web application is deployed before trying to remove it.

<target name="undeploy" depends="init" if="is.webapp.deployed">
  <taskdef name="remove" classname="org.apache.catalina.ant.RemoveTask">
    <classpath>
      <path location="${env.CATALINA_HOME}/server/lib/catalina-ant.jar"/>
    </classpath>
  </taskdef>

  <remove
      url="${url.manager}"
      username="${username.manager}"
      password="${password.manager}"
      path="/${webapp.context.name}"/>
</target>

The deploy target depends on generating a new WAR file, starting Tomcat, and removing a previously-deployed instance of the web application. Tomcat's InstallTask is used to install the WAR file.

<target name="deploy" depends="war,start.tomcat,undeploy">
  <taskdef name="install" classname="org.apache.catalina.ant.InstallTask">
    <classpath>
      <path location="${env.CATALINA_HOME}/server/lib/catalina-ant.jar"/>
    </classpath>
  </taskdef>

  <pathconvert dirsep="/" property="fullWarDir">
    <path>
      <pathelement location="${dir.build}/${webapp.context.name}.war"/>
    </path>
  </pathconvert>

  <install
      url="${url.manager}"
      username="${username.manager}"
      password="${password.manager}"
      path="/${webapp.context.name}"
      war="jar:file:/${fullWarDir}!/"/>
</target>

After the web application is successfully deployed, the Cactus test suite is executed. Cactus provides an Ant task called RunServerTestsTask to execute the tests. This task is located in the cactus-ant-1.4.1.jar file. Here is how to setup a target to execute Cactus tests:[5]

[5] The testURL attribute value has a line break so that the URL fits nicely on the page.

<target name="cactus " depends="deploy"
    description="Deploys and runs the Cactus Test suite on Tomcat 4.1.x">

  <taskdef name="runservertests"
      classname="org.apache.cactus.ant.RunServerTestsTask">
    <classpath>
      <path location="${env.CACTUS_HOME}/lib/cactus-ant-1.4.1.jar"/>
    </classpath>
  </taskdef>

  <runservertests
      testURL="${host}:${port}/${webapp.context.name}/${servlet.redirector}?
               Cactus_Service=${cactus.service}"
      startTarget="start.tomcat"
      testTarget="junit.cactus"
      stopTarget="stop.tomcat"/>
</target>

The runservertests task defines four attributes:

  • The testURL attribute checks if the specified URL is available by constantly polling the server. Cactus recommends using a specific URL to the server is ready to execute Cactus tests. This URL invokes the ServletTestRedirector servlet and passes a parameter to it, telling the servlet to check if all is well to execute tests. If the URL fails, then the server has not been started, or the web application is not properly deployed (probably classpath issues).

    If the server hangs when executing this task, you need to check the Tomcat logfiles. Typically, the web application did not start properly due to classpath issues. Almost all Cactus problems are classpath-related.

  • The startTarget attribute specifies a target within the buildfile that starts the server, if the server is not already started.

  • The testTarget attribute specifies a target within your buildfile that executes the Cactus tests.

  • The stopTarget attribute specifies a target within your buildfile that stops the server. The server is only stopped if the startTarget started the server.

First, the startTarget starts the server, if the server is not already started. Once the server is running, the testTarget is responsible for starting the tests. A Cactus test suite is started the same way as a JUnit test suite. The next target uses the junit task to start the Cactus tests on the client. It's important to know that Cactus retains control during test execution, ensuring that the client and server tests are kept in sync.

<target name="junit.cactus">
  <junit printsummary="yes" haltonfailure="yes" haltonerror="yes" fork="yes">
    <classpath refid="classpath.project"/>
    <formatter type="plain" usefile="false"/>
    <batchtest fork="yes" todir="build">
      <fileset dir="src">
        <include name="**/Test*.java"/>
      </fileset>
    </batchtest>
  </junit>
</target>

After the tests are complete, the stopTarget is executed to stop the server (if the server was started by startTarget). Otherwise, the server is left running. In order to facilitate test-first development, we created a custom Ant task specifically to stop Tomcat. Here is the target to stop Tomcat:

<target name="stop.tomcat">
  <taskdef name="stoptomcat"  
      classname="com.oreilly.javaxp.tomcat.tasks.StopTomcatTask">
    <classpath>
      <path location="${dir.lib}/tomcat-tasks.jar"/>
    </classpath>
  </taskdef>

  <stoptomcat
      testURL="${host}:${port}"
      catalinaHome="${env.CATALINA_HOME}"/>
</target>

7.3.4 See Also

Chapter 10 describes the custom Ant tasks to start and stop Tomcat.