Class not found with Ant, Ivy and JUnit – error in build.xml?

Example

Project contains the following files:

├── build.xml
├── ivy.xml
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── demo
    │   │           └── App.java
    │   └── resources
    │       └── log4j.properties
    └── test
        └── java
            └── org
                └── demo
                    └── AppTest.java

Build runs as follows:

$ ant 
Buildfile: /home/mark/Files/Dev/ivy/demo/build.xml

resolve:
[ivy:resolve] :: Apache Ivy 2.3.0 - 20130110142753 :: http://ant.apache.org/ivy/ ::
[ivy:resolve] :: loading settings :: url = jar:file:/home/mark/.ant/lib/ivy.jar!/org/apache/ivy/core/settings/ivysettings.xml
[ivy:resolve] :: resolving dependencies :: com.myspotontheweb#demo;working@mark-Lemur-Ultra
[ivy:resolve]   confs: [compile, runtime, test]
[ivy:resolve]   found org.slf4j#slf4j-api;1.7.5 in public
[ivy:resolve]   found org.slf4j#slf4j-log4j12;1.7.5 in public
[ivy:resolve]   found log4j#log4j;1.2.17 in public
[ivy:resolve]   found junit#junit;4.11 in public
[ivy:resolve]   found org.hamcrest#hamcrest-core;1.3 in public
[ivy:resolve] :: resolution report :: resolve 347ms :: artifacts dl 14ms
    ---------------------------------------------------------------------
    |                  |            modules            ||   artifacts   |
    |       conf       | number| search|dwnlded|evicted|| number|dwnlded|
    ---------------------------------------------------------------------
    |      compile     |   1   |   0   |   0   |   0   ||   1   |   0   |
    |      runtime     |   3   |   0   |   0   |   0   ||   3   |   0   |
    |       test       |   5   |   0   |   0   |   0   ||   5   |   0   |
    ---------------------------------------------------------------------
[ivy:report] Processing /home/mark/.ivy2/cache/com.myspotontheweb-demo-compile.xml to /home/mark/Files/Dev/ivy/demo/build/ivy-reports/com.myspotontheweb-demo-compile.html
[ivy:report] Processing /home/mark/.ivy2/cache/com.myspotontheweb-demo-runtime.xml to /home/mark/Files/Dev/ivy/demo/build/ivy-reports/com.myspotontheweb-demo-runtime.html
[ivy:report] Processing /home/mark/.ivy2/cache/com.myspotontheweb-demo-test.xml to /home/mark/Files/Dev/ivy/demo/build/ivy-reports/com.myspotontheweb-demo-test.html

resources:
     [copy] Copying 1 file to /home/mark/Files/Dev/ivy/demo/build/classes

compile:
    [javac] Compiling 1 source file to /home/mark/Files/Dev/ivy/demo/build/classes

compile-tests:
    [mkdir] Created dir: /home/mark/Files/Dev/ivy/demo/build/test-classes
    [javac] Compiling 1 source file to /home/mark/Files/Dev/ivy/demo/build/test-classes

