Since its introduction several years ago, JUnit has become an indispensible tool for Java developers for writing automated test cases. Now, JUnit 4.5 has taken unit testing to a whole new level with a new, more flexible architecture and some long-overdue new features. Here, you will learn how JUnit 4.5 makes writing test cases even easier than ever before.
Getting Started
To use JUnit 4.5, you will need to download it at http://sourceforge.net/project/showfiles.php?group_id=15278&package_id=12472 or go to http://www.junit.org and click on the Download JUnit link. The download is just a single .jar file, junit-4.5.jar. This file includes the latest framework for writing JUnit test cases and test suites, as well as the original JUnit 3.8 classes for backwards compatibility. JUnit 4.5 relies on assertions, a Java language feature introduced in JDK 1.5, so you will need to use JDK 1.5 or later to compile and run the examples. To begin writing test cases and test suites, all you need to do is include the .jar file in your classpath.
Although JUnit test cases can be executed at the command line by using the java command with the appropriate runner class, it is handy to use the Ant <junit> tag for this purpose instead. You will need to download Ant version 1.7.1 or later from http://ant.apache.org/bindownload.cgi to do so. Once you have installed Ant, you will need to copy junit-4.5.jar to the ANT_HOME/lib directory, so that Ant can make the necessary calls to JUnit to process the <junit> tag. The examples in this article are executed using Ant, and a build.xml file demonstrating the use of the <junit> tag is included in the source code.
Obviously, before you can write any test cases, you will need something to test. Listing 1 shows a simple program, NumberCruncher.java, with a handful of simple number calculation functions. The test cases in the examples refer to this code, which contains the following functions:
- Public Boolean isPrime(Integer theNumber): Returns true if the integer passed in is a prime number.
- Public Integer factorial(Integer theNumber): Returns the factorial value of the number passed in. For example, 6 factorial = 1 x 2 x 3 x 4 x 5 x 6 = 720.
- Public Object[] getFactors(Integer theNumber): Returns an array of Integer objects that are factors of the number passed in.
- Public Integer doForever(): This method would return an integer value, but actually performs an infinite loop.
Listing 1: NumberCruncher.java
package com.dlt.developer.numbers; import java.util.ArrayList; public class NumberCruncher { public boolean isPrime(Integer theNumber) { boolean value = true; for (int i = 2; i < theNumber.intValue(); i++) { if (theNumber.intValue() % i == 0) { value = false; break; } // if } // for i return value; } // isPrime() public Integer factorial(Integer theNumber) throws IllegalArgumentException { if (theNumber.intValue() < 1) { throw new IllegalArgumentException("Factorial cannot be computed for numbers less than 1."); } // if int value = 1; for (int i = 2; i <= theNumber.intValue(); i++) { value = value * i; } // for return new Integer(value); } // factorial() public Object[] getFactors(Integer theNumber) { ArrayList factors = new ArrayList(); for (int i = 1; i <= theNumber.intValue(); i++) { if (theNumber.intValue() % i == 0) { factors.add(new Integer(i)); } // if } // for i return factors.toArray(); } // getFactors() public Integer doForever() { Integer theValue = new Integer(0); boolean finished = false; while (!finished) { theValue = new Integer(theValue.intValue()+1); } // while return theValue; } // takeForever() } // NumberCruncher
The next section demonstrates how to write a test case using JUnit 4.5 to test out some of the functionality in NumberCruncher.java, and highlights some changes between versions 4.5 and 3.8.
Writing a Test Case
Listing 2 shows a set of JUnit 4.5 test cases for NumberCruncher.java. Listing 2.1 shows some of the equivalent test cases using JUnit 3.8.
Listing 2: NumberCruncherTestCase45.java
package com.dlt.developer.junit; import com.dlt.developer.numbers.NumberCruncher; import org.junit.*; import static org.junit.Assert.*; public class NumberCruncherTestCase45 { @BeforeClass public static void init() { // Do one-time setup for all test cases } // init() @Before public void doSetup() { // Do setup before each test case } // setUp() @After public void doTearDown() { // Do tear down after each test case } // tearDown() @AfterClass public static void destroy() { // Do tear-down after all test cases are finished } // destroy() @Test public void testIsPrime() { NumberCruncher nc = new NumberCruncher(); assertTrue("17 is a prime number", nc.isPrime(new Integer(17))); assertFalse("16 is not a prime number", nc.isPrime(new Integer(16))); } // testIsPrime() @Test public void testFactorial() { NumberCruncher nc = new NumberCruncher(); assertEquals(new Integer(120), nc.factorial(new Integer(5))); } // testFactorial() @Test (expected = IllegalArgumentException.class) public void testFactorialInvalid() { NumberCruncher nc = new NumberCruncher(); nc.factorial(new Integer(-4)); } // testFactorialInvalid() @Test (timeout = 5000) public void testDoForever() { NumberCruncher nc = new NumberCruncher(); assertEquals(new Integer(0), nc.doForever()); } // testDoForever() @Test public void testGetFactors() { NumberCruncher nc = new NumberCruncher(); Object[] f1 = nc.getFactors(new Integer(32)); Object[] f2 = nc.getFactors(new Integer(32)); assertEquals("Factors are equal", f1, f2); } // testGetFactors() @Ignore ("Not ready yet") public void testSomethingElse() { NumberCruncher nc = new NumberCruncher(); } // testSomethingElse() } // NumberCruncherTestCase45
Listing 2.1: NumberCruncherTestCase38.java
package com.dlt.developer.junit; import com.dlt.developer.numbers.NumberCruncher; import junit.framework.TestCase; public class NumberCruncherTestCase38 extends TestCase { public void setup() { // Do setup before each test case } // setUp() public void tearDown() { // Do tear down after each test case } // tearDown() public void testIsPrime() { NumberCruncher nc = new NumberCruncher(); assertTrue("17 is a prime number", nc.isPrime(new Integer(17))); assertFalse("16 is not a prime number", nc.isPrime(new Integer(16))); } // testIsPrime() public void testFactorial() { NumberCruncher nc = new NumberCruncher(); assertEquals(new Integer(120), nc.factorial(new Integer(5))); } // testFactorial() public void testFactorialInvalid() { NumberCruncher nc = new NumberCruncher(); boolean gotException = false; try { nc.factorial(new Integer(-4)); } catch (IllegalArgumentException ex) { gotException = true; } // catch assertTrue("Factorial Validation", gotException); } // testFactorialInvalid() public void testGetFactors() { NumberCruncher nc = new NumberCruncher(); Object[] f1 = nc.getFactors(new Integer(32)); Object[] f2 = nc.getFactors(new Integer(32)); assertTrue("Factor Arrays Equal Length", f1.length == f2.length); for (int i = 0; i < f1.length; i++) { assertTrue("Array Element " + i + " Is Equal", f1[i].equals(f2[i])); } // for i } // testGetFactors() } // NumberCruncherTestCase38
There are some very significant differences between the JUnit 4.5 test cases and the JUnit 3.8 version.
The first noteworthy difference is that the familiar junit.framework package is not used in NumberCruncherTestCase45.java. Furthermore, this class does not even extend the JUnit TestCase class. So, how can JUnit execute the test cases?
JUnit 4.5 uses a new feature available in JDK1.5 and later called assertions. The line:
import static org.junit.Assert.*;
is necessary, because the code must import the JUnit assertions rather than using the new Java assert keyword. Without this statement, the code will not compile.
Next, notice many instances of the @ sign followed by one or more keywords in the code that look like JavaDoc annotations. These annotations are the mechanism that JUnit 4.5 uses to work its magic.
The first example of this is the @Test annotation. In JUnit 3.8 and earlier, all test case methods must begin with “test”; for example, the testIsPrime() method. In JUnit 4.5, this convention is no longer necessary. Any method that has the @Test annotation before the method signature is recognized as a test case by the JUnit test runner and is executed as such. This flexibility means that you are free to name your test case methods as you see fit. Note, however, that it is necessary to import the org.junit package for the compiler to recognize the assertions and compile the code.
As mentioned earlier, it is no longer necessary to extend the junit.framework.TestCase class when writing a test case. This is particularly helpful, because Java does not allow multiple inheritance. Before, developers were locked in to creating objects of type TestCase, meaning that test cases had to be separated from the rest of the business logic within an application. Now, it is possible to write test cases directly inside of the business objects that are being tested. A business object can extend some other class and still be executable by the JUnit framework.
Another difference between the JUnit 4.5 and 3.8 architecture is the way in which test case fixtures are prepared and taken down. A fixture is a pre-existing condition that is necessary to run a test case, such as initialization of variables, allocation of database connections, and so on.
In JUnit 3.8, the setup() and teardown() methods were executed automatically before each “testXXX()” method was invoked. Now, it is only necessary to annotate the setup and destruction methods of test case fixtures with @Before and @After, respectively. The JUnit test runner will invoke the method annotated with @Before prior to running each @Test method, and will run the method annotated with @After when the test case is finished. As with the individual test case methods themselves, you are now free to name these test fixture methods as you see fit.
Similarly, it is now possible to create fixtures that run once at the beginning of the entire test case class’s execution and once when all test cases are completed. Suppose the test cases required that an application’s properties be initialized from a configuration file, or that a connection pool of JDBC connections should be created. Likewise, the connection pool would need to be deallocated when the test cases are done. Denoting the setup method with the @BeforeClass annotation and the cleanup method with the @AfterClass annotation will notify the JUnit test runner to invoke these methods when the class is initialized and after all tests are done.
In JUnit 3.8, the only way to handle this situation was to code class-level fixture setup in the constructor and class-level fixture tear-down in the object finalizer. JUnit 4.5 has improved upon this approach by introducing a true life cycle model for all phases of the testing process.
New Features of JUnit 4.5
So far, you have seen how JUnit 4.5 accomplishes the same things that could be accomplished in version 3.8 and earlier. Now, here are some juicy new tidbits of functionality to make writing test cases easier.
First, examine the testFactorialInvalid() method from NumberCruncherTestCase38.java below:
public void testFactorialInvalid() { NumberCruncher nc = new NumberCruncher(); boolean gotException = false; try { nc.factorial(new Integer(-4)); } catch (IllegalArgumentException ex) { gotException = true; } // catch assertTrue("Factorial Validation", gotException); } // testFactorialInvalid()
Here, an invalid value of -4 is passed to the NumberCruncher class’s factorial() method, and the anticipated IllegalArgumentException is caught in an exception-handling block. Although this is not overly complex, it still lends itself to the possibility of bugs in the test case itself.
Compare this to the same method from NumberCruncherTestCase45.java below:
@Test (expected = IllegalArgumentException.class) public void testFactorialInvalid() { NumberCruncher nc = new NumberCruncher(); nc.factorial(new Integer(-4)); } // testFactorialInvalid()
Here, the expected= attribute of the @Test annotation tells JUnit to expect an IllegalArgumentException to be thrown when the factorial() method is invoked. This test case requires much less coding and is far less complex than its JUnit 3.8 counterpart above, greatly reducing the possibility of a bug occurring in the test case itself.
Best practices indicate that test cases should be written as a part of the development process, rather than being postponed until the code is long finished. In JUnit 3.8, doing this meant that a test case that was not yet ready for prime-time must either be commented out or renamed so that it didn’t conform to the “testXXX()” convention for test case methods. To get around this, JUnit 4.5 has introduced the @Ignore annotation, as shown below:
@Ignore ("Not ready yet") public void testSomethingElse() { NumberCruncher nc = new NumberCruncher(); } // testSomethingElse()
This annotation causes the JUnit test runner to disregard the test case completely until it is ready to be executed. The message is used for JUnit’s output when it encounters an ignored test case.
Another complex situation that is easily addressed in JUnit 4.5 is when a method fails to complete in a timely manner. This might be due to network latency, an inefficient algorithm, or perhaps even the ever-dreaded infinite loop. Consider the test case below for the NumberCruncher class’s appropriately-named doForever() method:
@Test (timeout = 5000) public void testDoForever() { NumberCruncher nc = new NumberCruncher(); assertEquals(new Integer(0), nc.doForever()); } // testDoForever()
Here, the timeout= attribute tells JUnit to wait up to 5000 milliseconds for the doForever() method to return, after which the test case fails.
Checking for timeouts was not even a part of the original JUnit framework. Thus, performing this same check in JUnit 3.8 and earlier would mean doing some home-grown coding to kick off the test as a separate thread and notify the caller when the test took too long. As with the exception-handling example earlier, it is best for the test case itself to be as simple as possible, so the approach used in JUnit 4.5 is practically foolproof by comparison.
Another handy bit of functionality added in JUnit 4.5 is the new assertEquals() method that accepts array arguments. Consider the JUnit 3.8 test case below for testing the NumberCruncher’s getFactors() method:
public void testGetFactors() { NumberCruncher nc = new NumberCruncher(); Object[] f1 = nc.getFactors(new Integer(32)); Object[] f2 = nc.getFactors(new Integer(32)); assertTrue("Factor Arrays Equal Length", f1.length == f2.length); for (int i = 0; i < f1.length; i++) { assertTrue("Array Element " + i + " Is Equal", f1[i].equals(f2[i])); } // for i } // testGetFactors()
As you can see, the only way to verify whether the factors returned in the arrays are really identical is to check the array lengths to see if they are equal and to loop through all of the elements of the arrays, comparing each element individually. This is somewhat tedious, and possibly error-prone. JUnit 4.5 has addressed this problem, as you can see from the following:
@Test public void testGetFactors() { NumberCruncher nc = new NumberCruncher(); Object[] f1 = nc.getFactors(new Integer(32)); Object[] f2 = nc.getFactors(new Integer(32)); assertEquals("Factors are equal", f1, f2); } // testGetFactors()
The array length assertion and loop in the previous example are now replaced with a single assertion that takes the two arrays as arguments. Thus, the complexity of writing the test case itself is removed in JUnit 4.5, hopefully leading to bug-free test cases.
Writing a Test Suite
Groups of test cases are commonly referred to as a test suite. They often test blocks of related functionality within a system that depend on the same code. Listing 3 below shows how a test suite would be coded in JUnit 3.8:
Listing 3: NumberCruncherTestSuite38.java
package com.dlt.developer.junit; import junit.framework.*; public class NumberCruncherTestSuite38 { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(new TestSuite(NumberCruncherTestCase38.class)); return suite; } // suite() } // NumberCruncherTestSuite38
This code relies on the junit.framework.TestSuite class. Here, a TestSuite object is created, and one or more individual test case classes are added to the suite. The static suite() method returns the TestSuite object for the JUnit test runner to use in order to invoke the various test cases.
In JUnit 4.5, annotations are used to perform the same functions. Listing 3.1 demonstrates this.
Listing 3.1: NumberCruncherTestSuite45.java
package com.dlt.developer.junit; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ NumberCruncherTestCase45.class }) public class NumberCruncherTestSuite45 { } // NumberCruncherTestSuite45
Here, the test suite is essentially an empty class declaration with the desired test case classes added using the @Suite annotation. This annotation associates the test case class with the test case runner, which, in this case, is the org.junit.runners.Suite class, as specified with the @RunWith annotation. You can specify any of the other specialized test case runners found in the org.junit.runners package on the @RunWith annotation as well.
Although the test suite class above is an empty shell, this does not have to be the case. Again, as with the test cases, the test suite actually can be integrated into the business layer of an application without any impact to the functionality of the business objects.
So, what should you do if you need to run JUnit 4.5 test cases using JUnit 3.8? Luckily, JUnit provides an adapter class for this purpose. To use the adapter, create the usual JUnit 3.8 test suite class above, but code the suite() method as follows:
public static junit.framework.Test suite() { return new JUnit4TestAdapter(NumberCruncherTestCase45.class); } // suite()
As you can see above, the adapter class takes the JUnit 4.5 test case class and allows a JUnit 3.8 test runner to invoke the proper test case methods automatically.
Running the Test Cases
There are two ways to run JUnit test cases. The first is to invoke a test runner at the command line, as shown by the following example:
$%JAVA_HOME%binjava -cp "%classpath -ea org.junit.runner.JUnitCore com.dlt.developer.junit. NumberCruncherTestSuite45
Note that all test runners in JUnit 4.5 are text-based; the old Swing version that was available previously is now deprecated.
The other alternative is to use Ant and the <junit> tag. An example of this is shown below:
<target name="test45" depends="compile-test"> <junit> <classpath refid="classpath.test" /> <formatter type="plain" usefile="false" /> <test name="com.dlt.developer.junit. NumberCruncherTestSuite45" /> </junit> </target>
This Ant target runs the JUnit 4.5 test cases using the NumberCruncherTestSuite45.java class discussed earlier. The output is plain text that is echoed to the console. There are many fancier options for saving the output of extensive test suites to various output formats, such as XML, and even for applying XSL stylesheets to the output for control over the appearance of the report that is generated.
The target for JUnit 3.8 is nearly identical, except of course that the test suite class is NumberCruncherTestCase38.java. The build.xml file that is included with the source code in the Resources section has a target for running the JUnit 4.5 examples (target “test45”), the JUnit 3.8 examples (target “test38”), and for running both (target “all”). Remember that you will need to download Ant version 1.7.1 or later to use the <junit> tag to run JUnit 4.5 test suites.
Conclusion
Now, you should be familiar with the new features of JUnit 4.5, and should have a good idea of how JUnit 4.5 differs from the old, tried-and-true JUnit 3.8. The new features and more flexible architecture of JUnit 4.5 make an already invaluable tool even better.
References
For a good discussion of software testing in general, see “Software Testing for Programmers, Part 1,” and for a detailed introduction to the features of JUnit 3.8, see “Software Testing for Programmers, Part 2.”
JUnit Documentation and Downloads
The complete code examples from this article and the Ant script to execute them can be downloaded here.
About the Author
David Thurmond is a Sun Certified Developer with over fifteen years of software development experience. He has worked in the agriculture, construction equipment, financial, home improvement and logistics industries.