JUnit is a powerful library for writing programmer-level tests for Java code. Its small and readily comprehensible framework enables a shallow learning curve and has made it the de facto standard for programmer testing in Java.

JUnit's maintainers purposely keep its core minimal, so to augment or complement JUnit's functionality, programmers write extensions. Thankfully, JUnit's simplicity makes it easy to extend. There are several excellent JUnit extensions available, including JUnit Addons, Cactus, and EasyMock. These extensions provide new TestCase derivatives, assertions, test runners, or some combination thereof; or they use existing JUnit functionality in interesting ways.

This article explores writing custom assertions for use in JUnit, and along the way illustrates some interesting ways to work with Java arrays.

Suppose you're writing a JUnit test, and you want to assert that two arrays are "equal." Some such assertions, as it turns out, are more equal than others. For the following, assume we have two arrays of String called expectedNames and actualNames:

String[] expectedNames; String[] actualNames; // ...

Whatever the context, you probably don't want this:

[a]
assertEquals( "names?", expectedNames, actualNames );

Java array classes do not override the equals() method [1]. They rely on their superclass's definition of equals()--namely, that of java.lang.Object. Further, JUnit's Assert.assertEquals(Object,Object) method simply tests that expected.equals(actual); it offers no overload of assertEquals() that accepts array types or handles arrays specially [2]. Thus, [a] tests only that the two variables refer to the same array in memory; i.e. it has the same effect as:

[b]
assertSame( "names?", expectedNames, actualNames );

We probably intend that "equal" arrays have the same length, and equal elements at each position. No problem:

[c]
assertEquals( "length?", expectedNames.length, actualNames.length ); for ( int i = 0; i < expectedNames.length; ++i ) { assertEquals( "names[" + i + "]", expectedNames[ i ], actualNames[ i ] ); }

This certainly works, but I wouldn't want to have to type this over and over every time I wanted to compare arrays. "Aha, Extract Method and have your own ArrayAssert.assertEquals()!", you say [3]. That's good sense--whenever we're confronted with the potential for duplicate code, our instincts should be to factor out the duplication, into a method on an existing or brand new class. We could do that now, but before we do I want to address a few things.

As written, our array assertion shows only the first pair of mismatched elements--once an assertion fails, the test method raises an assertion exception and bails out. Maybe more mismatches lurk undetected (until the next run of the test). To see them all at once, we'd have to trap and remember assertion failures, construct some message indicating all the failures after the loop, and fail the test with the constructed message. I smell a hack. What if we wanted to see the matching elements too? Couldn't we just print the entire contents of the array? Sure, but array classes do not override toString()--we'd have to hand-roll the string representation.

Fortunately, it's easy to turn an array into something that meets our needs--just make a List out of it:

[d]
assertEquals( "names?", Arrays.asList( expectedNames ), Arrays.asList( actualNames ) );

This gives exactly the desired behavior. Arrays.asList() creates and returns a derivative of List that uses the target array as its backing store. This derivative has an overridden equals() and toString(). In accordance with List.equals()'s contract, the equals() override answers true if the comparand is a List and has the same number of elements with equal elements in corresponding positions. The overridden toString() answers something like "[Manny, Moe, Jack]". So now, if [d] fails, we see a message along the lines of:

[e]
names? expected <[Manny, Moe, Jack]> but was <[Larry, Moe, Curly]>

This is fine for object arrays, but there are no overloads of Arrays.asList() that accept arrays of primitives. To achieve similar results using primitive arrays, we'll need to come up with another way of turning the arrays into Lists. Here are two possible approaches:

  1. Iterate over the array, adding a primitive wrapper for each element to a List.
  2. Create a fixed-size derivative of List that uses a primitive array as the backing store, à la Arrays.asList().

Here's how the approaches might look:

