The Fin Manual

This manual references Fin 1.2
Last updated: August 6, 2002
Author: Dale Anson

Description

Fin is a replacement for JUnit. The Fin article explains the rational for a different approach than that used by JUnit. This manual assumes you have some familiarity with unit testing and makes occasional reference to JUnit. JUnit has become the defacto standard for unit testing within the extreme programming community, but as explained in the article, there are a few problems with JUnit that Fin attempts to correct. If you are new to unit testing and extreme programming in general, pick up a copy of Kent Beck's excellent "Extreme Programming Explained".

Fin has several parts: a methodology for constructing tests, some test runners, some Ant tasks, and a postprocessor.

First, you should know how to write Fin tests.

Second, you should know how to run Fin to execute the tests. Fin can be used in four ways:

And last, you should know how to postprocess your classes. Fin comes with one utility application, a "postprocessor". The postprocessor can strip out the test methods from the compiled classes to help reduce the footprint of your distributed classes. The postprocessor can be used as Ant task.

You may already have unit tests written for JUnit. Converting JUnit tests to Fin tests is straight forward, as explained below.


How to Write Fin Tests

Fin runs tests on compiled classes. Classes compiled for Fin testing need to use a Java 1.4 compiler with the -source 1.4 parameter passed to the compiler. Both the javac compiler that is issued with the Java 1.4 JDK and Jikes version 1.16 support compiling with assertions enabled. See the instructions for the particular compiler to ensure that this option is used correctly. For javac, the command would look like this:

javac -classpath $CLASSPATH -source 1.4 -d $OUTPUT *.java

With Ant, use the source=1.4 option to the javac task, like this:

      <javac  srcdir="${src.home}"
            destdir="${build.home}"
            source="1.4">
            <classpath refid="compile.classpath"/>
      </javac>

Notice that debug is turned on by default in the above examples. When the debug option is on, the compiled classes will contain line number and source file information, which is useful to help track down the location of failed tests. Once all tests have passed and the code is ready for distribution, the debug setting can be turned off which will help reduce the size of the distributed code.

For writing tests, some example code should be all that is necessary:

package ise.fin;

import java.awt.*;
import java.io.*;

public class FinUtil {
   
