'Is there a way to do deep comparison on a nested property with Hamcrest

I use hamcrest for most of my testing ,but have encountered a issue with it not being able to test a property one level down in the object graph .A snipped of my test case is below

final List<Foo> foos= fooRepository.findAll(spec);
      assertThat(results, is(notNullValue()));
      assertThat(results, hasItem(hasProperty("id.fooID1", equalTo("FOOID1"))));

so here I want to check if in the list of foos I have a property id.fooID1 equla to FOOID1 .Here I am going one level down to check my nested property .This doesnt currently work in hamcrest and I get the following error.

java.lang.AssertionError: 
Expected: a collection containing hasProperty("id.fooID1", "FOOID1")
     but: No property "id.fooID1"
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
    at org.junit.Assert.assertThat(Assert.java:956)
    at org.junit.Assert.assertThat(Assert.java:923)

any help or workaround on this issue .



Solution 1:[1]

You can nest hasProperty calls:

assertThat(results, hasItem(hasProperty("id", hasProperty("fooID1", equalTo("FOOID1")))));

For deeper nestings this might be a bit unwieldy.

Solution 2:[2]

I've achieved the result you expected with this simple utility method:

private static <T> Matcher<T> hasGraph(String graphPath, Matcher<T> matcher) {

    List<String> properties = Arrays.asList(graphPath.split("\\."));
    ListIterator<String> iterator =
        properties.listIterator(properties.size());

    Matcher<T> ret = matcher;
    while (iterator.hasPrevious()) {
        ret = hasProperty(iterator.previous(), ret);
    }
    return ret;
}

which I am able to use in asserts like this:

 assertThat(bean, hasGraph("beanProperty.subProperty.subSubProperty", notNullValue()));

check if this is of any help

Solution 3:[3]

I did not find a API solution to your problem, but found on source of 1.3 hamcrest that the HasPropertyWithValue matcher really does not dive into nested properties.

I've made a lousy solution (please observe that the messages when not found are not working properly):

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.beans.PropertyUtil;

public class NestedPropertyMatcher<T> extends TypeSafeDiagnosingMatcher<T>{

    private final String[] props;
    private final String path;
    private final Matcher<?> valueMatcher;

    @Override
    public boolean matchesSafely(T bean, Description mismatch) {
        if (props.length == 1) {
            return org.hamcrest.beans.HasPropertyWithValue.hasProperty(props[props.length - 1], valueMatcher).matches(bean);
        } else {
            Object aux = bean;
            for (int i = 0; i < props.length - 1; i++) {
                if (!org.hamcrest.beans.HasProperty.hasProperty(props[i]).matches(aux)) {
                    return false;
                } else {
                    PropertyDescriptor pd = PropertyUtil.getPropertyDescriptor(props[i], aux);
                    try {
                        aux = pd.getReadMethod().invoke(aux);
                    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                        mismatch.appendText("Exception while trying to access property value: " + e.getLocalizedMessage());
                        return false;
                    }
                }
            }
            return org.hamcrest.beans.HasPropertyWithValue.hasProperty(props[props.length - 1], valueMatcher).matches(aux);
        }
    }

    private NestedPropertyMatcher(String path, String[] propertiesTokens, Matcher<?> valueMatcher) {
        this.path = path;
        this.props = propertiesTokens;
        this.valueMatcher = valueMatcher;
    }

    public static <T> Matcher<T> hasPathProperty(String propertyPath, Matcher<?> valueMatcher) {
        String[] props = propertyPath.split("\\.");
        return new NestedPropertyMatcher<T>(propertyPath, props, valueMatcher);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("hasProperty(").appendValue(path).appendText(", ").appendDescriptionOf(valueMatcher).appendText(") did not found property");
    }
}

Pretty sure that the hamcrest folks will make a better job than mine, but I think this code will be enough for you.

Solution 4:[4]

You can try to use Hamcrest BeanMatcher APT generator. It can generate Matcher for your Foo bean (and all nested beans), so you will be able to use the following construction

assertThat(results, hasItem(fooMatcher()
     .withId(idBeanMatcher()
       .withFooID1("FOOID1"))));

For this case it slightly looks like overkill but if you need to check more complicated cases it may significantly simplify the code.

Also, it automatically regenerates the BeanMatchers on each update, so you will see a compilation error in your test if you change the name of some property (e.g. FooID1 to FooId1) that is much easy to detect and fix than some hardcoded string.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 eee
Solution 2 Cristiano Costantini
Solution 3
Solution 4 foal