Test-Driven Development Series Part 2 - Testing Java Classes with JUnit

The first article is a concept piece explaining why testing helps in an enterprise server-side software environment. Most software applications today are written in tiers: the presentation tier, the logic tier (where business logic is kept), and the data tier. The logic tier is the meat of the application and comprises all of the rules and actions of the application.

Download the sample code here

Introduction

By now you should know why testing is valuable. If you don't, please see the first article in this five part series. The first article is a concept piece explaining why testing helps in an enterprise server-side software environment. Most software applications today are written in tiers: the presentation tier, the logic tier (where business logic is kept), and the data tier. The logic tier is the meat of the application and comprises all of the rules and actions of the application. The business logic usually resides in JAR files containing libraries of Java class files. People often think of testing as trying out a product. With a car, it's easy: start the ignition and drive. With desktop software, it's also easy: start up the application, then click around and type to test the functions that interest you. How do you test a JAR file filled with Java classes?

Testing Java Classes

Why, of course, by virtually starting up the Java class and pushing the buttons, so to speak. You test a Java class by calling methods on it from another Java class. The following Java source file is an example. Place the contents in a file called FactorCalculator.java, as displayed below in Listing 1:

Listing 1 (FactorCalculator.java, taken from FactorCalculator.java.v1):

import java.util.List;
import java.util.ArrayList;

public class FactorCalculator {
  public int[] factor(int number) {
    List factorList = new ArrayList();
    while(isDivisor(number, 2)) {
      factorList.add(new Integer(2));
      number /= 2;
    }
    int upperBound = (int)Math.sqrt(number) + 1;
    for(int i = 3; i <= upperBound; i += 2) {
      while(isDivisor(number, i)) {
        factorList.add(new Integer(i));
        number /= i;
      }
    }
    if (number != 1) {
      factorList.add(new Integer(number));
      number = 1;
    }
    int[] intArray = new int[factorList.size()];
    for(int i = 0; i < factorList.size(); i++) {
      intArray[i] = ((Integer)factorList.get(i)).intValue();
    }
    return intArray;
  }
  public boolean isPrime(int number) {
    boolean isPrime = true;
    int upperBound = (int)Math.sqrt(number) + 1;
    if (number == 2) {
      isPrime = true;
    } else if (isDivisor(number, 2)) {
      isPrime = false;
    } else {
      for(int i = 3; i <= upperBound; i += 2) {
        if (isDivisor(number, i)) {
          isPrime = false;
          break;
        }
      }
    }
    return isPrime;
  }
  public boolean isDivisor(int compositeNumber, int potentialDivisor) {
    return (compositeNumber % potentialDivisor == 0);
  }
}

The FactorCalculator class provides a simple interface:

  • factor: a method to factor a number into its constituent prime factors.
  • isPrime: a method to test whether a number is prime.
  • isDivisor: a method to determine whether a number is divisible by another number.

These public methods could comprise the API for a math library.

To test FactorCalculator, you could create a Java class with a main method that you invoke from the command line. The test class in listing 2 tests the FactorCalculator class you wrote earlier.

Listing 2 (CalculatorTest.java, taken from CalculatorTest.java.v1):

public class CalculatorTest {
  public static void main(String [] argv) {
    FactorCalculator calc = new FactorCalculator();
    int[] intArray;
    intArray = calc.factor(100);
    if (!((intArray.length == 4) && (intArray[0] == 2) && (intArray[1] == 2) && (intArray[2] == 5) && (intArray[3] == 5))) {
      throw new RuntimeException("bad factorization of 100");
    }
    intArray = calc.factor(4);
    if (!((intArray.length == 2) && (intArray[0] == 2) && (intArray[1] == 2))) {
      throw new RuntimeException("bad factorization of 4");
    }
    intArray = calc.factor(3);
    if (!((intArray.length == 1) && (intArray[0] == 3))) {
      throw new RuntimeException("bad factorization of 3");
    }
    intArray = calc.factor(2);
    if (!((intArray.length == 1) && (intArray[0] == 2))) {
      throw new RuntimeException("bad factorization of 2");
    }
    boolean isPrime;
    isPrime = calc.isPrime(2);
    if (!isPrime) {
      throw new RuntimeException("bad isPrime value for 2");
    }
    isPrime = calc.isPrime(3);
    if (!isPrime) {
      throw new RuntimeException("bad isPrime value for 3");
    }
    isPrime = calc.isPrime(4);
    if (isPrime) {
      throw new RuntimeException("bad isPrime value for 4");
    }
    try {
      isPrime = calc.isPrime(1);
      throw new RuntimeException("isPrime should throw exception for numbers less than 2");
    } catch (IllegalArgumentException e) {
      // do nothing because throwing IAE is the proper action
    }
    boolean isDivisor;
    isDivisor = calc.isDivisor(6, 3);
    if (!isDivisor) {
      throw new RuntimeException("bad isDivisor value for (6, 3)");
    }
    isDivisor = calc.isDivisor(5, 2);
    if (isDivisor) {
      throw new RuntimeException("bad isDivisor value for (5, 2)");
    }
    try {
      isDivisor = calc.isDivisor(6, 0);
      throw new RuntimeException("isDivisor should throw exception when potentialDivisor (the second argument) is 0");
    } catch (ArithmeticException e) {
      // do nothing because throwing AE is the proper action
    }
    System.out.println("All tests passed.");
  }
}