   /**
    * Counts the class files in the given directory.
*
* @param dir directory to count class files * @param cnt running count of class files * @return total number of class files */ public int countFiles( File dir, int cnt ) { if ( dir == null || !dir.isDirectory() ) return cnt; File[] files = dir.listFiles(); for ( int i = 0; i < files.length; i++ ) { File f = files[i]; if ( f.isDirectory() ) cnt = countFiles( f, cnt ); else { if ( f.getName().endsWith( ".class" ) ) ++cnt; } } return cnt; } /** * A unit test for Fin */ private void testCountFiles() { String dir = "/usr/local/java/antelope/build"; int cnt = countFiles( new File( dir ), 0 ); assert cnt == 103 : "Check that " + dir + " actually contains 103 class files."; } /** * Run Fin tests. * * @return the number of tests run. */ public static int runTests() { // set up FinUtil fu = new FinUtil(); // test int x = 0; fu.testCountFiles(); ++x; return x; } }

The nice color coding of the source is courtesy of the "Code2HTML" plugin for jEdit.

In the Fin way of testing, write your classes as usual. Instead of writing a separate test class for each of your application classes, write the test methods directly into your source. In each source file, add a method public static int runTests() similar to the one above. Within this method, make a call to each of your test methods. Return the number of test methods called.

Fin expects that AssertionErrors will be thrown on test failure. If you're not familiar with the assert, here's the syntax:

assert <boolean expression>[:message];

Any expression that evaluates to true or false can be used. An AssertionError will be thrown if the expression does not evaluate to true. The ": message" part is optional, but it is nice to use. Fin will output the message in the event of an assert failure, which can be helpful in tracking down the failure.

One of the main reasons to use Fin is that in the above example, the tests will run regardless of the visibility of the countFiles method. The tests will run exactly the same if the method is public, protected, or private.

Sometimes a method won't return a value that can be tested with assert. Sometimes a method may throw an exception on failure. The next example shows how to deal with an exception. Fin would prefer an AssertionError to be thrown on failure, so wrap the exception in an AssertionError as shown below and throw it. In this example, the test goes beyond ensuring that the loadClass method is successful, it also checks that the class can be manipulated correctly by calling getClassStats and getRunTestsMethod, either of which could throw an exception if there is a problem with reflection.

   /**
    * A unit test for Fin
    */
   private void testLoadClass() {
      try {
         setUserClasspath( "/usr/local/java/dbrowser/classes:" +
"/usr/local/java/dbrowser/lib/kappalayout.jar"
); Class c = loadClass( "ise.dbrowser.BarHandler" ); assert c != null; assert c.getName().equals( "ise.dbrowser.BarHandler" ); int[] stats = getClassStats( c ); java.lang.reflect.Method method = getRunTestsMethod( c ); } catch ( Exception e ) { throw new AssertionError( e ); } }


Using Fin with Ant

Installing Fin with Ant

Copy fin.jar and bcel.jar to your Ant lib directory. Add the fin task to your build file:

<taskdef name="fin" classname="ise.fin.FinTask">

Put this near the top of the build file, the best place is just after the <project> tag.

Parameters
AttributeDescriptionRequired
directory Fin will recurse this directory (also known as the "base directory") and test each class file that it finds. Yes
testfile If used, fin will look for this file in the base directory and test it. If not used, fin will assume you want to test all files in the base directory. No
usegui Fin can display a window showing the output of a test run. Set usegui to true to show this window. The default is false. Either way, fin will always send test output to the console. No
classpath This should be set to the same classpath as used for compiling. Fin will look for classes as needed in this path. The classpath may contain directories, individual classes, jar files, and zip files. This is optional, if not used, fin will look for dependencies only in the base directory. This may also be set as a nested <classpath> element or as a reference, exactly like the classpath is set for the java or javac tasks. No
classpathref The classpath to use, given as a reference to a path defined elsewhere. No

Parameters specified as nested elements

classpath

Fin's classpath attribute is a PATH like structure and can also be set via a nested classpath element.

Examples In the first example, Fin will run tests on a single in the diretory specified by the ${build.home} property. The class to be tested is specified by the ${test.file} property. The GUI will not be used to show the output of the test, so all output will be directed to System.err.

   <target name="test"
            depends="prepare"
            description="Tests a single file with Fin.">
      <fin directory="${build.home}"
           testfile="${test.file}"
           usegui="false">
          <classpath refid="compile.classpath"/> 
      </fin>
   </target>

In the next example, Fin will run tests on all classes in the directory specified by the ${build.home} property. The GUI will be used to show the test output, and the classpath specified by the reference 'compile.classpath' will be used as needed.

   
   <target name="test all"
            depends="prepare"
            description="Tests all files with Fin.">
      <fin directory="${build.home}"
           usegui="true">
          <classpath refid="compile.classpath"/> 
      </fin>
   </target>   

Using Fin from the command line

Fin can be ran from the command line, but does require one option not normally used. As Fin relies on the assert keyword, the -ea option must be passed to java. The following examples assume that the fin.jar and bcel.jar files are in the classpath. In the examples, items surrounded by [ and ] are optional.

The basic command line is:

java -ea ise.fin.Fin basedir [file1] [file2] [file3] [...]

The base directory is the top directory containing the classes to be tested, just like what would be added to the classpath for running the classes. In the examples below, the base directory is '/usr/local/java/fin/classes'.

The files may be listed in several forms: 1) the absolute path (for example, /usr/local/java/fin/classes/ise/fin/Fin.class), 2) the path relative to the base directory (for example, ise/fin/Fin.class), and 3) as a fully qualified class name (for example, ise.fin.Fin).

This example will cause Fin to run tests on FinUtil.class:

java -ea -cp fin_1.1.jar:lib/bcel.jar ise.fin.Fin /usr/local/java/fin/classes ise/fin/FinUtil.class

and so will this example:

java -ea -cp fin_1.1.jar:lib/bcel.jar ise.fin.Fin /usr/local/java/fin/classes ise.fin.FinUtil

This is the output from the test ran with the above commands:

[Fin] ===== Begin Testing Sat, 27 Jul 2002 11:54:16 =====
[Fin] Starting tests in directory /usr/local/java/fin/classes
[Fin] Testing class: ise.fin.FinUtil
[Fin] 3 tests for ise.fin.FinUtil complete.
[Fin] Tests finished.
[Fin] ===== Test Results =====
[Fin] Ran 3 tests on 1 class.
[Fin] Classes with tests = 1
[Fin] Classes without tests = 0:
[Fin] ALL TESTS PASSED.
[Fin] ===== FINished Testing Sat, 27 Jul 2002 11:54:18 =====

