A simple Ant script for a project with a target JAR file and JUnit tests

Be sure to check out the follow-on article, which builds on this tutorial.

In the olden days, whenever I wanted to start learning a new programming language I’d rewrite my bank account management software in that language. I started in Pascal and progressed through Modula 2 and C++ and Java. Later I augmented the Java version with Ruby (via JRuby) when I added automatic reconciliation of bank statements. It has been years since I changed anything consequential on the Java side.

In the last several years I’ve used JetBrains IDEs for other languages, and after recently upgrading to their “All Products Pack” I decided to use this Java project to experiment with the switch from Eclipse to IntelliJ IDEA. I quickly discovered that I didn’t have any standalone scripting to build or test the Java project, having previously relied solely on Eclipse to handle the details. Thus the next step was to create a standalone Ant script, since a project needs a definitive way to build independent of any IDE, and Ant has support for the typical operations involved in building and running Java programs.

The existing layout of the bank account project is very simple:

sources/org/emptyhammock/trx/*.java
The bank account manager, which should yield Trx.jar
tests/*.java
JUnit 4 testcases
lib/*.jar
junit-4.12.jar and its dependency hamcrest-core-1.3.jar

In terms of an outline of the Ant script (build.xml), here are the necessary targets:

<project>
    <target name="build">
    </target>

    <target name="clean">
    </target>

    <target name="test">
    </target>

    <target name="install">
    </target>
</project>

build will compile all the Java code to .class and create the JAR file for the bank account manager, and clean will remove all of these artifacts. test will run the JUnit tests and report results while install will place the JAR file in a well-known location ($HOME/bin, in Unix terms).

The next refinements are to set the default target, for when ant is invoked without parameters, and to declare the dependencies, such as the fact that test target requires that the build has already been performed:

<project default="test">
    <target name="build">
    </target>

    <target name="clean">
    </target>

    <target name="test" depends="build">
    </target>

    <target name="install" depends="build">
    </target>
</project>

The command-line usage is thus:

# Run unit tests, first building if necessary
$ ant
$ ant test

# Build artifacts
$ ant build

# Copy Trx.jar to well-known location for binaries, first building if necessary
$ ant install

# Remove all build artifacts
$ ant clean

But so far the script doesn’t actually do anything. The next step is to determine where the build artifacts will be placed. The artifacts are

  • the .class files for the bank account manager
  • the .jar file for the bank account manager
  • the .class files for the JUnit test classes

These will be generated in the following directories, respectively:

  • build/application/classes
  • build/application/jar
  • build/tests/classes

The directory names don’t use terminology specific to this project, so it will be easy to reuse this convention in another project with analogous artifacts.

This new version of the Ant script defines properties for the directories with the input files and the output artifacts:

<project default="test">
    <!-- input directories and files -->
    <property name="app.src.dir" value="sources" />
    <property name="tests.src.dir" value="tests" />
    <property name="lib.dir" value="lib" />

    <!-- output directories and files -->
    <property name="build.dir" value="build" />
    <property name="app.classes.dir" value="${build.dir}/application/classes" />
    <property name="app.jar.dir" value="${build.dir}/application/jar" />
    <property name="app.jar.name" value="Trx.jar" />
    <property name="app.jar.main-class" value="org.emptyhammock.trx.TrxEdit" />
    <property name="tests.classes.dir" value="${build.dir}/tests/classes" />
    <property name="install.dir" value="${user.home}/bin" />

    <target name="build">
    </target>

    <target name="clean">
    </target>

    <target name="test" depends="build">
    </target>

    <target name="install" depends="build">
    </target>
</project>

Note the declaration of the class that should run when the JAR is invoked (property app.jar.main-class).

With these properties in place, here are the steps for the build target:

<target name="build">
    <mkdir dir="${app.classes.dir}" />
    <mkdir dir="${app.jar.dir}" />
    <mkdir dir="${tests.classes.dir}" />

    <javac srcdir="${app.src.dir}" destdir="${app.classes.dir}" />
    <jar destfile="${app.jar.dir}/${app.jar.name}" basedir="${app.classes.dir}">
        <manifest>
            <attribute name="Main-Class" value="${app.jar.main-class}" />
        </manifest>
    </jar>

    <javac srcdir="${tests.src.dir}" destdir="${tests.classes.dir}">
        <classpath>
            <fileset dir="${lib.dir}" includes="**/*.jar"/>
            <path location="${app.jar.dir}/${app.jar.name}"/>
        </classpath>
    </javac>
