Running non-static inner classes of JUnit tests
JUnit ships with an "experimental" custom runner called Enclosed
. When you annotate a test class as @RunWith(Enclosed.class)
, it builds a test suite consisting not just of tests from the annotated class, but of every nested inner class of the annotated class. A handy way, perhaps, of running tests that are nested classes of a test class that groups tests of related test subjects or functionality, in the eventuality that one's Ant script might not be set up to pick up nested classes.
What if the nested class weren't static
? Would there ever be a legitimate reason to set up a test class as a non-static
inner class of another test class? How could we run them? I recently found a legitimate reason, and built a way to run them.
In the forthcoming Infinitest Continuous Testing Toolkit, I put together some abstract test classes that allow a developer to ensure that methods which return collections that may be fields of the receiving class are unmodifiable. Here are their essentials:
public abstract class UnmodifiableCollectionTestSupport<T> {
protected Collection<T> items;
private T containedItem;
@Before public final void initializeHarness() {
this.items = newCollection();
this.containedItem = containedItem();
assertFalse("need a non-empty collection", items.isEmpty());
assertTrue("need an item that is in the collection", items.contains(containedItem));
}
@Test(expected = UnsupportedOperationException.class)
public final void shouldPreventAdd() {
items.add(newItem());
}
// more tests, via collection and iterator methods...
protected abstract Collection<T> newCollection();
protected abstract T newItem();
protected abstract T containedItem();
}
public abstract class UnmodifiableListTestSupport<T> extends UnmodifiableCollectionTestSupport<T> {
@Test(expected = UnsupportedOperationException.class)
public final void shouldPreventAddAtIndex() {
list().add(0, newItem());
}
// more tests, via list and list iterator methods...
@Override protected final Collection<T> newCollection() {
return newList();
}
protected abstract List<T> newList();
private List<T> list() {
return (List<T>) items;
}
}
public abstract class UnmodifiableMapTestSupport<K, V> {
private Map<K, V> entries;
private K containedKey;
private V containedValue;
@Before public final void initializeHarness() {
this.entries = newMap();
this.containedKey = containedKey();
this.containedValue = containedValue();
assertFalse("need a non-empty map", entries.isEmpty());
assertTrue("need a key that is in the map", entries.containsKey(containedKey));
assertTrue("need a value that is in the map", entries.containsValue(containedValue));
}
@Test(expected = UnsupportedOperationException.class)
public final void shouldPreventPut() {
entries.put(newKey(), null);
}
// more tests via map methods...
protected abstract Map<K, V> newMap();
protected abstract K newKey();
protected abstract K containedKey();
protected abstract V containedValue();
}
And a example of a concrete test:
public class UnmodifiableMapTest extends UnmodifiableMapTestSupport<String, String> {
@Override protected String containedKey() {
return "foo";
}
@Override protected String containedValue() {
return "value";
}
@Override protected String newKey() {
return "bar";
}
@Override protected Map<String, String> newMap() {
return unmodifiableMap(singletonMap("foo", "value"));
}
}
This is a good start. However, not just do we want to ensure a map's unmodifiability via its own methods, but also via the methods on its keySet(), values(), and entrySet() views. (Did you know that you can modify a map via these views? I didn't.) It would seem natural, then, to leverage the UnmodifiableCollectionTestSupport
class for this purpose, feeding different instances of it an keySet()
, values()
, and keySet()
view in turn. But I did not want a developer to have to remember to do this. I thought it would be nice if a subclasser of UnmodifiableMapTestSupport
would get all four test classes run without any additional effort.
I decided to give UnmodifiableMapTestSupport
three non-static inner test classes, like so:
public abstract class UnmodifiableMapTestSupport<K, V> {
private Map<K, V> entries;
private K containedKey;
private V containedValue;
@Before public final void initializeHarness() {
this.entries = newMap();
this.containedKey = containedKey();
this.containedValue = containedValue();
assertFalse("need a non-empty map", entries.isEmpty());
assertTrue("need a key that is in the map", entries.containsKey(containedKey));
assertTrue("need a value that is in the map", entries.containsValue(containedValue));
}
@Test(expected = UnsupportedOperationException.class)
public final void shouldPreventPut() {
entries.put(newKey(), null);
}
// more tests via map methods...
protected abstract Map<K, V> newMap();
protected abstract K newKey();
protected abstract K containedKey();
protected abstract V containedValue();
class KeySetTest extends UnmodifiableCollectionTestSupport<K> {
@Override protected K containedItem() {
return containedKey;
}
@Override protected Collection<K> newCollection() {
return entries.keySet();
}
@Override protected K newItem() {
return newKey();
}
}
class ValuesTest extends UnmodifiableCollectionTestSupport<V> {
@Override protected V containedItem() {
return containedValue;
}
@Override protected Collection<V> newCollection() {
return entries.values();
}
@Override protected V newItem() {
return null;
}
}
class EntrySetTest extends UnmodifiableCollectionTestSupport<Map.Entry<K, V>> {
@Override protected Entry<K, V> containedItem() {
return new Map.Entry<K, V>() {
public K getKey() {
return containedKey;
}
public V getValue() {
return containedValue;
}
public V setValue(V value) {
throw new UnsupportedOperationException();
}
@Override public boolean equals(Object that) {
if (!(that instanceof Map.Entry<?, ?>))
return false;
Map.Entry<?, ?> other = (Map.Entry<?, ?>) that;
return ObjectUtils.equals(getKey(), other.getKey())
&& ObjectUtils.equals(getValue(), other.getValue());
}
@Override public int hashCode() {
return ObjectUtils.hashCode(getKey()) ^ ObjectUtils.hashCode(getValue());
}
@Override public String toString() {
return getKey() + " -> " + getValue();
}
};
}
@Override protected Collection<Entry<K, V>> newCollection() {
return entries.entrySet();
}
@Override protected Entry<K, V> newItem() {
return null;
}
}
}
This gives the enclosed instances, however they might be created, visibility to the enclosing class's fixture.
Now, we have to figure out how to run all these tests. I created a custom JUnit runner, EnclosedInners
, and marked UnmodifiableMapTestSupport
as @RunWith(EnclosedInners.class)
. EnclosedInners
is a ParentRunner<Runner>
just as the Suite runner is. It builds up a runner for the concrete subclass of UnmodifiableMapTestSupport
as normal (modulo checking for @RunWith
), and also makes runners for each of the enclosed inner classes of the same (all the way up the inheritance hierarchy of the concrete class). If the inner class is static
, it builds a normal Runner
for that class. If the inner class is not static
, it builds a runner which, when it is time to run a test method:
- instantiates the enclosing class
- instantiates the enclosed class, passing its constructor the enclosing instance (inner classes get a synthetic constructor with a parameter representing the enclosing instance)
- runs any @Before methods for the enclosing instance. It is important to do this so that the enclosed instance will be able to access fully initializes fixture fields from the enclosing instance.
- runs any
@Before
methods for the enclosed instance - runs the test method
- runs any @After methods for the enclosed instance
- runs any
@After
methods for the enclosing instance
Worked like a charm!
Some gotchas I tripped over:
- Reflectively finding the inner class constructor based on the runtime type of the enclosing instance wasn't doable by mere Commons Beanutils checking (since the type of the constructor parm is that runtime type's superclass). I had to bring in my reflective method lookup mojo to be able to find a signature that was compatible with the actual parm.
- I purposely made the inner classes non-
public
-- so in order to get their constructors invoked reflectively, I had to use setAccessible(true). - Note that JUnit wants the declaring classes of @Before, @After, @Test, etc. methods to be
public
. No problem for our inner classes -- those methods actually live on their superclass,UnmodifiableCollectionTestSupport
, which ispublic
.
Creating a custom JUnit test runner was easier than I had anticipated, and it was a pleasure to work with the extension points it offered.