Note the two try-catch blocks, one while testing isPrime and another while testing isDivisor. Sometimes the proper behavior for a piece of code is to throw an exception. If you are testing such code, you must catch the exception and compare it to the exception you expect. If the thrown exception is not the right exception, you should pass it up the chain. If the code you are exercising does not throw an exception when you expect it to, you should throw your own exception in order to signal that the application is not functioning properly. You will use a similar pattern later in this article with the JUnit test code in order to test for portions of code that should throw one or more exceptions when behaving properly.

For brevity, the test code above omits some tests you really should do, such as testing what happens when the second argument to isDivisor is negative.

Compile the classes with javac *.java. Run the tests in CalculatorTest by typing java CalculatorTest.

You should get a runtime exception telling you that you should not be able to test primality of numbers less than two. It should look like the following, although the line reported as 36 might be different depending on how you format the whitespace in your CalculatorTest.java file:

Exception in thread "main" java.lang.RuntimeException: isPrime should throw exception for numbers less than 2
at CalculatorTest.main(CalculatorTest.java:36)

In other words, the FactorCalculator library is not functioning properly. You can resolve the problem by adding a check in the isPrime method at the very beginning. The check should throw IllegalArgumentException when the argument is less than two. The following stitch of code will do the trick:

if (number < 2) {
  throw new IllegalArgumentException();
}

Place the above portion of code as the first line of the isPrime method in the FactorCalculator class. For your reference, the revised version of the isPrime method is in FactorCalculator.java.v2, which you should rename to FactorCalculator.java if you plan to use it instead of typing in the changes.

Once you have added the check to isPrime, recompile the classes and re-run CalculatorTest. The FactorCalculator class should now pass all tests.

For those not familiar with number theory, here is some brief background. Every positive whole number greater than or equal to two is either prime or the product of two or more prime factors. A number is considered prime if it has no other whole number factors than one and itself. The algorithm in the FactorCalculator class to calculate the prime factors of a number is to first try dividing two into the number as many times as possible, then to try dividing every odd number, starting with three and ending with the square root of the number, into the number as many times as possible.

JUnit Provides Advantages as a Test Framework

Testing Java classes at its core involves not much more than what you have just done. The big differences between a real-life test and the simple example test you just wrote are size and complexity of the code base. With a larger code base, you need aggregate reporting. The example test above stops at the first error rather than collecting as many errors as it can before stopping and reporting on which tests passed. Software development would be slow indeed if you had to recompile and re-run a test suite for every single failed test. Oftentimes bugs are related or are caused by the interaction of multiple sections of code. Seeing multiple test failures at once can help you isolate the cause of a particular bug and resolve it, as well as resolve related bugs quickly.

