Is there a way to do deep comparison on a nested property with Hamcrest
Asked Answered
P

5

14

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 .

Presuppose answered 1/4, 2016 at 18:26 Comment(0)
O
31

You can nest hasProperty calls:

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

For deeper nestings this might be a bit unwieldy.

Oregano answered 2/4, 2016 at 9:11 Comment(2)
Thanks that works like a charm but true unwieldy when it comes to a deeper hierarchy ..Presuppose
This is great, just what I needed, thanks for sharing.Sniperscope
W
12

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

Weariful answered 12/5, 2016 at 10:14 Comment(0)
E
2

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.

Evasion answered 1/4, 2016 at 19:50 Comment(1)
Changed the matcher function name to avoid collisions.Peyton
I
1

I am just 7 years too late here. But thought it might come helpful to someone. For a response like below ( A list containing another list among other elements ( classic one-to many relationship) )

[
{
    "id": 1,
    "title": "Spaghetti Carbonara",
    "description": "Just make Spaghetti Carbonara",
    "ingredients": [
        {
            "id": 1,
            "value": 8,
            "unit": "pc",
            "type": "egg"
        },
        {
            "id": 2,
            "value": 500,
            "unit": "g",
            "type": "spaghetti"
        }
    ],
    "instructions": "DIY",
    "servings": 4
}

]

I validated using below code

mockMvc
            .perform(myRequest)
            .andExpect(MockMvcResultMatchers.status().isFound())
            .andExpect(MockMvcResultMatchers.jsonPath("$[*].title").value("Spaghetti Carbonara"))
            .andExpect(MockMvcResultMatchers.jsonPath("$[*].description").value("Just make Spaghetti Carbonara"))
            .andExpect(MockMvcResultMatchers.jsonPath("$[*].ingredients.[*].value",
            containsInAnyOrder(8,500)))
            .andExpect(MockMvcResultMatchers.jsonPath("$[*].ingredients.[*].unit",
                    containsInAnyOrder("pc","g")))
            .andExpect(MockMvcResultMatchers.jsonPath("$[*].ingredients.[*].type",
                    containsInAnyOrder("egg","spaghetti")));

Using hamcrest 2.2.

Irenics answered 18/6, 2023 at 22:45 Comment(0)
K
0

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.

Kaleb answered 2/5, 2022 at 18:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.