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

Tuesday, October 25th

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.

jbrains on 10.25.05 @ 11:04 PM ET [link]

Monday, October 24th

The Irony of Urgency

On the extremeprogramming Yahoo! group there has recently been an interesting discussion about urgency. Specifically, what does it mean to work "with a sense of urgency"? IBM touts this as a key trait of a True Blue IBMer: working with a sense of urgency. Is it really desirable? Is there something other than urgency that managers truly want that they don't consciously realize? In reviewing recent conversations with developers in their natural habitat, I have found a delicious irony about urgency.

The more urgency demanded of developers, the more time they spend complaining about the situation. In other words, the very act of demanding developers work with a sense of urgency often leads them to spend less time working (effectively or otherwise) on the project, because they need more time to commiserate with one another about how difficult work is with this overhanging demand for urgency.

Kent Beck has written that he likes to program as though he has all the time in the world. I have to admit that I have done my best work when I have been able to do that. When I spend the time to do it well (not "get it right", since that might take forever), I go faster. You can tell when someone is suffering from the myth of urgency, because he emphasizes doing it over doing it well.

jbrains on 10.24.05 @ 07:42 PM ET [link]