Saturday, February 13, 2010

Additional features of Mockachino

Mockachino is making good progress!
Apart from generic refactorings, I've added a few nifty features:

Support for equals, hashCode and toString
Unlike EasyMock and Mockito, Mockachino now supports stubbing, verifying of all these methods. The other mocking frameworks probaby have good reasons for not supporting those. I at least know that Mockachino used to have good reasons. If you're not interested in the technical details, you can skip the next two paragraphs.

All mocks need some sort of data storage for their metadata. The metadata is basically just a memory of all invocations and a list of all stubbing definitions. This metadata needs to either be stored in the mock itself or use some sort of mapping strategy. I used to have a WeakHashMap that mapped the mocks to their metadata, and for that to work I required a fixed equals and hashCode.

My new approach is instead to let all my mocks implement a secondary interface which has a secret method for retrieving the metadata. The invocation handler for the mock simply listens for this method call and returns its reference to the metadata. It's really quite simple, and as with all simple ideas, you start to wonder why you didn't think of it before.

The problem with in order verification
One problem I had was in order verification of this sort:

@Test
public void test() {
List mock = mock(List.class);
mock.add(100);
mock.add(200);
mock.add(100);

OrderingContext ordering = newOrdering();
ordering.verifyAtMost(1).on(mock).add(100);
ordering.verifyAtLeast(1).on(mock).add(200);
}

By reading the entire ordering part, it seems that the test would pass, since there is only one call to add(100) before the call to add(200). But at the time of the first verification, it doesn't know when it should stop looking for more matches. This is easy to see when considering the next example:

@Test
public void test() {
List mock = mock(List.class);
mock.add(100);
mock.add(100);

OrderingContext ordering = newOrdering();
ordering.verifyAtMost(1).on(mock).add(100);
}

Here you obviously expect a failure since there were in fact two calls to add(100) when you expected at most one. For this reason, Mockachino doesn't support "at most"-matching for in order verifications.

So I started thinking about different approaches.
You could do something like this instead:

@Test
public void test() {
List mock = mock(List.class);
mock.add(100);
mock.add(200);
mock.add(100);

OrderingContext ordering = newOrdering();
ordering.verifyAtMost(1).on(mock).add(100);
ordering.verifyAtLeast(1).on(mock).add(200);
ordering.verifyOrdering();
}

Simply change the ordering concept to not verify anything at all until the call to verifyOrdering() is called. At this point you do have all the information you need.
The problem with this is that it adds a lot of complexity, both to the code base and to the tests. It's similar to solving a complex regular expression - it would need to backtrack on failures until it can be sure that it's impossible to satisfy the conditions.

It's simply not a clean solution. In practice such an ordering tests would be frail and be far to dependent on the internal state.

Adding "points in time" and time range verification.
Another way of supporting these kind of use cases is to add two new very simple constructs, that should rarely be needed, but be very useful and powerful in the few cases that you do.

The first construct is the MockPoint. This is simply a point in time that you can get in two different ways:

// 1
MockPoint point = Mockachino.getCurrentPoint();

// 2
OrderingContext order = Mockachino.newOrdering();
order.verifyOnce().on(mock).add();
MockPoint point = order.afterLastCall();

With these points, you can take advantage of the second construct: between, after and before
Simply add between(point1, point2) before your verification line to limit your matches to calls done between those points in time.

So instead of checking that something happened at most a certain number of times in order, grab the time points and verify it normally with between instead:


@Test
public void test() {
List mock = mock(List.class);
mock.add(100);
mock.add(200);
mock.add(100);

// Get the points
OrderingContext ordering = newOrdering();
MockPoint p1 = ordering.atLastCall();
ordering.verifyAtLeast(1).on(mock).add(200);
MockPoint p2 = ordering.beforeLastCall();

between(p1, p2).verifyAtMost(1).on(mock).add(100);
}

It's not as pretty as in the first example, but it works and is conceptually simple.
I don't recommend overusing this feature since it's probably in most cases a warning sign that your code is frail and makes too many assumptions.

1 comment:

Unknown said...

Great stuff. Keep up the good work!

Post a Comment