</target>

The key points for this implementation of the build target are

  • A simple manifest is created for the JAR file that simply specifies the class which should be used for the entry point, in lieu of a more complicated manifest that would live in a separate source file.
  • The JUnit test compilation must be able to access JUnit and its dependency as well as Trx.jar.
  • The bank account manager and the test classes are built for this target, instead of deferring the build of the test classes to the test target. The reason is that even if running tests is skipped, the build target should still confirm that the classes expected by the unit tests are included in Trx.jar.

The clean and install implementations need no explanation:

<target name="clean">
    <delete dir="${build.dir}" />
</target>

<target name="install" depends="build">
    <copy file="${app.jar.dir}/${app.jar.name}" todir="${install.dir}" />
</target>

Implementation of the test target leads to a dilemma about how to invoke the JUnit tests:

  • Implement a test runner and invoke that from Ant like any other Java program
  • Use JUnit-specific features of Ant to run the individual testcases

The test runner for the first choice could look like this:

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(
            TrxAmountTest.class,
            TrxCatTest.class,
            TrxDateParserTest.class,
            TrxDateTest.class,
            TrxSetTest.class,
            TrxTest.class
        );

        for (Failure failure : result.getFailures()) {
            System.err.println(failure.toString());
        }

        if (result.wasSuccessful()) {
            System.exit(0);
        }
        System.exit(1);
    }
}

However, instead of running that program for the test target I’ll instead use Ant’s JUnit integration in order to avoid hard-coding the list of test classes and also to get better reporting of errors. Here’s the working test target:

<junit printsummary="on" haltonfailure="yes" fork="true">
    <classpath>
        <fileset dir="${lib.dir}" includes="**/*.jar"/>
        <path location="${app.jar.dir}/${app.jar.name}"/>
        <pathelement location="${tests.classes.dir}"/>
    </classpath>
    <formatter type="brief" usefile="false" />
    <batchtest>
        <fileset dir="${tests.src.dir}" includes="**/*Test.java" />
    </batchtest>
</junit>

With this addition, the entire build.xml is:

<project default="test">
    <!-- input directories and files -->
    <property name="app.src.dir" value="sources" />
    <property name="tests.src.dir" value="tests" />
    <property name="lib.dir" value="lib" />

    <!-- output directories and files -->
    <property name="build.dir" value="build" />
    <property name="app.classes.dir" value="${build.dir}/application/classes" />
    <property name="app.jar.dir" value="${build.dir}/application/jar" />
    <property name="app.jar.name" value="Trx.jar" />
    <property name="app.jar.main-class" value="org.emptyhammock.trx.TrxEdit" />
    <property name="tests.classes.dir" value="${build.dir}/tests/classes" />
    <property name="install.dir" value="${user.home}/bin" />

    <target name="build">
        <mkdir dir="${app.classes.dir}" />
        <mkdir dir="${app.jar.dir}" />
        <mkdir dir="${tests.classes.dir}" />

        <javac srcdir="${app.src.dir}" destdir="${app.classes.dir}" />
        <jar destfile="${app.jar.dir}/${app.jar.name}" basedir="${app.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${app.jar.main-class}" />
            </manifest>
        </jar>

        <javac srcdir="${tests.src.dir}" destdir="${tests.classes.dir}">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar"/>
                <path location="${app.jar.dir}/${app.jar.name}"/>
            </classpath>
        </javac>
    </target>

    <target name="clean">
        <delete dir="${build.dir}" />
    </target>

    <target name="test" depends="build">
        <junit printsummary="on" haltonfailure="yes" fork="true">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar"/>
                <path location="${app.jar.dir}/${app.jar.name}"/>
                <pathelement location="${tests.classes.dir}"/>
            </classpath>
            <formatter type="brief" usefile="false" />
            <batchtest>
                <fileset dir="${tests.src.dir}" includes="**/*Test.java" />
            </batchtest>
        </junit>
    </target>

    <target name="install" depends="build">
        <copy file="${app.jar.dir}/${app.jar.name}" todir="${install.dir}" />
    </target>
</project>