You need to know some terminology and testing concepts before proceeding to the next step of rewriting the test to make use of JUnit.

  • Unit test: a unit test is a small quantity of code, nearly always fitting in a single Java class, that tests a very specific portion of a software application or library. Unit tests verify the proper behavior of small units of code such as individual classes or small numbers of classes. They are used to test EJB components and regular Java class libraries, whether those class libraries execute in a server-side environment or a standalone environment. The primary difference between unit tests and functional tests, also defined in this lexicon, is that unit tests focus on constituent components that generally cannot be seen by end users, whereas functional tests focus on "pushing the buttons" of the application, so to speak. In JUnit, a unit test could be considered either a single method in a TestCase class or possibly the entire TestCase class. A good size for a unit test is a page or two of code. Ten pages is probably too many and warrants splitting into multiple fine-grained tests.
  • Functional test: a functional test is a test that verifies the proper behavior of the application from the perspective of the end user. Functional testing is synonymous with black box testing.
  • Black box test: a black box test is a test that relies only on the published interface or public contract and not upon knowledge of how an application is implemented. This usually means you only know what the inputs should be and only test what the expected outputs are and do not know how the application generates the output or other peripheral characteristics such as performance or side effects of code, unless those aspects of the code are part of the public contract. Black box testing is especially important if you develop a software library with an API that other developers -- your customers -- rely upon. It is important both that the software library does what the published interface says and that the software library refrains from doing what the published interface forbids or omits. If you develop a software application, conforming to the published interface is also important because it promotes effective collaboration with your teammates.
  • White box test: a white box test is a test that exercises the functionality of a piece of code with specific knowledge of how that piece of code is implemented. White box tests are useful when you want to test the implementation for specific behavior that is not specified in the public contract but is important nonetheless, perhaps because of performance issues. White box tests are also useful in cases where the implementation of a particular algorithm or section of business logic is particularly tricky. In those cases you can use white box testing to focus on errors that are likely to happen or otherwise difficult to isolate without knowledge of internals, which you would not have with black box testing.
  • In-container test: an in-container test resides in the servlet or EJB container and thus can communicate more directly with the code it is meant to test. The Jakarta Cactus project produces a free testing tool called Cactus that lets you execute tests in the same container as the code being tested, whether the container is a servlet container or an EJB container. In-container testing is not much use to black box functional testing, but it is a boon to unit testing.
  • Test case: a test case in the world of JUnit is a group of related tests. A test case is expressed as a class that inherits from junit.framework.TestCase. A test case usually has multiple methods, each testing some small aspect of application behavior. The test methods in a test case are usually named with a prefix of "test", although they can be named anything that does not conflict with other methods.
  • Test suite: a test suite is a group of test cases or test suites. It is expressed as a class that inherits from junit.framework.TestSuite. It is not necessary for a test suite to contain only test cases or only test suites. A test suite may contain both test cases and test suites. A child test suite that is contained within a parent test suite may also have a mix of test cases and test suites as its children, thereby allowing nested tests. If you wanted, you could set up small groups of test cases, each group testing a small and well-defined area of your application. Each of those small groups could be collected together into a test suite. Then, you could aggregate those test suites into a master test suite that put the entire application through its paces, functional area by functional area.
  • Test runner: the test runner is the JUnit class that bootstraps the process of running tests. You invoke the test runner, which then in turn executes the tests that you define. There are several ways of defining the tests you want the test runner to execute. These ways of defining the tests are covered in a later section of this article entitled “Specifying Which Tests to Run”. JUnit has three different test runners: text, AWT, and Swing, with class names junit.textui.TestRunner, junit.awtui.TestRunner, and junit.swingui.TestRunner, respectively.

Writing a Test with JUnit

To write a test using JUnit, extend the junit.framework.TestCase class. Your subclass of TestCase should simply invoke the test cases in the order you desire, possibly also performing setup before the test cases and teardown after the test cases. The setup is done in a method called setUp. The teardown is done is a method called tearDown. You may override both of these to do what you want, but it is not necessary to override them.

Here is the example test case above rewritten to take advantage of JUnit:

Listing 3 (CalculatorTest.java, taken from CalculatorTest.java.v2):

import junit.framework.TestCase;

public class CalculatorTest extends TestCase {

  private FactorCalculator calc;

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

  protected void setUp() {
    calc = new FactorCalculator();
  }

