Sunday, May 29, 2011

EasyMock Issue #2: Bad Argument equals()

One of the two canonical EasyMock failure messages (along with "Expectation failure on verify" is "Unexpected method call". This failure happens between replay() and verify() whenever EasyMock sees the class-under-test making a method call on a mock object for which the test did not set up an expectation. Generally, there are two possible problems to immediately look for:
  1. A deficiency in the test, i.e. the behavior of the class-under-test is not correctly modeled during the expectation-setting phase of the test case.
  2. A bug in the class-under-test, i.e. the test is expecting the right thing but the code isn't doing it.

(Incidentally, when it turns out to be #1, programmers tend to complain about how difficult it is to write and maintain mock-object tests, and when it is instead #2, they often fail to give mock-object-testing enough credit for detecting their bug.)

But there is a third possibility, which I would argue is neither a bug in the test nor in the class-under-test, but rather a bug in a completely separate, possibly innocuous-looking class that they both use. As usual, let's look at an example.

Here is the class-under-test, CustomerController for this post (slightly modified from the one I used in the previous two posts:
public class CustomerController {
  private OrderService orderService;

  public CustomerController(OrderService orderService) {
    this.orderService = orderService;
  }
  
  public void postOrders(Order...orders) {
    orderService.postOrders(Arrays.asList(orders));
  }
}
And the test for the postOrders() method, which uses a mock OrderService:
public class TestCustomerController {
  @Test
  public void testPostOrders() {
    OrderService mockOrderService = EasyMock.createMock(OrderService.class);
    CustomerController controller = new CustomerController(mockOrderService);
    List<Order> expectedOrders = new ArrayList<Order>();
    expectedOrders.add(new Order(1));
    
    EasyMock.expect(mockOrderService.postOrders(expectedOrders)).andReturn(1);
    EasyMock.replay(mockOrderService);
    controller.postOrders(new Order(1));
  }
}
It is also important to note that we are posting Order instances:
public class Order {
  private int orderId;

  public Order(int orderId) {
    this.orderId = orderId;
  }
}
When you run this, alas, you get an error:

java.lang.AssertionError:
Unexpected method call postOrders([Order@1e4cbc4]):
postOrders([Order@b2fd8f]): expected: 1, actual: 0
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:45)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:73)
at org.easymock.internal.ClassProxyFactory$MockMethodInterceptor.intercept(ClassProxyFactory.java:92)
at OrderService$$EnhancerByCGLIB$$6d00fba4.postOrders()
at CustomerController.postOrders(CustomerController.java:11)
at TestCustomerController.testPostOrders(TestCustomerController.java:19)

The CustomerController code is quite trivial and correct in this case. The test is less trivial but also, well, at least ought to work. So what is going on here?

When EasyMock compares a mock method call to the expectation, it checks equality on each argument. In this case, that fails because the Order instance in the expectation does not equal the Order instance in the invocation. Note that Order is not overriding equals(), and since I set the test up to use different instances, the default instance-equality check fails. This example is pretty contrived. In reality, the more common case I've seen is that there actually is an equals() override, but somebody changes the class and breaks it, in which case the test is finding a real bug (albeit not in the actual class-under-test!).

There are two ways to fix this problem. First, I could override equals() (and hashCode(), of course) if it doesn't already exist on the class in question (the case with Order) or fix equals() if it were already there. That solution is probably more robust. The quick-and-dirty fix is to make the expectation and the invocation use the exact same instance(s). I will actually show the code for that:
public class TestCustomerController {
  @Test
  public void testPostOrders() {
    OrderService mockOrderService = EasyMock.createMock(OrderService.class);
    CustomerController controller = new CustomerController(mockOrderService);
    List<Order> expectedOrders = new ArrayList<Order>();
    
    // I will use this here AND below...
    Order order = new Order(1);
    expectedOrders.add(order);
    
    EasyMock.expect(mockOrderService.postOrders(expectedOrders)).andReturn(1);
    EasyMock.replay(mockOrderService);
    controller.postOrders(order);
  }
}
Sorry if I've rambled on for too long about something that is actually quite obvious. But I have made and seen others make this type of error enough times that I can hopefully save someone some time. Next time, I will discuss an EasyMock limitation that might impact your interface design.