JUnit Primer

 Summary
This article demonstrates how to write and run simple test cases and test suites using the JUnit testing framework. This article also demonstrates how to use the JUnit Load Testing Extensions to write and run performance and scalability tests.

Mike Clark
October 7, 2000


 
  Introduction

The goal of this article is to demonstrate a quick and easy way to write and run JUnit test cases and test suites. We'll start by reviewing the benefits of using JUnit and then write some example Java code to demonstrate its effectiveness.

This article contains the following sections:

Before you start, make sure you have downloaded and installed the following software:

 
  Why Use JUnit?

It's worth asking why we should use JUnit at all. The subject of unit testing always conjures up visions of long nights slaving over a hot keyboard trying to meet the project's test case quota. However, unlike the draconian style of conventional unit testing, using JUnit actually helps you write code faster while increasing code quality by creating synergy between coding and testing.

Here are just a few reasons for using JUnit:

 
  Design Of JUnit

JUnit is designed around two key design patterns: the Command pattern and the Composite pattern.

A TestCase is a command object. Any class that contains test methods must subclass the TestCase class. A TestCase can define any number of testXXX() methods. When you want to check the expected and actual test results, you invoke any variation of the assert() convenience method and pass a boolean expression that returns true to indicate the assertion is true. TestCase subclasses which contain multiple testXXX() methods can use the setUp() and tearDown() convenience methods to initialize and release any common objects under test.

TestCase instances can be composed into TestSuite hierarchies, which automatically invoke all the testXXX() methods defined in each TestCase instance. A TestSuite is a composite of other tests, either TestCase instances or other TestSuite instances. TestSuite instances and TestCase instances can be added to a TestSuite using the addTest() convenience method. This supports suites of suites of suites of tests, to an arbitrary depth. It's the power of this composite behavior that makes it easy to compose a hierarchy of test suites and test cases and run all the tests automatically and uniformly.

 
  Step 1: Write A Test Case

First, we'll write a test case to exercise a single software component. We'll focus on writing tests that exercise the component behavior that has the highest potential for breakage, thereby maximizing our return on testing investment.

To write a test case, follow these steps:

  1. Define a subclass of TestCase.
  2. Override the setUp() method to initialize object(s) under test.
  3. Override the tearDown() method to release object(s) under test.
  4. Define one or more testXXX() methods which exercise the object(s) under test.
  5. Define a suite() factory method which creates a TestSuite containing all the testXXX() methods of the TestCase.
  6. Define a main() method for running the TestCase.

The following is an example test case:

(The complete source code for this example is available in the Resources section).

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

public class ShoppingCartTest extends TestCase {

    private ShoppingCart _bookCart;

    /**
     * Constructs a ShoppingCartTest with the specified name.
     *
     * @param name Test case name.
     */
    public ShoppingCartTest(String name) {
        super(name);
    }

    /**
     * Sets up the text fixture.
     *
     * Called before every test case method.
     */
    protected void setUp() {
        _bookCart = new ShoppingCart();

        Product book = new Product("Extreme Programming", 23.95);
        _bookCart.addItem(book);
    }

    /**
     * Tears down the text fixture.
     *
     * Called after every test case method.
     */
    protected void tearDown() {
        _bookCart = null;
    }

    /**
     * Tests the emptying of the cart.
     */
    public void testEmpty() {
        _bookCart.empty();
        assert(_bookCart.isEmpty());
    }

    /**
     * Tests adding a product to the cart.
     */
    public void testProductAdd() {

        Product book = new Product("Refactoring", 53.95);
        _bookCart.addItem(book);

        double expectedBalance = 23.95 + book.getPrice();
        double currentBalance = _bookCart.getBalance();
        double tolerance = 0.0;

        assertEquals(expectedBalance, currentBalance, tolerance);

        int expectedItemCount = 2;
        int currentItemCount = _bookCart.getItemCount();

        assertEquals(expectedItemCount, currentItemCount);
    }

    /**
     * Tests removing a product from the cart.
     *
     * @throws ProductNotFoundException If the product was not in the cart.
     */
    public void testProductRemove() throws ProductNotFoundException {

        Product book = new Product("Extreme Programming", 23.95);
        _bookCart.removeItem(book);

        double expectedBalance = 23.95 - book.getPrice();
        double currentBalance = _bookCart.getBalance();
        double tolerance = 0.0;

        assertEquals(expectedBalance, currentBalance, tolerance);

        int expectedItemCount = 0;
        int currentItemCount = _bookCart.getItemCount();

        assertEquals(expectedItemCount, currentItemCount);
    }