  public void testFactor() {
    int numToFactor;
    int[] factorArray;
    int[] correctFactorArray;

    numToFactor = 100;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2, 2, 5, 5};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 4;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2, 2};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 3;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {3};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 2;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));
  }

  // presumes both factor arrays are in numeric order
  private boolean isSameFactorArray(int[] factorArray1, int[] factorArray2) {
    boolean isSame = false;
    if (factorArray1.length == factorArray2.length) {
      isSame = true;
      for(int i = 0; i < factorArray1.length; i++) {
        if (factorArray1[i] != factorArray2[i]) {
          isSame = false;
          break;
        }
      }
    }
    return isSame;
  }

  public void testIsPrime() {
    int numToCheck;
    boolean isPrime;

    numToCheck = 2;
    isPrime = calc.isPrime(numToCheck);
    assertTrue("bad isPrime value for " + numToCheck, isPrime);

    numToCheck = 3;
    isPrime = calc.isPrime(numToCheck);
    assertTrue("bad isPrime value for " + numToCheck, isPrime);

    numToCheck = 4;
    isPrime = calc.isPrime(numToCheck);
    assertFalse("bad isPrime value for " + numToCheck, isPrime);

    try {
      numToCheck = 1;
      isPrime = calc.isPrime(numToCheck);
      fail("isPrime should throw exception for numbers less than 2");
    } catch (IllegalArgumentException e) {
      // do nothing because throwing IAE is the proper action
    }
  }

  public void testIsDivisor() {
    int numToCheck;
    int potentialDivisor;
    boolean isDivisor;

    numToCheck = 6;
    potentialDivisor = 3;
    isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
    assertTrue("bad isDivisor value for (" + numToCheck + ", " + potentialDivisor + ")", isDivisor);

    numToCheck = 5;
    potentialDivisor = 2;
    isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
    assertFalse("bad isDivisor value for (" + numToCheck + ", " + potentialDivisor + ")", isDivisor);

    try {
      numToCheck = 6;
      potentialDivisor = 0;
      isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
      fail("isDivisor should throw an exception when potentialDivisor is 0 but did not");
    } catch (ArithmeticException e) {
      // do nothing because throwing AE is the proper action
    }
  }
}

You tell JUnit of your expectations through methods named assertXxx, where Xxx is True, False, Equals, or other condition. JUnit keeps track of the pass/fail status from the assertXxx methods and reports to you after execution of all of the tests. Here are some of the assertion methods in JUnit with a description of the signature and operation of each:

  • assertTrue(String errorMessage, boolean booleanExpression): Checks that booleanExpression evaluates to true. If not, add errorMessage to the list for display in the error report.
  • assertFalse(String errorMessage, boolean booleanExpression): Checks that booleanExpression evaluates to false. If not, add errorMessage to the list for display in the error report.
  • assertEquals(String errorMessage, Object a, Object b): Checks that Object a is equal to Object b, as defined by the equals method. If not, add errorMessage to the list for display in the error report. Object a is the expected value. Object b is the actual value from the application you are testing.
  • assertNull(String errorMessage, Object o): Checks that Object o is null. If not, add errorMessage to the list for display in the error report.

For a full list of assertion methods, consult the javadoc for the Assert class in the downloadable documentation at http://www.junit.org/junit/javadoc/index.htm.

You sprinkle the assertXxx statements throughout your test code to confirm that certain conditions are true (or false, as the case may be) of the code you are testing.

Specifying Which Tests to Run

To run your tests, you need:

  1. An instance of a TestRunner class.
  2. An instance of your test class (named MyTestClass for the purposes of this example) containing the tests you want to run. MyTestClass must extend junit.framework.TestCase.
  3. Some way of telling the chosen TestRunner instance which tests on your MyTestClass instance to run.

Creating the instance of the TestRunner class and specifying the MyTestClass instance is easy. You do so with the following command line:

java junit.textui.TestRunner MyTestClass

You can replace junit.textui.TestRunner with an alternative TestRunner for a different UI, such as junit.awtui.TestRunner for AWT or junit.swingui.TestRunner for Swing. You should replace MyTestClass with the name of your test class.

There are two ways to tell TestRunner which tests within MyTestClass you want it to run. One is implicit and one is explicit. In MyTestClass, you may choose whether to include a public static method named suite. The suite method should accept no parameters and should return a Test object. More accurately, it should return an object that implements the Test interface, since Test is an interface, not a class. Most often, you will use TestSuite and your own subclasses of TestCase. Both TestCase and TestSuite implement the Test interface.

If you omit the suite method in MyTestClass, then TestRunner uses reflection to determine which methods within MyTestClass start with the “test” prefix and assumes that it should execute all of the matching methods. This is the implicit method of informing TestRunner about which tests to run.

If you implement a suite method in MyTestClass, TestRunner calls the suite method in MyTestClass and expects to receive a Test object with information about all of the tests that TestRunner should execute. This is the explicit method of telling TestRunner which tests to run. Both TestCase and TestSuite classes implement the Test interface, meaning that you can choose to return a single TestCase or you can choose to return a TestSuite that has zero or more TestCase and/or TestSuite objects embedded, allowing for multiple tests and for a hierarchy of tests.

