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:
Whatever the context, you probably don't want this:
[a]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:
We probably intend that "equal" arrays have the same length, and equal elements at each position. No problem:
[c]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:
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:
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:
- Iterate over the array, adding a primitive wrapper for each element to
a
List. - Create a fixed-size derivative of
Listthat uses a primitive array as the backing store, à laArrays.asList().
Here's how the approaches might look:
[f]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:
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):
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:
These assertions are getting a bit long-winded for my taste. Let's go
ahead with that ArrayAssert idea.
Some items of note:
- The examples of treating arrays as bags or sets are purely contrived, to drive us toward custom assertions. If ever you find yourself wanting to treat arrays in such ways in application code, ask yourself why you aren't using appropriate classes from the Java Collections Framework or extensions thereof.
- By convention, JUnit assertions provide overloads that accept a
Stringmessage as a first argument. For brevity's sake, I've left such overloads out of the example. They are trivial to add. Also in the interest of brevity, I have left out the overloads for the different types of primitive array. ArrayAssertextendsjunit.framework.Assertto gain unqualified access to the assertion methods from that class--they're static methods, so if we don't extendAssert, our other choices are:- to qualify the
assertEquals()calls fully, or - to wait for the static import feature of Java 1.5.
TestCaseextendAssertso that extensions ofTestCaseneed not qualify all those assertion calls. I won't be pedantic here, but be aware that this use of subclassing might be considered poor form [5].- to qualify the
- If the private methods of
Assertthat trigger test failures with specifically formatted messages, such asfailNotEquals(), were madeprotected, subclasses ofAssertsuch as ours could use those methods too. I've writtenArrayAssertas though it had such access. It's easy enough to build your own copy of JUnit that contains those modifications in source. assertSetEquals()andassertBagEquals()do not simply callassertEquals()with the expected and actual sets and sorted arrays, because the failures of such assertions would not show the original arrays, but the modified ones. So these assertion methods test for the appropriate equality themselves, and callfailNotEquals()usingListviews on the original arrays as arguments.- Where an operation would unduly mutate the arguments, e.g. the sorting
of arrays in
assertBagEquals(), we defensivelyclone()the arrays, then mutate the copies. - It might seem kind of funny to a consumer of
assertBagEquals()that she could hand it aComparator. I think that perhaps advertising aComparatorargument here reveals too much implementation detail. Maybe if we had another way of "bag-ifying" the arrays, we wouldn't have to resort to trickery like sorting. Unfortunately, the concept of "bag" is not directly supported in the Java Collections Framework--you're left to write your own, or find someone else's implementation [6]. - Writing tests for the assertions themselves brings to light an interesting twist. The expected outcome of a failed assertion is that an assertion exception is raised. The usual idiom for a JUnit test whose expected outcome is a raised exception is illustrated by the following:
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:
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:
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.