Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead. |
—Martin Fowler |
In my previous article on software testing—Software Testing for Programmers, Part 1—I presented the basics of software testing for programmers. In this article, I illustrate how to do unit testing of Java applications with JUnit, an open source framework for testing Java programs.
JUnit
The core of JUnit is a set of classes and interfaces that provides an easy-to-use framework for building automated tests. It is comprised of the following:
TestCase |
Defines tests and fixtures |
Test |
Defines the interface for running a test |
TestSuite |
Collects and runs a set of tests |
Assert |
Provides a set of assertion methods |
TestRunner |
Runs Tests and TestSuites |
Tests
Tests are defined in test cases. To create a test case:
- Create a class that extends
junit.framework.TestCase
. - Write at least one method that starts with the word “test.”
For example, suppose that you are developing an application to help you make homebrew (that’s right, home-brewed beer), and in the application you have a class that models a batch of brew and calculates alcohol by volume (ABV). Here is the code for the ABV calculation:
import java.util.*; public final class Brew { // ... /** * Calculate alcohol by volume. * abv = (og - fg) * 131.25 */ public double abv() { if (og < 1.0) { throw new ArithmeticException("OG < 1.0"); } if (fg < 1.0) { throw new ArithmeticException("FG < 1.0"); } return (og - fg) * 131.25; } }
Here is a test case that tests Brew.abv()
:
import junit.framework.*; public class AbvTest extends TestCase { /** * Construct an ABV test. */ public AbvTest(String name) { super(name); } /** * Test abv(). */ public void testAbv() { Brew brew = new Brew(); brew.setOrigGrav(1.042); brew.setFinalGrav(1.010); assertEquals(4.200000000000004, brew.abv(), 0.0); } }
Assertions
Assertions are used to control the result of a test by verifying that expected conditions are met. If an assertion fails, the test fails. Otherwise, the test passes.
Assertion methods are defined in junit.framework.Assert
and are inherited by TestCase
. To implement an assertion, call the assertion method that is applicable to the expected condition. A message may be specified to provide details about the assertion if it fails.
Here are some examples of assertions:
// Assert that a given value equals an expected value: assertEquals(1, x); // Assert that a condition is true: assertTrue("ABV exceeds 1.0", abv > 1.0); // Assert that a condition is false: assertFalse(abv > 1.0); // Assert that an object is null: assertNull("someObject is null.", someObject); // Assert that an object is not null: assertNotNull(someObject); // Assert that two variables refer to the same object: assertSame(obj1, obj2); // Assert that two variables do not refer to the // same object: assertNotSame(obj1, obj2);
Exceptions
Testing for expected exceptions is done by catching the exception and forcing failure, by calling fail
, if the exception is not thrown. As an example, here is the AbvTest
test case from above with some exception tests added:
import junit.framework.*; public class AbvExceptionTest extends TestCase { // ... /** * Test abv() with invalid OG. */ public void testInvalidOrigGrav() { try { Brew brew = new Brew(); brew.setOrigGrav(1.0); double abv = brew.abv(); // Force failure. fail("ArithmeticException not thrown."); } catch(ArithmeticException e) { // success } } /** * Test abv() with invalid FG. */ public void testInvalidFinalGrav() { try { Brew brew = new Brew(); brew.setFinalGrav(1.0); double abv = brew.abv(); // Force failure. fail("ArithmeticException not thrown."); } catch(ArithmeticException e) { // success } } }
Fixtures
Fixtures are implemented in test cases and are used to set up and tear down objects that are common to the tests in the test case. To implement a fixture:
- Define private variables for the items in the fixture.
- Override
setUp()
to initialize the fixture items. - Override
tearDown
to close any resources, such as files or database connections, that are opened insetUp()
.
Before a test is run, setUp()
is called. After the test completes, whether it passed or failed, tearDown()
is called. Each test gets a new fixure, so that one test does not cause side effects in another test.
Continuing with the homebrew example, suppose you add a method to Brew
that calculates the bitterness of your brew. Here is the code:
public final class Brew { // ... /** * Calculate bitterness in Homebrew Bitterness Units * (HBU). * hbu = sum [1, n]: ounces * alpha acid */ public double hbu() { double total = 0.0; for (Iterator i = hopsList.iterator(); i.hasNext(); ) { Hops hops = (Hops) i.next(); total += hops.hbu(); } return total; } }
Here is a test case that implements a fixture and a couple of tests for Brew.hbu()
:
import junit.framework.*; public class HbuTest extends TestCase { // Declare items in fixture. private Brew brew; private Hops hops1; private Hops hops2; /** * Construct an HBU test. */ public HbuTest(String name) { super(name); } /** * Set up fixture. */ protected void setUp() { brew = new Brew(); hops1 = new Hops(1.0, 1.0); hops2 = new Hops(2.0, 2.0); } /** * Test hbu() of zero hops. */ public void testZeroHops() { assertEquals(0.0, brew.hbu(), 0.0); } /** * Test hbu() of one hops. */ public void testOneHops() { brew.addHops(hops1); assertEquals(1.0, brew.hbu(), 0.0); } /** * Test hbu() of multiple hops. */ public void testMultipleHops() { brew.addHops(hops1); brew.addHops(hops2); assertEquals(5.0, brew.hbu(), 0.0); } }
Test Suites
Test suites are simply collections of tests. To create a test suite:
- Declare a
public static
method called “suite” that returns aTest
. - In the
suite
method, create aTestSuite
, and then add tests or other suites to it. - Return the test suite.
Here is an example of creating a test suite that will run all the test cases for Brew
:
import junit.framework.*; /** * AllTests is used to create a suite that contains * all tests. */ public class AllTests { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(new TestSuite(AbvTest.class)); suite.addTest(new TestSuite(HbuTest.class)); return suite; } }
Runners
JUnit comes with two test runners: one that has a Swing interface and one that runs tests from the command line.
To run tests using the Swing runner, start junit.textui.SwingRunner
.
To run tests from the command line:
java junit.textui.TestRunner [class]