Unifying the Test Algorithm
Despite the presence of some common inputs ("year", "month" and "day"),
it is not obvious that runValid and runInvalid can be merged.
The key insight is to recognize that InvalidDateException,
thrown by runInvalid, can be treated as an expected value,
just like other data elements.
So it is possible to store an instance of this exception
in the data map and then check for its presence similar to
the way in which the tests check for other values.
We use the presence or absence of this element
as the indicator whether or not to expect the exception.
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);
testData.put("expectedException", null); // or simply omit
this.data.put("testJan01", testData);
// More data here
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 4);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testApr31", testData);
}
The method runConstructor unifies runValid and runInvalid.
It calls getObject to retrieve all inputs and expected values
from the data map, including expectedException.
If expectedException is not null, then the test
immediately fails if an exception is not thrown
by the constructor.
Otherwise, the test proceeds to compare the
object fields with expected results.
If any exceptions are thrown by the constructor,
then the exception handler checks to see
if the thrown exception is a descendent from
the expected exception.
If it is not, then it is rethrown; otherwise,
it is simply discarded, since it indicates
successful execution of the test.
protected void runConstructor() throws Exception {
Integer day = (Integer)getObject("day");
Integer month = (Integer)getObject("month");
Integer year = (Integer)getObject("year");
Integer expectedDay = (Integer)getObject("expectedDay");
Integer expectedMonth = (Integer)getObject("expectedMonth");
Integer expectedYear = (Integer)getObject("expectedYear");
Exception expectedException = (Exception)getObject("expectedException");
try {
CompositeDate subject = new CompositeDate(year, month, day);
if (expectedException != null) {
fail("Exception not thrown");
}
assertTrue(expectedMonth == subject.getMonth());
assertTrue(expectedDay == subject.getDay());
assertTrue(expectedYear == subject.getYear());
}
catch (Exception ex) {
if (!expectedException.getClass().isAssignableFrom(ex.getClass())) {
throw ex;
}
}
}
After several refactoring steps, we have arrived at a version of the test
that has removed all of the redundancies present in the original brute force version.
All of the tests have been reduced to one line methods, which curiously,
are differentiated only by their names, since they all call the same method.
Of course, this simplicity was purchased at the price of a complicated data structure,
the data map.
There are two lingering issues related to this data.
First, the population of the map in code is cumbersome, though not difficult.
Second, and more importantly, because of the way the JUnit framework is designed,
this data structure is rebuilt for every test method in the class.
It would be preferable if it could be built once and then retained
across each test invocation.
To do that, we will need to marry two frameworks, JUnit and Spring,
to produce SpringUnit.
Before we do, we'll show the complete source code for this last variation
of a pure JUnit test.
public class CompositeDateConstructorTest extends TestCase {
private Map<String, Map<String, Object>> data;
protected Object getObject(String name) {
return this.data.get(getName()).get(name);
}
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);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 12);
testData.put("year", 2006);
testData.put("expectedDay", 31);
testData.put("expectedMonth", 12);
testData.put("expectedYear", 2006);
this.data.put("testDec31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 29);
testData.put("month", 2);
testData.put("year", 2000);
testData.put("expectedDay", 29);
testData.put("expectedMonth", 2);
testData.put("expectedYear", 2000);
this.data.put("testFeb29Leap2000", testData);
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);
testData.put("expectedException", new InvalidDateException());
this.data.put("testApr31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 6);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testJun31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 9);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testSep31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 11);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testNov31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 31);
testData.put("month", 2);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testFeb31", testData);
testData = new HashMap<String, Object>();
testData.put("day", 30);
testData.put("month", 2);
testData.put("year", 2006);
testData.put("expectedException", new InvalidDateException());
this.data.put("testFeb30", testData);
testData = new HashMap<String, Object>();
testData.put("day", 29);
testData.put("month", 2);
testData.put("year", 2003);
testData.put("expectedException", new InvalidDateException());
this.data.put("testFeb29NoLeap", testData);
testData = new HashMap<String, Object>();
testData.put("day", 29);
testData.put("month", 2);
testData.put("year", 1900);
testData.put("expectedException", new InvalidDateException());
this.data.put("testFeb29NoLeap1900", testData);
}
protected void runConstructor() throws Exception {
Integer day = (Integer)getObject("day");
Integer month = (Integer)getObject("month");
Integer year = (Integer)getObject("year");
Integer expectedDay = (Integer)getObject("expectedDay");
Integer expectedMonth = (Integer)getObject("expectedMonth");
Integer expectedYear = (Integer)getObject("expectedYear");
Exception expectedException = (Exception)getObject("expectedException");
try {
CompositeDate subject = new CompositeDate(year, month, day);
if (expectedException != null) {
fail("Exception not thrown");
}
assertTrue(expectedMonth == subject.getMonth());
assertTrue(expectedDay == subject.getDay());
assertTrue(expectedYear == subject.getYear());
}
catch (Exception ex) {
if (!expectedException.getClass().isAssignableFrom(ex.getClass())) {
throw ex;
}
}
}
public void testJan01() throws Exception {
runConstructor();
}
public void testDec31() throws Exception {
runConstructor();
}
public void testFeb29Leap2000() throws Exception {
runConstructor();
}
public void testFeb29Leap2004() throws Exception {
runConstructor();
}
public void testApr31() throws Exception {
runConstructor();
}
public void testJun31() throws Exception {
runConstructor();
}
public void testSep31() throws Exception {
runConstructor();
}
public void testNov31() throws Exception {
runConstructor();
}
public void testFeb31() throws Exception {
runConstructor();
}
public void testFeb30() throws Exception {
runConstructor();
}
public void testFeb29NoLeap() throws Exception {
runConstructor();
}
public void testFeb29NoLeap1900() throws Exception {
runConstructor();
}
}