    /**
     * Tests removing an unknown product from the cart.
     */
    public void testProductNotFound() {

        try {
            Product book = new Product("Ender's Game", 4.95);
            _bookCart.removeItem(book);

            fail("Should raise a ProductNotFoundException");

        } catch(ProductNotFoundException pnfe) {
            // should never get here
        }
    }

    /**
     * Assembles and returns a test suite for
     * all the test methods of this test case.
     *
     * @return A non-null test suite.
     */
    public static Test suite() {

        //
        // Reflection is used here to add all
        // the testXXX() methods to the suite.
        //
        TestSuite suite = new TestSuite(ShoppingCartTest.class);

        //
        // Alternatively, but prone to error when adding more
        // test case methods...
        //
        // TestSuite suite = new TestSuite();
        // suite.addTest(new ShoppingCartTest("testEmpty");
        // suite.addTest(new ShoppingCartTest("testProductAdd");
        // suite.addTest(new ShoppingCartTest("testProductRemove");
        // suite.addTest(new ShoppingCartTest("testProductNotFound");
        //

        return suite;
    }

    /**
     * Run the test case.
     */
    public static void main(String args[]) {
        String[] testCaseName = {ShoppingCartTest.class.getName()};
        //junit.textui.TestRunner.main(testCaseName);
        //junit.swingui.TestRunner.main(testCaseName);
        junit.ui.TestRunner.main(testCaseName);
    }
}

 
  Step 2: Write A Test Suite

Next, we'll write a test suite that includes several test cases. The test suite will allow us to run all of its test cases in one fell swoop.

To write a test suite, follow these steps:

  1. Define a subclass of TestCase.
  2. Define a suite() factory method which creates a TestSuite containing all the TestCase instances and TestSuite instances contained in the TestSuite.
  3. Define a main() method for running the TestSuite.

The following is an example test suite:

public class EcommerceTestSuite extends TestCase {
	
    /**
     * Constructs a EcommerceTestSuite with the specified name.
     *
     * @param name Test suite name.
     */
    public EcommerceTestSuite(String name) {
        super(name);
    }

    /**
     * Assembles and returns a test suite
     * containing all known tests.
     *
     * New tests should be added here.
     *
     * @return A non-null test suite.
     */
    public static Test suite() {

        TestSuite suite = new TestSuite();
	
        suite.addTest(ShoppingCartTest.suite());
        suite.addTest(CreditCartTestSuite().suite());

        return suite;
    }

    /**
     * Runs the test suite.
     */
    public static void main(String args[]) {
        String[] testCaseName = {EcommerceTestSuite.class.getName()};
        //junit.textui.TestRunner.main(testCaseName);
        //junit.swingui.TestRunner.main(testCaseName);
        junit.ui.TestRunner.main(testCaseName);
    }
}

 
  Step 3: Run The Tests

Now that we've written a test suite containing a collection of test cases and other test suites, we can run either the test suite or any of its test cases individually. Running a TestSuite will automatically run all of its subordinate TestCase instances and TestSuite instances. Running a TestCase will automatically invoke all of its defined testXXX() methods.

JUnit provides both a graphical and a textual user interface. Both user interfaces indicate how many tests were run, any errors or failures, and a simple completion status. The graphical user interface displays either an AWT-based (junit.ui.TestRunner) or Swing-based (junit.swingui.TestRunner) window that displays a green progress bar if all the tests passed or a red progress bar if any of the tests failed. The textual user interface (junit.textui.TestRunner) displays "OK" if all the tests passed and failure messages if any of the tests failed.

In general, TestSuite and TestCase classes should define a main() method which employs the appropriate user interface. The tests we've written so far have defined a main() using the AWT-based user interface.

Running our test case is simple:

java ShoppingCartTest

Running our test suite, and all of its test cases, is equally simple:

java EcommerceTestSuite

The simplicity of the user interfaces is the key to running tests quickly. You should be able to run your tests and know the status with a glance, much like you do with a compiler.

 
  Step 4: Organize The Tests

The last step is to decide where the tests will live within our development environment.

