All right... so it wasn't a FitNesse bug
Today I was writing a FitNesse test and ran into a problem I couldn't explain. This is a simplified version of the table I wrote.
| com.mycompany.MyRowFixture | ||
| id | date | name? |
| 762 | never | Doesn't so much matter |
I wrapped java.util.Date in a new class FitDate that handles my keyword "never". (Here, "never" corresponds to a date far in the future, which wasn't my design choice, but it is what it is.) I implemented parse, equals and toString, ran the test, and although the system was sending back the right object (with key 762 and never), the row didn't match. I had no idea what the problem was: I had implemented custom data types in FitNesse dozens of times.
Perhaps there's a defect in FitNesse related to how RowFixture handles rows whose key include custom data types. Naturally, I had to write some tests.
First, I verified what I knew about how RowFixture works.
public void testSimplestKey() throws Exception {
RowFixture fixture = new RowFixture() {
public Object[] query() throws Exception {
return new Object[] { new PrimeData(2) };
}
public Class getTargetClass() {
return PrimeData.class;
}
};
Parse table = new Parse("<table>"
+ "<tr><td>The Name Doesn't Matter</td></tr>"
+ "<tr><td>prime</td></tr>" + "<tr><td>2</td></tr>"
+ "</table>");
fixture.doTable(table);
assertTrue("Surplus: " + fixture.surplus.toString(), fixture.surplus
.isEmpty());
assertTrue("Missing: " + fixture.missing.toString(), fixture.missing
.isEmpty());
assertEquals(new Counts(1, 0, 0, 0), fixture.counts);
}
Since that worked, I tried the same thing with a target class whose key is a custom data type.
public void testOnlySingleColumnKey() throws Exception {
RowFixture fixture = new RowFixture() {
public Object[] query() throws Exception {
return new Object[] { SingleColumnKey.with(IntWrapper
.valued(762)) };
}
public Class getTargetClass() {
return SingleColumnKey.class;
}
};
Parse table = new Parse("<table>"
+ "<tr><td>The Name Doesn't Matter</td></tr>"
+ "<tr><td>key</td></tr>" + "<tr><td>762</td></tr>"
+ "</table>");
fixture.doTable(table);
assertTrue(fixture.surplus.toString(), fixture.surplus.isEmpty());
assertTrue(fixture.missing.toString(), fixture.missing.isEmpty());
assertEquals(new Counts(1, 0, 0, 0), fixture.counts);
}
static class SingleColumnKey {
public IntWrapper key;
public static SingleColumnKey with(IntWrapper intWrapper) {
SingleColumnKey singleColumnKey = new SingleColumnKey();
singleColumnKey.key = intWrapper;
return singleColumnKey;
}
public boolean equals(Object other) {
if (other instanceof SingleColumnKey) {
SingleColumnKey that = (SingleColumnKey) other;
return this.key.equals(that.key);
}
else {
return false;
}
}
public String toString() {
return "SingleColumnKey with " + key;
}
}
static class IntWrapper {
public int value;
public static IntWrapper valued(int value) {
IntWrapper result = new IntWrapper();
result.value = value;
return result;
}
public static IntWrapper parse(String text) {
return IntWrapper.valued(Integer.parseInt(text));
}
public boolean equals(Object other) {
if (other instanceof IntWrapper) {
IntWrapper that = (IntWrapper) other;
return this.value == that.value;
}
else {
return false;
}
}
public String toString() {
return String.valueOf(value);
}
}
This test failed! I couldn't figure it out. I'm ashamed to admit that I submitted to stepping through RowFixture with the debugger. (Hold your cards and letters; I feel bad enough.) I found out that when RowFixture computed the keys in common between the expected results (in the table) and the actual results (returned by query()), it was treating the two keys as different, even though they are equal according to equals().
Then it hit me. The defect wasn't in RowFixture... I didn't implement hashCode(). That was likely the problem. I wrote this test to verify that.
public void testDifferenceBetweenCustomTypeWithAndWithoutHashCode()
throws Exception {
RowFixture fixture = new RowFixture() {
public Object[] query() throws Exception {
return null;
}
public Class getTargetClass() {
return null;
}
};
class TargetClass {
public boolean equals(Object other) {
return true;
}
}
assertEquals(2, fixture.union(Collections.singleton(new TargetClass()),
Collections.singleton(new TargetClass())).size());
class TargetClassWithHashCode {
public boolean equals(Object other) {
return true;
}
public int hashCode() {
return 0;
}
}
assertEquals(1, fixture.union(
Collections.singleton(new TargetClassWithHashCode()),
Collections.singleton(new TargetClassWithHashCode())).size());
}
When I implement hashCode(), equal keys are treated equally, as I'd expect. When I corrected my second test by implementing IntWrapper.hashCode() correctly, that test passed.
The lesson: when implementing a custom data type in FitNesse, take time to implement hashCode(), even if it's as stupid as return 0;. You'll spend a few seconds per custom data type to avoid re-learning this lesson however often you might need to re-learn it. I'm sure I'll forget again some time.