Specifying a Test to Run in junit.framework.TestCase

There are two ways of specifying a single test method in TestCase: one is static and the other dynamic. The static method of specifying a single test is to override the runTest method of the TestCase class with a method that calls your tests. Here is an example:

import junit.framework.TestCase;
public class MySimpleTest extends TestCase {
  public MySimpleTest(String name) {
    super(name);
  }
  public void runTest() {
    testTurnLeft();
  }
  public void testTurnLeft() {
    ... code here ...
  }
}

Sometimes the simplest and most flexible way to override TestCase.runTest is to use an anonymous inner class. The following code demonstrates this technique:

TestCase testCase = new MySimpleTest("myNameForTurnLeft") {
  public void runTest() {
    testTurnLeft();
  }
}

The anonymous inner class lets you override the runTest method in the class that instantiates the test class, meaning that you could have different implementations of runTest in different places, all of them still using MySimpleTest for the actual test methods. The class that instantiates the test class can be the same as the test class if you are instantiating the test class from its own suite method.

The dynamic way of specifying the test method in TestCase is via the name parameter to the constructor. For the MySimpleTest class above, you would write the following:

TestCase testCase = new MySimpleTest("testTurnLeft");

Because you did not override runTest, the default implementation from the TestCase class will use reflection to find the method named “testTurnLeft”. Change “testTurnLeft” to whatever method you like.

Specifying a Hierarchy of Tests to Run With junit.framework.TestSuite

The TestSuite class lets you package together many tests into a single group. The basic pattern is as follows:

TestSuite testSuite = new TestSuite();
testSuite.addTest(new MySimpleTest("testTurnLeft"));
testSuite.addTest(new CalculatorTest("testIsDivisor"));
testSuite.addTest(new TestSuite(MyThirdTest.class));

The first and second addTest calls are straightforward. The TestSuite.addTest method accepts as a parameter an object implementing the Test interface. Both MySimpleTest and CalculatorTest are subclasses of TestCase, which itself implements the Test interface. In the first and second addTest calls, you are simply adding two individual test methods to the list of tests that the TestSuite instance will execute.

The third addTest call demonstrates how you can create a hierarchy of tests to execute by including TestSuite instances within TestSuite instances. The TestSuite class implements the Test interface, so you can specify a TestSuite instance as a parameter to addTest. In the third addTest call, the new TestSuite object you instantiate contains all of the testXxx methods in the MyThirdTest class. There is no reason that the TestSuite instance you specify to addTest need be a flat list. The child TestSuite instance could also have children.

Back to the TestCase.suite() Method

Now that you have a better understanding of how to specify tests to run with TestCase and TestSuite, you can return to the TestCase.suite() method that the TestRunner class expects. Here is an example of a TestCase.suite() method that adds a single test method from a TestCase class, all test methods from a TestCase class, and a hierarchy of tests in a child TestSuite:

Listing 4 (a suite method demonstrating many different ways of specifying tests):

public static suite() {
  TestSuite globalTestSuite = new TestSuite();

  TestCase addToCartTestCase = new ShopCartTest("testAddToCart");
  globalTestSuite.addTest(addToCartTestCase);

  TestCase checkOutTestCase = new ShopCartTest("testCheckOut");
  globalTestSuite.addTest(checkOutTestCase);

  TestSuite calcTestSuite = new TestSuite(CalculatorTest.class);
  globalTestSuite.addTest(calcTestSuite);

  TestSuite fileModuleTestSuite = new TestSuite();
  fileModuleTestSuite.addTest(new ImportExportTest("testImport"));
  fileModuleTestSuite.addTest(new TestSuite(SaveFileTest.class));
  globalTestSuite.addTest(fileModuleTestSuite);

  return globalTestSuite;
}

Now that you have learned about the different ways of specifying tests to TestRunner, you should proceed to run the tests. If you added a suite method to CalculatorTest, get rid of it because the next section of this article assumes that TestRunner will run all testXxx methods in the CalculatorTest class. The suite method becomes important when you have a large number of tests.

Running the Test

