Search code examples
javaunit-testingjunithamcrest

Hamcrest: test list contains an item that has a private field with a certain value


I have this class:

public class A {
    private int x;

    public A(int x) {
        this.x = x;
    }
}

And a method in a different class I want to test:

public class B {
    public List underTest() {
        int x = doStuff();
        return Collections.singletonList(new A(x));
    }

    private int doStuff() { /* ... */ }
}

I want to test that, at the end of underTest(), the item in the returned list contains an x field equal to a certain value. I've written this:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;

@Test
public void Test1() {
    List result = bInstance.underTest();
    assertThat(result, contains(hasProperty("x", is(1))));
}

But junit complains that item 0: No Property "x" for my test case.

How can I test this? The only thing I can think of is to add a public getter for getX(), then iterate through the returned List and check every element. Remember that, while the method returns a singletonList, the return type is just List, so it could be changed in the future to have multiple values in it.


Solution

  • I think it's first worth saying that testing class internals like this is not a good idea, except maybe in very special cases. Your tests will be brittle and changes that would normally be totally safe - i.e. renaming a field - may now cause your automated build to fail. You should test external behaviour, not implementation details.

    It seems like you'd be better off implementing equals and hashCode in your class A, so then you can simply do:

    contains(new A(1))
    

    With that said, if you do have a genuinely good reason to do this (and such cases will be rare) then you cannot use hasProperty for this.

    From the JavaDoc:

    Creates a matcher that matches when the examined object has a JavaBean property with the specified name whose value satisfies the specified matcher.

    I believe this implies that you would need a method named getX.

    You shouldn't add such a method just for the purpose of a test, but you can write your own general-purpose Matcher implementation that will use reflection to examine the fields of the class.

    Here is a sample implementation:

    class ReflectiveFieldMatcher<T> extends BaseMatcher<Object>
    {
        private final String fieldName;
        private final T expectedValue;
    
        ReflectiveFieldMatcher(final String fieldName, final T expectedValue)
        {
            this.fieldName = fieldName;
            this.expectedValue = expectedValue;
        }
    
        @Override
        public boolean matches(final Object obj)
        {
            for (final Field field : obj.getClass().getFields())
            {
                if (field.getName().equals(fieldName))
                {
                    field.setAccessible(true);
                    try
                    {
                        Object value = field.get(obj);
                        return expectedValue.equals(value);
                    }
                    catch (final IllegalAccessException e)
                    {
                        throw new RuntimeException(e);
                    }
                }
            }
            return false;
        }
    
        @Override
        public void describeTo(final Description description)
        {
            description.appendText("Object with field '" + fieldName + "' with value: " + expectedValue);
        }
    }
    

    Your example would now look like this:

    assertThat(result, contains(new ReflectiveFieldMatcher<>("x", 1)));