test:
    [mkdir] Created dir: /home/mark/Files/Dev/ivy/demo/build/test-reports
    [junit] Running org.demo.AppTest
    [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.085 sec

build:
[ivy:retrieve] :: retrieving :: com.myspotontheweb#demo
[ivy:retrieve]  confs: [runtime]
[ivy:retrieve]  3 artifacts copied, 0 already retrieved (512kB/16ms)
      [jar] Building jar: /home/mark/Files/Dev/ivy/demo/build/dist/demo.jar

BUILD SUCCESSFUL
Total time: 4 seconds

ivy.xml

A very powerful feature of ivy is configurations. These allow you to group dependencies together.

<ivy-module version="2.0">
    <info organisation="com.myspotontheweb" module="demo"/>

    <configurations>
        <conf name="compile" description="Required to compile application"/>
        <conf name="runtime" description="Additional run-time dependencies" extends="compile"/>
        <conf name="test"    description="Required for test only" extends="runtime"/>
    </configurations>

    <dependencies>
        <!-- compile dependencies -->
        <dependency org="org.slf4j" name="slf4j-api" rev="1.7.5" conf="compile->default"/>

        <!-- runtime dependencies -->
        <dependency org="org.slf4j" name="slf4j-log4j12" rev="1.7.5" conf="runtime->default"/>

        <!-- test dependencies -->
        <dependency org="junit" name="junit" rev="4.11" conf="test->default"/>
    </dependencies>

</ivy-module>

Notes:

  • The configurations use the “extends” feature in order to emulate the Maven “compile”, “runtime” and “test” Maven scopes.
  • Note the special “conf” attribute on each dependency. This is the mapping from local to remote. For more details on how remote Maven modules are managed by ivy see: How are maven scopes mapped to ivy configurations by ivy

build.xml

Ivy configurations can be leveraged by tasks like cachepath (to create an ANT path) and retrieve (copy files into your build).
I also recommend using the report target so that you can see which jars appear in each configuration (Useful to managing transitive dependencies)

<project name="demo" default="build" xmlns:ivy="antlib:org.apache.ivy.ant">

    <!--
    ================
    Build properties
    ================
    -->
    <property name="src.dir"          location="src/main/java"/>
    <property name="resources.dir"    location="src/main/resources"/>
    <property name="test.src.dir"     location="src/test/java"/>
    <property name="build.dir"        location="build"/>
    <property name="classes.dir"      location="${build.dir}/classes"/>
    <property name="test.classes.dir" location="${build.dir}/test-classes"/>
    <property name="ivy.reports.dir"  location="${build.dir}/ivy-reports"/>
    <property name="test.reports.dir" location="${build.dir}/test-reports"/>
    <property name="dist.dir"         location="${build.dir}/dist"/>

    <property name="jar.main.class" value="org.demo.App"/>
    <property name="jar.file"       value="${dist.dir}/${ant.project.name}.jar"/>

    <available classname="org.apache.ivy.Main" property="ivy.installed"/>

    <!--
    ===========
    Build setup
    ===========
    -->
    <target name="install-ivy" description="Install ivy" unless="ivy.installed">
        <mkdir dir="${user.home}/.ant/lib"/>
        <get dest="${user.home}/.ant/lib/ivy.jar" src="http://search.maven.org/remotecontent?filepath=org/apache/ivy/ivy/2.3.0/ivy-2.3.0.jar"/>
        <fail message="Ivy has been installed. Run the build again"/>
    </target>

    <target name="resolve" depends="install-ivy" description="Use ivy to resolve classpaths">
        <ivy:resolve/>

        <ivy:report todir="${ivy.reports.dir}" graph="false" xml="false"/>

        <ivy:cachepath pathid="compile.path" conf="compile"/>
        <ivy:cachepath pathid="test.path"    conf="test"/>
    </target>

    <!--
    ===============
    Compile targets
    ===============
    -->
    <target name="resources" description="Copy resources into classpath">
        <copy todir="${classes.dir}">
            <fileset dir="${resources.dir}"/>
        </copy>
    </target>

    <target name="compile" depends="resolve,resources" description="Compile code">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}" includeantruntime="false" debug="true" classpathref="compile.path"/>
    </target>

    <target name="compile-tests" depends="compile" description="Compile tests">
        <mkdir dir="${test.classes.dir}"/>
        <javac srcdir="${test.src.dir}" destdir="${test.classes.dir}" includeantruntime="false" debug="true">
            <classpath>
                <path refid="test.path"/>
                <pathelement path="${classes.dir}"/>
            </classpath>
        </javac>
    </target>

    <!--
    ============
    Test targets
    ============
    -->
    <target name="test" depends="compile-tests" description="Run unit tests">
        <mkdir dir="${test.reports.dir}"/>
        <junit printsummary="yes" haltonfailure="yes">
            <classpath>
                <path refid="test.path"/>
                <pathelement path="${classes.dir}"/>
                <pathelement path="${test.classes.dir}"/>
            </classpath>
            <formatter type="xml"/>
            <batchtest fork="yes" todir="${test.reports.dir}">
                <fileset dir="${test.src.dir}">
                    <include name="**/*Test*.java"/>
                    <exclude name="**/AllTests.java"/>
                </fileset>
            </batchtest>
        </junit>
    </target>

    <!--
    =====================
    Build and run targets
    =====================
    -->
    <target name="build" depends="test" description="Create executable jar archive">
        <ivy:retrieve pattern="${dist.dir}/lib/[artifact]-[revision](-[classifier]).[ext]" conf="runtime"/>

        <manifestclasspath property="jar.classpath" jarfile="${jar.file}">
            <classpath>
                <fileset dir="${dist.dir}/lib" includes="*.jar"/>
            </classpath>
        </manifestclasspath>

        <jar destfile="${jar.file}" basedir="${classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${jar.main.class}" />
                <attribute name="Class-Path" value="${jar.classpath}" />
            </manifest>
        </jar>
    </target>

    <target name="run" depends="build" description="Run code">
        <java jar="${jar.file}" fork="true"/>
    </target>

    <!--
    =============
    Clean targets
    =============
    -->
    <target name="clean" description="Cleanup build files">
        <delete dir="${build.dir}"/>
    </target>

    <target name="clean-all" depends="clean" description="Additionally purge ivy cache">
        <ivy:cleancache/>
    </target>

</project>

Note:

  • The conditional “install-ivy” target will install ivy automatically. Just re-run the build, only needs to be done once.

App.java

Hello world logging example.

package org.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Hello world!
 *
 */
public class App {
    static final Logger log = LoggerFactory.getLogger(App.class);

    public static void main( String[] args ) {
        App a = new App();
        a.speak("hello world");
    }

    public void speak(String message) {
        log.info(message);
    }
}

AppTest.java

This is an old example from my archives. Not using the Junit assertions.

ackage org.demo;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * Unit test for simple App.
 */
public class AppTest 
    extends TestCase
{
    /**
     * Create the test case
     *
     * @param testName name of the test case
     */
    public AppTest( String testName )
    {
        super( testName );
    }

    /**
     * @return the suite of tests being tested
     */
    public static Test suite()
    {
        return new TestSuite( AppTest.class );
    }

    /**
     * Rigourous Test :-)
     */
    public void testApp()
    {
        assertTrue( true );
    }
}

log4j.properties

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

Leave a Comment