Magic Java Bean Matchers

poetix edited this page Apr 20, 2012 · 5 revisions
Clone this wiki locally

Bean Matching Made Easier

Suppose you have a bean-like class with various public properties, something like this:

public class Bean {

    private final int eyeDiameter;
    private final Planet planetOfOrigin;

    public Bean(int eyeDiameter, Planet planetOfOrigin) {
        this.eyeDiameter = eyeDiameter;
        this.planetOfOrigin = planetOfOrigin;
    }

    public int getEyeDiameter() {
        return this.eyeDiameter;
    }

    public Planet getPlanetOfOrigin() {
        return planetOfOrigin;
    }
}

Someone gives you a bean, and you would like to validate using Hamcrest matchers that it is indeed a big-eyed bean from Venus:

assertThat(myBean, is(a_bean().with_eye_diameter(greaterThan(7)).from_the(Planet.VENUS));

The method a_bean() returns us an object with a fluent API for building up a matcher for the Bean class property-by-property. Here's the interface for this API:

public interface BeanMatcher extends Matcher<? super Bean> {
    BeanMatcher with_eye_diameter(Matcher<? super Integer> eyeDiameterMatcher);
    BeanMatcher from_the(Planet expectedPlanet);
}

Normally at this point you'd have to write an implementation of this interface that stashed away the eyeDiameterMatcher and expectedPlanet parameters and then used them to provide a description of the expected Bean, to check whether the supplied Bean matched that description, and to describe the mismatch in the event that it did not.

MagicJavaBeanMatchers provides some clever dynamic-proxying logic that means you don't have to do this. Instead, you write a_bean() as follows:

public BeanMatcher a_bean() {
    return MagicJavaBeanMatcher.matching(Bean.class).using(BeanMatcher.class);
}

MagicJavaBeanMatcher maps the method names in the interface to the names of properties of the Bean class, and converts calls to those methods into matchers for each of those properties. It understands (by convention) that with_eye_diameter addresses the property eyeDiameter, for which it should call getEyeDiameter() on the Bean. However, it can't guess that from_the addresses the property planetOfOrigin. So we have to give it a hint, in the form of an AddressesProperty annotation:

public interface BeanMatcher extends Matcher<? super Bean> {
    BeanMatcher with_eye_diameter(Matcher<? super Integer> eyeDiameterMatcher);
    @AddressesProperty("planetOfOrigin") BeanMatcher from_the(Planet expectedPlanet);
}

And that's (mostly) it!