jMock: Matching beans property-for-property

One of the differences between jMock mocks and traditional stub-based mocks is that jMock won’t do things like hold state for you in the mock. Where before we might have created something along the lines of

public class MockBroadcastServiceImpl implements BroadcastService {
  private Broadcast posted;

  public void post(String text, Date expiry, User postedBy) {
    this.posted = new Broadcast(text, expiry, postedBy);
  }

  Broadcast getPostedBroadcast { // not in interface
    return posted;
  }
}

public class OurPageTest extends PageTestCase {
  public void testSubmit() {
    invokeThingThatCallsOurMockService(new MockBroadcastServiceImpl());
    assertBroadcastCreated(TEXT, EXPIRY_DATE);
  }

  private void assertBroadcastCreated(String text, Date date) {
    MockAdministrationServiceImpl service = getMockService();

    Broadcast broadcast = service.getBroadcast();
    assertNotNull("broadcast", broadcast);
    assertEquals("text", text, broadcast.getText());
    assertEquals("expiry", date,  broadcast.getExpiry());
  }
}

it’s generally necessary to write a custom constraint such as

public class IsSameBroadcast implements Constraint {
  private final String text;
  private final Date expiryDate;

  public IsSameBroadcast(String text, Date expiryDate) {
    this.text = text;
    this.expiryDate = expiryDate;
  }

  public boolean eval(Object arg0) {
    Broadcast broadcast = (Broadcast) arg0;
    return broadcast.getText().equals(text)
        && broadcast.getExpiry().equals(expiryDate);
  }
}

public void testSubmit() throws Exception {
  Mock mock = mock(BroadcastService.class);
  mock.expects(once())
    .method("saveBroadcast")
    .with(new IsSameBroadcast(TEXT, EXPIRY_DATE));
  ...
}

if you want to verify anything about the content of the broadcast that is passed as a parameter. While jMock does implement eq() and same(), we may not have access to (for example) an object created in some intermediate layer.

Writing a custom constraint for every test like that works, but it’s a bit cumbersome. Our final solution was to write a general purpose reflective constraint that matches non-null properties in a supplied example:

public class Matching implements Constraint {
  private final Object bean;

  public Matching(Object bean) {
    this.bean = bean;
  }

  public boolean eval(Object candidate) {
    boolean matches = true;

    PropertyDescriptor[] properties =
      PropertyUtils.getPropertyDescriptors(bean);
    for (PropertyDescriptor property : properties) {
      Method getter = property.getReadMethod();
      try {
        Object expectedValue = getter.invoke(bean, new Object[]{});
        Object providedValue = getter.invoke(bean, new Object[]{});
        if (expectedValue != null
            && !expectedValue.equals(providedValue)) {
          matches = false;
          break;
        }
      } catch (Exception e) {
        throw new IllegalArgumentException(e);
      }
    }

    return matches;
  }
}

That lets us write tests like this:

public void testSubmit() throws Exception {
  Broadcast broadcast = new Broadcast(TEXT, EXPIRY_DATE, null);
  Mock mock = mock(BroadcastService.class);
  mock.expects(once())
      .method("saveBroadcast")
      .with(matching(broadcast));

  BroadcastService service = (BroadcastService) mock.proxy();
  invokeThingThatCallsOurMockService(service);
}

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply