Separating Data from Algorithm
Viewed more critically,
Removing Duplicate Code
didn't really eliminate all redundancy.
Notwithstanding the important observation that the data values vary,
the establishment of inputs and expected values follows
the same pattern from one test to the next.
Therefore, if we could extract the assignment of these
values into something common, the amount of code
in each test could be correspondingly reduced.
The first step down this path is the biggest leap:
we will remove the data values from each test
and store them in a separate data struture.
For each test, there exists a set of heterogenous
data items (inputs and expected values).
This suggests a map indexed by the names of the tests,
where the value of each entry is itself a map,
having a name describing the purpose of the item
that serves as the key to the value.
The skeleton of the new TestCase appears here,
showing the declaration and initialization
of the data structre.
package org.springunit.examples.constructor.junit.v3;
public class CompositeDateConstructorTest extends TestCase {
private Map<String, Map<String, Object>> data;
protected void setUp() throws Exception {
this.data = new HashMap<String, Map<String, Object>>();
// Population of data values follows here
}
}
The data field, a map of maps, will hold all of the data values for all of the tests.
It is instantiated and populated with values in the setUp method.
Recall that the setUp method is inherited from the JUnit framework.
It is called before executing the body of every test method in the class.
As we shall see, for any given test class, this data structure could become
arbitrarily large.
This has ramifications for performance that we will return to later.
A fuller excerpt of the setUp method follows.
protected void setUp() throws Exception {
this.data = new HashMap<String, Map<String, Object>>();
Map<String, Object> testData = new HashMap<String, Object>();
testData.put("day", 1);
testData.put("month", 1);
testData.put("year", 2006);
testData.put("expectedDay", 1);
testData.put("expectedMonth", 1);
testData.put("expectedYear", 2006);
this.data.put("testJan01", testData);
// more valid test data here
testData = new HashMap<String, Object>();
testData.put("day", 29);
testData.put("month", 2);
testData.put("year", 2004);
testData.put("expectedDay", 29);
testData.put("expectedMonth", 2);
testData.put("expectedYear", 2004);
this.data.put("testFeb29Leap2004", testData);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 4);
testData.put("year", 2006);
this.data.put("testApr31", testData);
// more invalid test data here
testData = new HashMap<String, Object>();
testData.put("day", 29);
testData.put("month", 2);
testData.put("year", 2003);
this.data.put("testFeb29NoLeap", testData);
}
Because the test values must now be retrieved from this data structure,
the test methods become considerably more verbose.
Here is an excerpt.
public void testJan01() throws Exception {
Integer day = (Integer)this.data.get("testJan01").get("day");
Integer month = (Integer)this.data.get("testJan01").get("month");
Integer year = (Integer)this.data.get("testJan01").get("year");
Integer expectedDay = (Integer)this.data.get("testJan01").get("expectedDay");
Integer expectedMonth = (Integer)this.data.get("testJan01").get("expectedMonth");
Integer expectedYear = (Integer)this.data.get("testJan01").get("expectedYear");
runValid(year, month, day, expectedYear, expectedMonth, expectedDay);
}
// More valid tests here
public void testFeb29Leap2004() throws Exception {
Integer day = (Integer)this.data.get("testFeb29Leap2004").get("day");
Integer month = (Integer)this.data.get("testFeb29Leap2004").get("month");
Integer year = (Integer)this.data.get("testFeb29Leap2004").get("year");
Integer expectedDay = (Integer)this.data.get("testFeb29Leap2004").get("expectedDay");
Integer expectedMonth = (Integer)this.data.get("testFeb29Leap2004").get("expectedMonth");
Integer expectedYear = (Integer)this.data.get("testFeb29Leap2004").get("expectedYear");
runValid(year, month, day, expectedYear, expectedMonth, expectedDay);
}
public void testApr31() throws Exception {
Integer day = (Integer)this.data.get("testApr31").get("day");
Integer month = (Integer)this.data.get("testApr31").get("month");
Integer year = (Integer)this.data.get("testApr31").get("year");
runInvalid(year, month, day);
}
// More invalid tests here
public void testFeb29NoLeap() throws Exception {
Integer day = (Integer)this.data.get("testFeb29NoLeap").get("day");
Integer month = (Integer)this.data.get("testFeb29NoLeap").get("month");
Integer year = (Integer)this.data.get("testFeb29NoLeap").get("year");
runInvalid(year, month, day);
}
We can reduce the clutter in two steps.
First, we'll
Eliminate the Test Name
and then we'll
Add Syntactic Sugar