This example will cause Fin to test itself and all other classes in the fin distribution:

java -ea ise.fin.Fin /usr/local/java/fin/classes


Using Fin's GUI Test Runner

Fin also has a nice, Swing-based, graphical user interface. To start the graphical interface, use:

java -ea -jar fin.jar

or if fin.jar is in the classpath,

java -ea ise.fin.FinRunner

As explained in the command line instructions, the -ea parameter must be used.

Here is a picture of the user interface:

This should be self-explanatory, but here are the steps:

  1. Pick the directory containing the classes to test in the top box.
  2. Enter one or more files to test in the second box (leave blank to test all files). If entering the files by hand, use the path separator as appropriate for the operating system between each file, for example, on a Unix system, use something like:

    ise.fin.Fin:ise.fin.FinRunner:ise.fin.FinTask

    On Windows, it would look like this:

    ise.fin.Fin;ise.fin.FinRunner;ise.fin.FinTask

    If the "Add file(s)" button is used, the appropriate path separator will be inserted automatically.

  3. Set up the class path in the bottom box. Note that Fin doesn't necessarily use the same class path for running the tests as is used for running Fin. This classpath should contain the items necessary to run your application classes. Again, use the path separator as appropriate for the operating system.
  4. Check the "Show log" check box to see the raw output of the test run.
  5. Click the "Test" button to start testing.

Test progress will be indicated on the progress bar. Like JUnit, the bar will be all green if all tests pass, and will be red if one or more fail. Once testing is complete, a new window will open with the test results.


Using Fin as a jEdit plugin

Fin can be installed as a jEdit plugin. To install, simply copy fin.jar to the ${user.home}/.jedit/jars directory and restart jEdit. An alternate method is to copy fin.jar to the "jars" directory in the jEdit installation directory. Fin will be an option on the "Plugins" menu. These instructions are automatically installed into jEdit's help system. Fin is activated by clicking Plugins - Fin - Show for setting up the test parameters. Fin is then run exactly like in the GUI instructions above. There is a menu item to start the tests, Plugins - Fin - Test, that can be mapped to a shortcut key if desired (Utilities - Global Options... - Shortcuts). When being used as a jEdit plugin, the detailed test output will be shown in the "Console" window. This output is a duplicate of the output shown when the "Show log" box is checked.


Using Fin's Postprocessor

Fin's postprocessor strips test methods from compiled classes. This can reduce the footprint of your distributed classes, although there is something to be said for distributing your classes with the tests intact. Fin uses the Byte Code Engineering Library to scan each class file, find each method whose name starts with "test", and removes that method from the class file. Obviously, this should be used with care. Having a policy that only Fin-style test methods may start with "test" will help.

To use the postprocessor, from the command line enter:

java ise.fin.PostProcessor basedirectory

where "basedirectory" is the directory containing classes that you want to strip test methods from.

Using the postprocessor from the command line has limited options. The methods to be stripped are those starting with "test" and all public static int runTests() methods will will be stripped also. The postprocessor can also be invoked as an Ant task, which gives access to more options.

Using Fin's Postprocessor with Ant

Fin's postprocessor is better controlled via Ant than from the command line. Define the postprocessor task as follows:

<taskdef name="postprocessor" classname="ise.fin.PostProcessFiles">

Put this near the top of the build file, the best place is just after the <project> tag.

Parameters

AttributeDescriptionRequired
directory Fin will recurse this directory (also known as the "base directory") and strip methods from each class file that it finds. Yes
prefix The prefix of the methods to strip. The default is "test", but could be anything. You could even strip out all "main" or "set" methods. No, defaults to "test".
stripruntests Strip methods with signature public static int runTests(). The default is true. No, defaults to "true".

Examples:

In this example, the postprocessor will strip the test methods and the public static int runTests() methods:

   <target name="strip" description="Removes tests from classes.">
      <postprocessor directory="${build.home}"/>    
   </target>

In the next example, the postprocessor will strip all "set" methods, and not strip the public static void runTests() methods:

   <target name="strip" description="Removes sets from classes.">
      <postprocessor directory="${build.home}" prefix="set" stripruntests="false"/>    
   </target>


Converting JUnit Tests to Fin Tests