Try compiling the CalculatorTest class by typing javac -classpath ~/packages/junit3.8.1/junit.jar *.java. Replace the ~/packages/junit3.8.1/junit.jar portion with the path to the junit.jar file on your system. Run the test by typing java -classpath ~/packages/junit3.8.1/junit.jar:. junit.textui.TestRunner CalculatorTest. Again, replace the junit.jar path with the correct path on your system. To avoid having to specify the classpath on the command line in the future, make sure the JUnit library and the current directory are both in your classpath. The way to do this on Linux if you use the bash shell is the following two commands:

CLASSPATH=~/packages/junit3.8.1/junit.jar:.
export CLASSPATH

Make sure to replace “~/packages/junit3.8.1/junit.jar” with the correct location of the junit.jar file on your system and make sure to also keep the trailing colon and trailing period in the classpath. On Windows the command to set an environment variable is “set”, which you should use to set your CLASSPATH to a similar value, with the exception that the forward slashes should be backslashes. The reason for including “.” in the classpath is so that the JUnit TestRunner will be able to find the CalculatorTest class, which is in the current directory. For the purposes of this article, you want to use “.” rather than hard-coding the path to the current directory because you will be doing further examples in which you want to execute or access classes in the new current directory of whatever example you are working on. The rest of this article will assume that you have set up your classpath properly.

After running the tests in CalculatorTest, you should see the following output:

...
Time: 0.008

OK (3 tests)

The periods indicate tests that JUnit ran. JUnit also displays the number of tests that passed or failed in a summary line. If one of the tests had failed, the display would instead have looked like this:

..F
Time: 0.01
There was 1 failure:
1) testAddition(Test) "expected:<5> but was:<4>"

FAILURES!!!
Tests run: 2,  Failures: 1,  Errors: 0

Different TestRunner Classes and Ways of Executing Them

There are several TestRunner classes you can use: text, AWT, and Swing. The classes are called junit.textui.TestRunner, junit.awtui.TestRunner, and junit.swingui.TestRunner, respectively. To run them, you use similar command lines:

java junit.awtui.TestRunner CalculatorTest


    -or-


java junit.swingui.TestRunner CalculatorTest

The AWT and Swing versions of TestRunner require that you be running in a graphical environment such as Windows, OS X, or X11. They display their results in an interactive, graphical format. The text UI is most often used because tests are usually run in batch mode, where interactivity is a drawback.

When you invoke TestRunner and pass it the name of your test class, the TestRunner loads up your class and uses reflection to find all of the methods whose names start with "test". Alternatively, if you did not want to pass the TestRunner class to java on the command line, there is also a way of executing a test suite directly by invoking the main method of the class containing the test suite.

Enter the following for the main method of your TestCase subclass:

public static void main(String[] argv) {
  junit.textui.TestRunner.run(suite());
}

You can replace junit.textui.TestRunner with junit.awtui.TestRunner or junit.swingui.TestRunner if you would rather use those instead.

For your convenience, a version of CalculatorTest with the suite and main methods implemented as recommended has been included in the example source files as CalculatorTest.java.v3, which you should rename to CalculatorTest.java before use. If you are developing under Linux or using the Cygwin UNIX emulation environment under Windows, you can see the differences between versions of files with the diff command, which is oftentimes instructive while learning a new subject. For example, to see the differences between CalculatorTest.java.v2 and CalculatorTest.java.v3, type diff CalculatorTest.java.v2 CalculatorTest.java.v3.

After compiling the updated CalculatorTest class, you can now run it. Instead of typing java junit.textui.TestRunner CalculatorTest, you can now type java CalculatorTest.

Adding Functionality to Your Application and to Your Test

Now say that you wish to add functionality to FactorCalculator. The test-driven approach to development counsels that you first add the test, verify that the test fails, then program the functionality to make sure it passes the test. You decide you want to add a greatest common divisor method and that you want to name it "gcd". The gcd is a mathematical function that tells you, given two positive integers x and y, the largest number that divides x and divides y. For example, the gcd of 6 and 4 is 2. The gcd of 36 and 18 is 18. The gcd of 30 and 75 is 15.

The examples just given are good test cases to formalize into code. Additionally, you should test for boundary cases and error conditions such as gcd(2, 1) and gcd(3, -1). The following code will perform the desired tests:

Listing 5:

