I recently read the thought provoking article, Simplifying Contract Testing. It does a good job of discussing how Java interfaces act as contracts, and highlighting that method signatures go only so far towards defining the contract, pointing out that the comments/documentation end up describing key contractual behaviors. With key aspects of the contract appearing only in the text, writing useful contract tests is a valuable goal. The article also offered a JUnit solution, but that’s where it fell apart for me. The solution was thorough, but too complex for my tastes, with custom annotations, gratuitous dependency injection, and as many as three extra support classes to test an interface/implementation pair.
Inspired by the topic, I decided to return to the stated problem and look for a simpler, more pragmatic solution. In the end, I came up with a decent approach, using just stock JUnit4. So, lets start at the beginning.
Defining “The Contract”
So what is an interface’s contract? Lets use Iterator
as an example. Stripped down, the code defines the method signature contract as:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
That much is enforced by Java when you implement it. But that isn’t the full contract. Looking at the documentation there are other specified behaviors making up the contract, for example:
next
should throw NoSuchElementException
if hasNext
would return false
.
Enforcing the Contract With Tests
Since the contract is more then the interface signatures, and can’t effectively be enforced by the compiler, you’ll want to write tests to enforce it, right? These tests will cover the behavior of the signatures that are only described in the documentation. The question becomes, how do you do that efficiently? If your project has a bunch of classes that implement some interface you obviously want to avoid adding code to test the interface’s contract to each implementation’s tests.
A Pragmatic Approach With JUnit4
If you are using JUnit4, here is an outline of a pragmatic approach to contract testing:
- Make sure JUnit is only running
*Test.java
files. With maven’s surefire you do this via <include>**/*Test.java</include>
- Create an abstract
*Contract.java
for contracts you want to test, give them abstract methods to access resources needed by the tests.
- Add
@Test
methods to the *Contract.java
to test the desired behaviors.
- Have the
*Test.java
implementation test classes extend the abstracts appropriately.
The Iterator Example
As an example we’ll create the contract test for the Iterator
behavior we noted. Create IteratorContact.java
:
public abstract class IteratorContract<T> {
protected abstract Iterator<T> getNonEmptyIterator();
@Test(expected=NoSuchElementException.class)
public void nextThrowsExceptionWhenIteratorIsAtEnd()
throws Exception {
Iterator<T> anIterator = getNonEmptyIterator();
assert(anIterator != null);
assert(anIterator.hasNext() != null);
while(anIterator.hasNext()) {
anIterator.next();
}
anIterator.next();
}
}
Since the abstract class is not a *Test.java
file, JUnit will not try to run its tests. Now create a contract test for any concrete implementations. Take for example an EnumerationIterator
which creates an Iterator
adapter for an Enumeration
. The implementation test would be EnumerationIteratorTest.java
and look like:
public class EnumerationIteratorTest
extends IteratorContract<String> {
@Override
protected Iterator<String> getNonEmptyIterator() {
Vector<String> strings =
new Vector<String>();
strings.add("a");
return new EnumerationIterator(
strings.elements());
}
// Any other tests you want ...
}
JUnit4 will run that test because its a *Test.java
file. The getNonEmptyIterator()
will provide an instance to test, and the inherited @Test nextThrowsExceptionWhenIteratorIsAtEnd()
method will test the behavior of the contract behavior we desired. For any other Iterator
you’ve implemented, just add another *Test.java
extending the IteratorContract
appropriately. Rinse, lather, repeat.
In Summary
For a single Iterator
this isn’t a particularly compelling approach but consider an interface like Comparable
, it has only one method, but that one method has at least five key contractual behaviors found only in the documentation:
- equals returns 0
- less than returns < 0
- greater than returns > 0
- throw runtime exception
NullPointerException
if the argument is null
- throw runtime exception
ClassCastException
if the argument can’t be cast to the current class
If your project has implementations of Comparable
sprinkled throughout, which isn’t unrealistic, a contract test implemented as described can get you completely consistent contract testing coverage with minimal code.