[f]
import java.util.*; public class PrimitiveArrays { private PrimitiveArrays() { } // approach 1 public static List toList( final int[] primitives ) { List list = new ArrayList(); for ( int i = 0; i < primitives.length; ++i ) { list.add( new Integer( primitives[ i ] ) ); } return list; } // approach 2 public static List asList( final int[] primitives ) { return new AbstractList() { public Object get( int index ) { return new Integer( primitives[ index ] ); } public Object set( int index, Object item ) { Object previous = new Integer( primitives[ index ] ); primitives[index] = ((Integer) item).intValue(); return previous; } public int size() { return primitives.length; } } } }

The first approach is straightforward--PrimitiveArrays.toList() builds and returns a new ArrayList of pre-boxed primitives. This list is effectively divorced from the array; changes to one will not affect the other. The second approach is more subtle PrimitiveArrays.asList() creates and returns a derivative--an anonymous inner class, no less!--of AbstractList that uses the primitive array as its backing store. Invocations of the list's set() method change the corresponding array element. Boxing and unboxing occur "on demand" with each call to get() and set(). AbstractList provides most of the behavior, including equals() and toString(). AbstractList's overrides of modification methods like add() and retainAll() raise UnsupportedOperationException if they would actually modify the list, so effectively we have a fixed-size view on the array--precisely what Arrays.asList() offers for object arrays.

Supporting only integer arrays isn't terribly useful; we should overload PrimitiveArrays.asList() and PrimitiveArrays.toList() for arrays of all the primitive types. However, we certainly don't want to replicate all that code for the different primitive arrays. Enter java.lang.reflect.Array (ever think you'd use this?). Here's a refactored version:

[g]
import java.util.*; public class PrimitiveArrays { private PrimitiveArrays() { } // approach 1 public static List toList( final int[] primitives ) { return newBoxedList( primitives ); } // ...similar overloads of toList for other primitive array types... private static List newBoxedList( Object primitives ) { List list = new ArrayList(); for ( int i = 0, len = Array.getLength( primitives ); i < len; ++i ) { list.add( Array.get( primitives, i ) ); } return list; } // approach 2 public static List asList( final int[] primitives ) { return new PrimitiveArrayBackedList( primitives ); } // ...similar overloads of asList for other primitive array types... private static class PrimitiveArrayBackedList extends AbstractList { private final Object backingStore; private PrimitiveArrayBackedList( Object backingStore ) { this.backingStore = backingStore; } public Object get( int index ) { return Array.get( backingStore, index ); } public Object set( int index, Object item ) { Object previousItem = get( index ); Array.set( backingStore, index, item ); return previousItem; } public int size() { return Array.getLength( backingStore ); } } }

Notice how java.lang.reflect.Array staves off some duplicated, ugly, cast-filled code quite nicely by performing the boxing and unboxing of primitive array elements for us. Its get() and set() methods hide the gory details. You could even "get away with" storing, say, a java.lang.Float into a double[]-backed List, as Array.set() performs the widening conversion for you.

Attempts to store a wider boxed primitive than the backing array allows--for example, a java.lang.Long versus an int[]--cause Array.set() to raise IllegalArgumentException, however. Allowing such exceptions to propagate unhindered from our List still keeps us within the bounds of List.set()'s contract: the API documentation advertises that this exception could be raised "if some aspect of the specified element prevents it from being added to this list."

It's worth noting that these implementations are far from speed demons, what with all the instantiation, boxing, and unboxing. They are likely not ready for "prime time," and you should restrict their use to test code. [4]

So now we have the ability to turn any one-dimensional array into a List for ready comparisons via assertEquals(). Now, what if we mean to compare two arrays as though they were sets, disregarding duplicate elements? Easy--make Sets from the List views into Sets, and leverage Set's notion of equality (and of string representation):

[h]
Set expectedAsSet = new HashSet( Arrays.asList( expectedNames ) ); Set actualSet = new HashSet( Arrays.asList( actualNames ) ); // ...

Collections, such as HashSet, typically offer a "copy constructor" that creates a collection with the same contents as another collection. HashSet's copy constructor in particular discards duplicates for us.

Or what if we want to treat the arrays as "multisets", or "bags", where the number of occurrences of the contained elements is important? Sort the arrays first, and turn each into a List:

[i]
Arrays.sort( expectedNames ); Arrays.sort( actualNames ); List expectedAsList = Arrays.asList( expectedNames ); List actualAsList = Arrays.asList( actualNames ); // ...

These assertions are getting a bit long-winded for my taste. Let's go ahead with that ArrayAssert idea.

[j]
import java.util.*; import junit.framework.*; public class ArrayAssert extends Assert { protected ArrayAssert() { } public static void assertBothNullOrBothNotNull( Object first, Object second ) { if ( first == null || second == null ) { assertSame( "first and second not both null", first, second ); } } public static void assertListEquals( Object[] expected, Object[] actual ) { assertBothNullOrBothNotNull( expected, actual ); assertEquals( Arrays.asList( expected ), Arrays.asList( actual ) ); } public static void assertListEquals( int[] expected, int[] actual ) { assertBothNullOrBothNotNull( expected, actual ); assertEquals( PrimitiveArrays.asList( expected ), PrimitiveArrays.asList( actual ) ); } public static void assertSetEquals( Object[] expected, Object[] actual ) { assertBothNullOrBothNotNull( expected, actual ); List expectedAsList = Arrays.asList( expected ); List actualAsList = Arrays.asList( actual ); Set expectedAsSet = new HashSet( expectedAsList ); Set actualAsSet = new HashSet( actualAsList ); if ( !expectedAsSet.equals( actualAsSet ) ) { failNotEquals( null, expectedAsList, actualAsList ); } } public static void assertSetEquals( int[] expected, int[] actual ) { assertBothNullOrBothNotNull( expected, actual ); List expectedAsList = PrimitiveArrays.asList( expected ); List actualAsList = PrimitiveArrays.asList( actual ); Set expectedAsSet = new HashSet( expectedAsList ); Set actualAsSet = new HashSet( actualAsList ); if ( !expectedAsSet.equals( actualAsSet ) ) { failNotEquals( null, expectedAsList, actualAsList ); } } public static void assertBagEquals( Object[] expected, Object[] actual ) { assertBagEquals( expected, actual, null ); } public static void assertBagEquals( Object[] expected, Object[] actual, Comparator comparator ) { assertBothNullOrBothNotNull( expected, actual ); Object[] copyOfExpected = (Object[]) expected.clone(); Object[] copyOfActual = (Object[]) actual.clone(); Arrays.sort( copyOfExpected, comparator ); Arrays.sort( copyOfActual, comparator ); if ( !Arrays.equals( copyOfExpected, copyOfActual ) ) { failNotEquals( null, Arrays.asList( expected ), Arrays.asList( actual ) ); } } public static void assertBagEquals( int[] expected, int[] actual ) { assertBothNullOrBothNotNull( expected, actual ); int[] copyOfExpected = (int[]) expected.clone(); int[] copyOfActual = (int[]) actual.clone(); Arrays.sort( copyOfExpected ); Arrays.sort( copyOfActual ); if ( !Arrays.equals( copyOfExpected, copyOfActual ) ) { failNotEquals( null, PrimitiveArrays.asList( expected ), PrimitiveArrays.asList( actual ) ); } } }

Some items of note:

[k]
import java.util.*; import junit.framework.*; public class ArgumentExceptionTest extends TestCase { public void testConstructHashSetWithNull() { try { new HashSet( null ); fail( "should have raised NullPointerException" ); } catch ( NullPointerException success ) { } } }

This test attempts to construct a HashSet with a null argument. Since doing so should raise a NullPointerException, we wrap the constructor call in a try block, and trap the exception in a catch block. If the invocation indeed raises NullPointerException, the test passes. If the invocation raises no exception at all, execution proceeds to the fail() call, and the test is marked a failure. If an exception other than NullPointerException is raised, it propagates out of the test, and the test is marked an error.

Let's attempt to apply the idiom to a test for testListEquals(). The invocation of testListEquals() should raise an assertion exception:

[l]
import junit.framework.*; public class ArrayAssertFixture extends TestCase { public void testListEqualsFails() { try { ArrayAssert.testListEquals( new int[] { 1, 2, 3 }, new int[] { 1, 2, 4 } ); fail(); } catch ( AssertionFailedError success ) { } } }

This appears to work, but there's a subtle gotcha--"fail() can (and, in fact, always does) raise an assertion exception. We have no way of telling whether the trapped exception came from ArrayAssert (which we want) or from the fail() call, since both are in the try block. So, we need to shuffle this around a bit:

[m]
import junit.framework.*; public class ArrayAssertFixture extends TestCase { public void testListEqualsFails() { try { ArrayAssert.testListEquals( new int[] { 1, 2, 3 }, new int[] { 1, 2, 4 } ); } catch ( AssertionFailedError success ) { return; } fail(); } }

Now, if testListEquals() does not raise an assertion exception, execution proceeds out of the try-catch, and hits fail(); if it does, the catch block executes, and stops the test.

I hope the foregoing is useful to you in your Java programming and JUnit testing.

References

Endnotes

[1] JLS 10.7 shows how an array class might be defined if its declaration were written by hand.

[2] Early looks at JUnit 4 indicate that it will have special assertion methods for arrays.

[3] Or use ArrayAssert from JUnit Addons.

[4] http://developer.java.sun.com/developer/bugParade/bugs/4348595.html

[5] Along similar lines, some developers frown on implementing an interface solely to gain unqualified access to constants declared in that interface. See Effective Java. Note that with JUnit 4 there will be no need for TestCase, hence Assert's methods can be used via static import.

[6] The Jakarta Commons Collections library supports the notion of "bag" and lots of other collection-related goodies.