Diasparsoft Logo Let's write software that people understand.

Home | Contact

Training

JUnit support

Outsourcing

The work that Diasparsoft did for us was outstanding. We are now using the software on a daily basis and their work could well have a dramatic impact on the Reds' organization in the near future.Cincinnati Reds Baseball Club.


Publications

Tips & Tricks

Diasparsoft Toolkit

What is Diaspar?

Interesting Bits RSS

Home » Archives » October 2005 » All right... so it wasn't a FitNesse bug

[Previous entry: "The Irony of Urgency"] [Next entry: "Why I don't care about 100% code coverage"]

10/25/2005: "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.