Here's the recommended way to organize tests:

  1. Create a test package for each of your Java packages. For example, the com.mydotcom.ecommerce package would have a test package defined in com.mydotcom.ecommerce.test package.
  2. In the com.mydotcom.ecommerce.test package, define a TestSuite class that contains all the tests for verifying the code in this package.
  3. Define similar TestSuite classes that create higher-level and lower-level test suites in the other packages (and sub-packages) of the application.

By creating a TestSuite in each Java package, at various levels of packaging, you can run a TestSuite at any level of abstraction. For example, you can define a com.mydotcom.test.MasterTestSuite which runs all the tests in the system and a com.mydotcom.ecommerce.test.EcommerceTestSuite which runs only those tests for the e-commerce classes.

The testing hierarchy can extend to an arbitrary depth. Depending on the level of abstraction you're developing at in the system, you can run an appropriate test. Just pick a level of abstraction in the system and test it!

Here's an example testing hierarchy:

 
  Testing Idioms

Keep these things in mind when testing:

 
  Load Testing Extensions

The JUnit Load Testing Extensions were developed as extensions to the JUnit framework for the purpose of assemblying and executing performance and scalability tests.

BaseTestCase

A base class for test cases which includes some convenience methods for executing test cases.

Any BaseTestCase, or TestSuite which extends the BaseTestCase, can be run as follows:

java <TestName>   // Graphical UI

java <TestName> -text   // Text UI

TimedTest

A TestCase decorator that runs the decorated TestCase and displays the begin, end, and elapsed time in milliseconds. Each instance of a TimedTest has a specified maximum elapsed time, and if the max time is exceeded, then the test case fails.

The following is an example of how to construct a TimedTest:

//
// Creates a timed test of the testSomething() method
// with a max elapsed time of 2 seconds.
//
Test t = new MyTestCase("testSomething");
Test timedTest = new TimedTest(t, t.getName(), 2000);

Timer

An interface which defines the methods that must be implemented by pluggable timers. The ConstantTimer has a constant delay, and the RandomTimer has a random delay with a uniformly distributed variation.

LoadTestCase

A TestCase which has a specified number of maximum users and an elapsed time. This class provides a makeLoadTest() factory method which decorates the LoadTestCase as a timed and threaded test. Once a LoadTestCase has been decorated in this fashion, all the methods registered in the suite will be run as an atomic unit concurrently with the specified maximum number of simulated users. If the elapsed time is exceeded, then the test case fails.

ExampleLoadTest

A LoadTestCase which tests the creation of random numbers.

UrlLoadTest

A LoadTestCase which tests the response time of a collection of URLs.

LoadTestSuite

The top-most test suite used to run all known load tests. It includes two example load test cases - ExampleLoadTest and UrlLoadTest. You won't want to run this as part of your defect testing suite, as it will defeat the purpose of rapid feedback. This is a candidate for the lunch hour test suite.

The following is an example LoadTestCase:

public class ExampleLoadTest extends LoadTestCase {
	

    /**
     * Constructs an ExampleLoadTest with the specified name.
     * 
     * The test simulates 10 concurrent users ramping
     * at 1 user per second with a maximum elapsed time
     * of 2 minutes.
     *
     * @param name Test name.
     */
    public ExampleLoadTest(String name) {
        super(name);
        setMaxUsers(10);
        setTimer(new ConstantTimer(1000));
        setMaxElapsedTime(120000);
    }

    /**
     * Sets up the test fixture.
     */
    protected void setUp() {
        super.setUp();
    }

    /**
     * Tears down the test fixture.
     */
    protected void tearDown() {
        super.tearDown();
    }

    /**
     * Assembles and returns a test suite for all
     * the test methods of this class.
     *
     * All the load-related methods should be added here.
     *
     * @return A non-null test suite.
     */
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.addTest(makeLoadTest(new ExampleLoadTest("testRandomGen")));
        return suite;
    }

    /**
     * Example load test.
     */
    public void testRandomGen() {
        java.util.Random r = new java.util.Random();
        for (int i=0; i < 100000; i++) {
            r.nextDouble();
        }
    }

    /**
     * Test main.
     */
    public static void main(String args[]) {
        String[] testCaseName = {ExampleLoadTest.class.getName()};
        //junit.textui.TestRunner.main(testCaseName);
        //junit.swingui.TestRunner.main(testCaseName);
        junit.ui.TestRunner.main(testCaseName);
    }
}

 
  Resources


 About the author
Mike Clark is an independent consultant for Clarkware Consulting, specializing in Java-based architecture, design, and development using J2EE technologies.