public void testGcd() {
  assertEquals("bad value for gcd(6, 4)", 2, calc.gcd(6, 4));
  assertEquals("bad value for gcd(36, 18)", 18, calc.gcd(36, 18));
  assertEquals("bad value for gcd(30, 75)", 15, calc.gcd(30, 75));
  assertEquals("bad value for gcd(2, 1)", 1, calc.gcd(2, 1));
  try {
    calc.gcd(3, -1);
    fail("gcd should throw exception for when either argument is less than 1");
  } catch (IllegalArgumentException e) {
    // do nothing because throwing IAE is the proper action
  }
}

Add the code from listing 5 to CalculatorTest.java. If you do not want to type in the new testGcd method, you can copy CalculatorTest.java.v4 from the example source files, renaming it to CalculatorTest.java.

It is also necessary to add a stub to FactorCalculator so that CalculatorTest will compile. You make calls to a method named gcd in CalculatorTest, so you have to define a gcd method in FactorCalculator. Add the following to FactorCalculator in the body of the class:

public int gcd(int a, int b) {
  return 1;
}

If you do not want to type in the gcd method, you can copy FactorCalculator.java.v3 from the example source files and rename it to FactorCalculator.java in your current directory.

The gcd method shown above will obviously return the wrong answer most of the time, but the test-driven way of development embraces "errors first, then correction of errors" as the most effective approach for development. Test code, like any other code, has the possibility of errors. There is the possibility that your test code fails to detect errors in the application code. If you write your test code before you write the application functionality, you can verify that the test actually detects errors, thereby reducing the chance that the test is defective.

Compile both the FactorCalculator class and the CalculatorTest class by typing javac *.java. Make sure the JUnit library is in your classpath, both for the compile command and for the execution of the test. Run the test by typing java CalculatorTest. You should see the following output:

....F
Time: 0.01
There was 1 failure:
1) testGcd(CalculatorTest)junit.framework.AssertionFailedError: bad value for gcd(6, 4) expected:<2> but was:<1>
        at CalculatorTest.testGcd(CalculatorTest.java:125)
        ...
        at CalculatorTest.main(CalculatorTest.java:14)

FAILURES!!!
Tests run: 4,  Failures: 1,  Errors: 0

The test initially fails, which is both the expected and desired result. If the test did not fail, it would mean you designed or implemented the test incorrectly. JUnit echoes the message you specified, so it behooves you to write informative error messages. Now fix the FactorCalculator class so that it passes the test. Remove the "return 1;" statement in the gcd method of FactorCalculator and replace it with the following code:

Listing 6 (functional implementation of gcd method):

int gcd = 1;
int smallerInt = (a < b) ? a : b;
for(int i = smallerInt; i > 1; i--) {
  if (isDivisor(a, i) && isDivisor(b, i)) {
    gcd = i;
    break;
  }
}
return gcd;

If you do not want to type in the new gcd method, you can copy FactorCalculator.java.v4 from the example source files, renaming it to FactorCalculator.java.

Recompile FactorCalculator by typing javac FactorCalculator.java. Re-run the test by typing java CalculatorTest. You will still get a failure because the gcd method does not throw an exception when the arguments are invalid. Specifically, invoking gcd(3, -1) should cause an IllegalArgumentException, but does not. You can remedy this by adding the following code to the top of the gcd method:

if ((a < 1) || (b < 1)) {
  throw new IllegalArgumentException();
}

The revised FactorCalculator is in FactorCalculator.java.v5 from the example source files, which you can rename to FactorCalculator.java. Recompile FactorCalculator.java and re-run the test. Everything should pass just fine, yielding a status report like so:

....
Time: 0.008

OK (4 tests)

Conclusion

Now that you know how to unit test with JUnit, try it out on some of your own code to experience for yourself the advantages of programmatic testing.

Stay tuned for the next article in the test-driven development series. The next article, the third of five in the series, will go into implementation details of how you test server-side EJB components within the EJB container.

About the Author

Wellie Chao has been interested in software development since 1984 and has been doing software development professionally since 1994. He has led software development projects to build enterprise-class software applications and has extensive experience with Java and Perl. He is the author of several Java books, including one called Core Java Tools (Prentice Hall) that covers extreme programming and test-driven development using open source Java tools such as Ant, CVS, and JUnit. His published articles on IBM developerWorks, DevX, TheServerSide, and elsewhere cover topics of interest to Java developers in enterprise software development settings. He graduated with honors from Harvard University, where he studied economics and computer science, and now lives in New York City.

Dig Deeper on Development tools for continuous software delivery