In typical usage, JUnit tests are written as separate classes from the application classes. Each application class has a corresponding test class, each method in the application class has a corresponding method in the test class. Fin, on the other hand, does not have separate test classes, rather, in the Fin way of testing, the test methods exists side by side in the application class with the methods being tested.

As a first step, insert a public static int runTests() method in each of your test classes and call each of the test methods from the runTests method. Run Fin on your test classes. This is the easiest way to convert from JUnit to Fin, in fact, JUnit can continued to be used along with Fin in this case.

To convert completely to Fin, create a public static int runTests() method in each of your application classes. Copy the "setup" and "teardown" code from the corresponding JUnit test class to the runTests method. Copy each of the "test" methods from the JUnit test class to the application class. Insert a call to each test method into the runTests method.

Following is an example. In the table below, the original source code is on the left, the corresponding test is on the right. This is a very simple and somewhat contrived example, but is sufficient to show the process. This particular application has a separate directory and package for test classes, which means that the only methods that can be tested directly are those with public access.

Original source, compiled class is ise.scheduler.HourlyPanel
package ise.scheduler;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import library.*;
import ise.java.awt.*;

/**
 * HourlyPanel -- lets a user select the time to schedule an event 
 * that happens at a particular minute each hour.
 * @author Dale Anson, February 2002
 */
public class HourlyPanel extends TimePanel {

   private SpinField minutes;

   public HourlyPanel() {
      setLayout( new LambdaLayout() );
      KappaLayout kl = new KappaLayout();
      JPanel p = new JPanel( kl );

      minutes = new SpinField( 0, 0, 59, new HMSDocument() );

      JLabel label = new JLabel( "Minute:" );

      p.add( label, "0,0,,,E,,5" );
      p.add( minutes, "1,0" );
      add( p, "0, 0" );
   }

   /**
    * @return the selected minute, 0 - 59
    */
   public int getMinute() {
      return minutes.getValue();
   }

   /**
    * @return a string that looks like <code>&lt;hourly minute="20"/&gt;</code>   
    */
   public String toXML() {
      return "<hourly minute=\"" + getMinute() + "\"/>";
   }
}

Test source, compiled class is test.HourlyPanelTest
package test;

import junit.framework.*;
import ise.scheduler.*;
           
public class HourlyPanelTest extends TestCase { 
   private HourlyPanel hp;

   public HourlyPanelTest(String name) {
      super(name);
   }

   protected void setUp() {
      hp = new HourlyPanel();
   }

   public static TestSuite suite() {
      return new TestSuite(HourlyPanelTest.class);
   }

   public void testGetMinute() {
      assertEquals(0, hp.getMinute());
   }

   public void testToXML() {
      assertEquals("<hourly minute=\"0\"/>", hp.toXML());
   }
}

HourlyPanel.java after conversion to Fin. Note that both getMinute and toXML can now be protected.

package ise.scheduler;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import library.*;
import ise.java.awt.*;

/**
 * HourlyPanel -- lets a user select the time to schedule an event 
 * that happens at a particular minute each hour.
 * @author Dale Anson, February 2002
 */
public class HourlyPanel extends TimePanel {

   private SpinField minutes;

   public HourlyPanel() {
      setLayout( new LambdaLayout() );
      KappaLayout kl = new KappaLayout();
      JPanel p = new JPanel( kl );

      minutes = new SpinField( 0, 0, 59, new HMSDocument() );

      JLabel label = new JLabel( "Minute:" );

      p.add( label, "0,0,,,E,,5" );
      p.add( minutes, "1,0" );
      add( p, "0, 0" );
   }

   /**
    * @return the selected minute, 0 - 59
    */
   protected int getMinute() {
      return minutes.getValue();
   }
   
   private void testGetMinute() {
      assert getMinute() == 0;
   }

   /**
    * @return a string that looks like <code>&lt;hourly minute="20"/&gt;</code>   
    */
   protected String toXML() {
      return "<hourly minute=\"" + getMinute() + "\"/>";
   }
   
   private void testToXML() {
      assert toXML().equals("<hourly minute=\"0\"/>");
   }
   
   public static int runTests() {
      // setup
      HourlyPanel hp = new HourlyPanel();
      
      // test
      int x = 0;
      hp.testGetMinute();
      ++x;
      hp.testToXML();
      ++x;
      
      return x;
   }
}