There are a few glitches that I haven’t yet mentioned:

  • This error message is issued for every javac invocation:

    warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
    
  • The messy path to Trx.jar ("${app.jar.dir}/${app.jar.name}") is specified a handful of times. It would be helpful to define a property representing the full pathname.

  • One of my unit tests is failing, and the traceback doesn’t show the line number:

    [junit] Running TrxTest
    [junit] Testsuite: TrxTest
    [junit] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.025 sec
    [junit] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.025 sec
    [junit]
    [junit] Testcase: test2(TrxTest): FAILED
    [junit] expected:<15.97 [DB]> but was:<15.97 [CR]>
    [junit] junit.framework.AssertionFailedError: expected:<15.97 [DB]> but was:<15.97 [CR]>
    [junit]   at TrxTest.test2(Unknown Source)
    
  • The project has no provision for automatically obtaining its dependencies. Currently that’s just JUnit and its dependency, but as those aren’t tracked in Git they need to be obtained manually on any system where I develop the application.

The fix for the warning: 'includeantruntime' message is to simply include the attribute includeantruntime="false" on every javac invocation. Ant has a nice feature which will allow us to define our own javac variation which will alway set that attribute appropriately. This project-specific javac could be useful later if any other changes need to be made to all javac invocations. Here is the definition of our project-specific javac:

<presetdef name="project.javac">
    <javac includeantruntime="false" />
</presetdef>

With this defined, we’ll use <project.javac /> instead of <javac /> globally.

The following shortcut will be added to simplify references to Trx.jar:

<property name="app.jar.pathname" value="${app.jar.dir}/${app.jar.name}" />

The simplest solution for providing better diagnostics in stack traces is to set the debug attribute on all javac invocations, which can be handled in our project-specific javac. The new project.javac definition is:

<presetdef name="project.javac">
    <javac debug="on" includeantruntime="false" />
</presetdef>

Handling the dependencies in an automated fashion will be covered in a future blog post.

Here is the final build.xml with all but the dependency problem resolved:

<project default="test">
    <!-- input directories and files -->
    <property name="app.src.dir" value="sources" />
    <property name="tests.src.dir" value="tests" />
    <property name="lib.dir" value="lib" />

    <!-- output directories and files -->
    <property name="build.dir" value="build" />
    <property name="app.classes.dir" value="${build.dir}/application/classes" />
    <property name="app.jar.dir" value="${build.dir}/application/jar" />
    <property name="app.jar.name" value="Trx.jar" />
    <property name="app.jar.pathname" value="${app.jar.dir}/${app.jar.name}" />
    <property name="app.jar.main-class" value="org.emptyhammock.trx.TrxEdit" />
    <property name="tests.classes.dir" value="${build.dir}/tests/classes" />
    <property name="install.dir" value="${user.home}/bin" />

    <presetdef name="project.javac">
        <javac debug="on" includeantruntime="false" />
    </presetdef>

    <target name="build">
        <mkdir dir="${app.classes.dir}" />
        <mkdir dir="${app.jar.dir}" />
        <mkdir dir="${tests.classes.dir}" />

        <project.javac srcdir="${app.src.dir}" destdir="${app.classes.dir}" />
        <jar destfile="${app.jar.pathname}" basedir="${app.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${app.jar.main-class}" />
            </manifest>
        </jar>

        <project.javac srcdir="${tests.src.dir}" destdir="${tests.classes.dir}">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar"/>
                <path location="${app.jar.pathname}"/>
            </classpath>
        </project.javac>
    </target>

    <target name="clean">
        <delete dir="${build.dir}" />
    </target>

    <target name="test" depends="build">
        <junit printsummary="on" haltonfailure="yes" fork="true">
            <classpath>
                <fileset dir="${lib.dir}" includes="**/*.jar"/>
                <path location="${app.jar.pathname}"/>
                <pathelement location="${tests.classes.dir}"/>
            </classpath>
            <formatter type="brief" usefile="false" />
            <batchtest>
                <fileset dir="${tests.src.dir}" includes="**/*Test.java" />
            </batchtest>
        </junit>
    </target>

    <target name="install" depends="build">
        <copy file="${app.jar.pathname}" todir="${install.dir}" />
    </target>
</project>

Now I can use ant from the command-line for build and test tasks instead of relying on IDE support. An added bonus is that IntelliJ has built-in Ant integration which allows the Ant targets to be accessible from within the IDE:

View of Ant window in IntelliJ

IntelliJ documentation for setting up the Ant integration: