GitHub

Suppose you wanted to unit test java.util.Map.
How would you verify each implementation?
Take a quick look at the Map javadoc, and you'll find 20 implementing classes (https://docs.oracle.com/javase/7/docs/api/java/util/Map.html).
Each Map implementation has common properties that you want to avoid copying.
In this post I illustrate two techniques you can use for testing common requirements over implementations by trying to implement and test a simplified version of java.util.Map.
I encountered a similar problem when taking the Coursera parallel programming course.
The course's assignments required sequential and parallel implementations.
I noticed I ended up copying and pasting my tests of the sequential implementation to the parallel one.
I came up with these solutions to de-duplicate the code.
= Introduction to Example =
In this example we implement a simplified Map interface with three methods, get, put, and getKeys:
public interface Map<K,V>  {
	void put(K key, V value);
	V get(K key);
	List<K> getKeys();
}
We provide two implementations of the interface.
InefficientMap uses two ArrayLists to store the keys and values.
SortedMap which guarantees that getKeys() returns the keys in sorted order.
The implementations are purposely naive because the focus of this article is the testing aspect, not performance.
= Method 1: Abstract Class =
Abstract classes can be used to test the common requirements of differing implementations.
This method provides the least code duplicated code.
You have all the common test cases in the base class which make use of a factory method for generating the implementation.
To apply the inheritance you follow these steps:
1. Create an abstract class for the common unit tests
2. Add an abstract method that expects the extender acts as the factory for the real implementation
3. Add @Tests the verify the requirements of the interface
4. Create a unit test class for each implementation and extend from the abstract unit test class
5. Provide the real implementation expected by the abstract method
6. Add any additional @Tests that test requirements specific to the implementation
== Step 1 and 2: Creating the Abstract Class ==
You create an abstract class with an abstract factory method for returning the real implementation.
public abstract class InheritingMapTest {
	protect abstract Map<K,V> makeMap();
}
== Step 3: Add @Tests ==
In the abstract unit test, we include requirements that are common to all maps.
The tests make use of the factory method for getting an instance of the implementation.
Here we look at only one of the many test cases for the Map interface.
The factory provides us with a mechanism for obtaining a new instance of the implementation.
Getting the value using a key that was replaced is returns the most recent value.
	@Test
	public void testValueReplaced() {
		Map<Integer,Integer> map = makeMap();
		map.put(0, 10);
		map.put(0, 12);
		assertEquals(Integer.valueOf(12), map.get(0));
	}
== Step 4 and 5: Create a Unit Test Class for each Implementation ==
Now for each implementation we extend from the abstract class and implement the factory method.
public class InheritingSortedMapTest extends InheritingMapTest {
	protected Map<Integer,Integer> makeMap() {
		return new SortedMap<>(IntComparator.intComparator);
	}
}
== Step 6: Add Additional Tests of Uncommon Requirements to Subclass ==
The sorted map comes with the additional requirement that the getKeys() method returns the keys in sorted order.
We add tests of this new requirement into the subclass.
	@Test
	public void testKeysSorted() {
		SortedMap<Integer,Integer> map = new SortedMap<>();
		map.put(2,12);
		map.put(1,11);
		map.put(3,13);
		List<Integer> keys = map.getKeys();
		for(int i = 0 ; i<3; i++) {
			assertEquals(Integer.valueOf(i+1),keys.get(i));
		}
	}
= Method 2: Composition  =
An alternative to this pattern is to include all the tests in a help class and have each unit test call each of the help methods.
The problem with this solution is that you end up having a method which calls all your test methods which duplicates some code.
More importantly, it makes it more difficult to figure out which test failed because you have to inspect the stack trace instead of click on the failed test in an IDE like eclipse.
1. Create a factory interface
2. Create class containing that accepts the factory in the constructor
3. Add tests the verify the requirements of the interface
4. Call each test method from a public 'testAll()' method
5. Create a unit test class for each implementation that creates and instance of the test class
6. Provide an implementation of the factory to the test class
7. Add any additional @Tests that test requirements specific to the implementation
== Step 1 and 2: Create the Factory and Test Class ==
The 'Testing Class' has a Factory for creating new instances of the implementation when it needs it.
public class CompositionMapTest {
	public interface MapFactory {
		/**
		 * Return a new instance of your implementation of Map<>
		 */
		Map<Integer,Integer> make() ;
	}
	private MapFactory mapFactory;
	public CompositionMapTest(MapFactory mapFactory) {
		this.mapFactory = mapFactory;
	}
}
== Step 3 and 4: Add Tests ==
We make use of the factory to create an instance of the implementation to verify the object matches the specification.
	private void testValueReplaced() {
		Map<Integer,Integer> map = mapFactory.make();
		map.put(0, 10);
		map.put(0, 12);
		assertEquals(Integer.valueOf(12), map.get(0));
	}
The 'Testing Class' invokes all the test methods in a "testAll()" method which the real test classes will call.
	public void testAll() {
		...
		testValueReplaced();
		...
	}
== Step 5 and 6: Create a class for each implementation ==
To reuse the existing tests of the 'Test Class' we create an instance of it, providing it with a Factory for creating implementations.
We invoke the test class's "testAll()" method to check our implementation.
public class CompositionSortedMapTest {
	@Test
	public void testBasicRequirements() {
		CompositionMapTest basicTests = new CompositionMapTest(
			new CompositionMapTest.MapFactory() {
				public Map<Integer,Integer> make() {
					return new SortedMap<>(IntComparator.intComparator);
				}
			});
		basicTests.testAll();
	}
}
== Step 7: Add any additional tests for uncommon requirements ==
We write tests as we would normally for requirements that are not common to all Maps.
	@Test
	public void testKeysSorted() {
		SortedMap<Integer,Integer> map = new SortedMap<>();
		map.put(2,12);
		map.put(1,11);
		map.put(3,13);
		List<Integer> keys = map.getKeys();
		for(int i = 0 ; i<3; i++) {
			assertEquals(Integer.valueOf(i+1),keys.get(i));
		}
	}
= Conclusion =
If you notice you're copying and pasting unit test code for common requirements, consider one of these two techniques.
However, these techniques are only guides because you will encounter cases that do not fit cleanly.
As an exercise for the reader: how would you extend these techniques to apply to java.util.WeakHashMap?

Read the original on github.com ↗