Tuesday, 18 August 2015

Testing beans using Hamcrest's hasProperty

Like most devs, I write tests.  They generally start small but quickly become complex.  So, as a general rule over the last 5 or so years, I break tests into a single assertion per test - where possible.

I find this makes it neater to read and more concise to write.

However, it can also create large numbers of tests - particularly when testing transformers or bean properties.  I recently came across a neat way of testing for property values using Hamcrest's hasProperty.

Introducing hasProperty()

    @Test
    public void hasProperties() {
        final Price price = new Price()
                .symbol("AUDUSD")
                .bid(0.9865)
                .ask(0.9875);

        assertThat(price, allOf(
                notNullValue(),
                hasProperty("bid",    closeTo(0.9865, 0.00001)),
                hasProperty("ask",    closeTo(0.9875, 0.00001)),
                hasProperty("spread", closeTo(0.001, 0.0001)),
                hasProperty("symbol", equalTo("AUDUSD"))
        ));
    }

It fits all of my requirements:

 - concise
 - single test (even the null check)
 - easy to read

Fluent style Price

The only catch i that it would require Price to be a bean with getters and setters, which I've tended to lean away from lately.  Instead preferring a more fluent style of methods:
public class Price {

    private double bid = Double.NaN;

    private double ask = Double.NaN;

    private double spread;

    private String symbol;

    public Price bid(double bid) {
        this.bid = bid;
        calcSpread();
        return this;
    }

    public double bid() {
        return bid;
    }

    public Price ask(double ask) {
        this.ask = ask;
        calcSpread();
        return this;
    }

    public double ask() {
        return ask;
    }

    public double spread() {
        return spread;
    }

    public Price symbol(String symbol) {
        this.symbol = symbol;
        return this;
    }

    public String symbol() {
        return symbol;
    }

    private void calcSpread() {
        if (!Double.isNaN(bid) && !Double.isNaN(ask)) {
            spread = Math.abs(bid - ask);
        }
    }
}


The Price class above invalidates the usage of hasProperty() as it no longer has bean getters and setters.


Tying together with BeanInfo

What we're able to do though, is provide an implementation of a BeanInfo class in our test package:


public class PriceBeanInfo extends SimpleBeanInfo {

    private static final Logger log = LoggerFactory.getLogger(PriceBeanInfo.class);

    private final Class beanClass = Price.class;

    private PropertyDescriptor[] propertyDescriptors;

    public PropertyDescriptor[] getPropertyDescriptors() {
        if (propertyDescriptors == null) {
            collectPropertyDescriptors();
        }
        return propertyDescriptors;
    }

    private void collectPropertyDescriptors() {
        java.util.List fields = new ArrayList<>();
        fields.addAll(asList(beanClass.getDeclaredFields()));
        Class parent = beanClass.getSuperclass();
        while (parent != null) {
            fields.addAll(asList(parent.getDeclaredFields()));
            parent = parent.getSuperclass();
        }

        final java.util.List propertyDescriptors =

                fields.stream().filter(field -> !Modifier.isStatic(field.getModifiers()))
                        .map(p -> {
                            final String propertyName = p.getName();
                            final Method readMethod = findReadMethod(p);
                            final Method writeMethod = findWriteMethod(p);

                            try {
                                return new PropertyDescriptor(
                                        propertyName,
                                        readMethod,
                                        writeMethod
                                );

                            } catch (IntrospectionException e) {
                                log.warn("Failed to create property descriptor for: " + propertyName, e);
                                return null;
                            }
                        }).collect(Collectors.toList());
        this.propertyDescriptors = propertyDescriptors.toArray(new PropertyDescriptor[propertyDescriptors.size()]);
    }

}


The general premise of the BeanInfo implementation is to provide a set of PropertyDescriptor's that match the fluent style methods we created in the Price class.

The full source is available on my github.
  
So, there you have it - my find of the week that I thought I'd share.

Further thoughts

This code could easily be abstracted into a base class for easy reuse.  I would think you could also easily dynamically add getters and setters for classes that didn't have them.  This might allow for reuse of hasProperty() rather than using Spring's getField().

No comments:

Post a Comment