Search code examples
unit-testinghamcrest

Is there a Hamcrest macher that matches public fields?


I have different value objects, each with a different set of fields. How can I check against these with a Hamcrest matcher?

    public class ValueObjectA {
        public Integer field1;
        public String field2;
        public long filed3;
        public Object filed4;
    }

    public class ValueObjectB {
        public String field1;
        public int field2;
    }

This is, what I want to do:

    resultA = getResultA();
    ValueObjectA expectedA = new ValueObjectA();
    expectedA.field1 = 4;
    resultB = getResultB();
    ValueObjectB expectedB = new ValueObjectB();
    expectedB.field1 = "foo";
    assertThat(resultA, new ValueObjectMatcher(expectedA));
    assertThat(resultB, new ValueObjectMatcher(expectedB));

I found a PropertyMatcher but that only uses public getters. I can write something similar, that uses reflection to get the public fields. But is there a readymade one?


Solution

  • Anyway, I wrote something based on the PropertyMatcher, in case someone wants to write unit tests for eg. com.j256.ormlite :

    public class ValueObjectMatcher extends TypeSafeDiagnosingMatcher {
        private final Object expectedVo;
        private final Set<String> fieldNames;
        private final List<FieldMatcher> fieldMatchers;
    
        public ValueObjectMatcher(final Object expectedVo) {
            Field[] fieldsToMatch = expectedVo.getClass().getFields();
            this.expectedVo = expectedVo;
            this.fieldNames = fieldNamesFrom(fieldsToMatch);
            this.fieldMatchers = fieldMatchersFor(expectedVo, fieldsToMatch);
        }
    
        @Override
        protected boolean matchesSafely(final Object item, final Description mismatchDescription) {
            return hasAllFields(item, mismatchDescription) && hasMatchingValues(item, mismatchDescription);
        }
    
        @Override
        public void describeTo(final Description description) {
            description.appendText("same field values as " + expectedVo.getClass().getSimpleName())
                    .appendList(" <", ", ", ">", fieldMatchers);
        }
    
        private boolean hasMatchingValues(final Object item, final Description mismatchDescription) {
            mismatchDescription.appendText(item + " has <");
            int mismatchCount = 0;
    
            for (FieldMatcher fieldMatcher : fieldMatchers) {
                if (!fieldMatcher.matches(item)) {
                    if (mismatchCount != 0) {
                        mismatchDescription.appendText(", ");
                    }
                    fieldMatcher.describeMismatch(item, mismatchDescription);
                    mismatchCount++;
                }
            }
    
            mismatchDescription.appendText(">");
            return mismatchCount == 0;
        }
    
        private boolean hasAllFields(final Object item, final Description mismatchDescription) {
            final Field[] fields = item.getClass().getFields();
    
            final Set<String> itemsFieldNames = fieldNamesFrom(fields);
    
            boolean result = true;
    
            for (String fieldName : fieldNames) {
                if (!itemsFieldNames.contains(fieldName)) {
                    result = false;
                    mismatchDescription.appendText("missing field: " + fieldName);
                }
            }
    
            return result;
        }
    
        private List<FieldMatcher> fieldMatchersFor(final Object expectedVo, final Field[] fields) {
            List<FieldMatcher> result = new ArrayList<FieldMatcher>(fields.length);
            try {
                for (Field field : fields) {
                    result.add(new FieldMatcher(field, expectedVo));
                }
            }
            catch (NoSuchFieldException e) {
                throw new IllegalStateException("Programmer exception, pls replace programmer: " +
                                                "field list doesn't match with the fields of the provided expectedVo", e);
            }
            catch (IllegalAccessException e) {
                throw new IllegalStateException("Programmer exception, pls replace programmer: " +
                                                "field list doesn't match with the fields of the provided expectedVo", e);
            }
            return result;
        }
    
        private Set<String> fieldNamesFrom(final Field[] fieldsToMatch) {
            HashSet<String> result = new HashSet<String>();
            for (Field field : fieldsToMatch) {
                result.add(field.getName());
            }
            return result;
        }
    
        public class FieldMatcher extends DiagnosingMatcher<Object> {
    
            private final Object expectedFieldValue;
            private final String fieldName;
    
            private FieldMatcher(Field field, Object expectedVo) throws NoSuchFieldException, IllegalAccessException {
                this.fieldName = field.getName();
                this.expectedFieldValue = expectedVo.getClass().getField(fieldName).get(expectedVo);
            }
    
            @Override
            protected boolean matches(final Object item, final Description mismatchDescription) {
                try {
                    final Field fieldItem = item.getClass().getField(fieldName);
                    final Object fieldObjectItem = fieldItem.get(item);
    
                    if (fieldObjectItem == null) {
                        if (expectedFieldValue != null) {
                            mismatchDescription.appendText(fieldName + ": " + fieldObjectItem);
                        }
                    } else if (!fieldObjectItem.equals(expectedFieldValue)) {
                        mismatchDescription.appendText(fieldName + ": " + fieldObjectItem);
                    }
                }
                catch (IllegalAccessException e) {
                    mismatchDescription.appendText(fieldName + " is inaccessible");
                    e.printStackTrace();
                }
                catch (NoSuchFieldException e) {
                    mismatchDescription.appendText(fieldName + " doesn't exist");
                    e.printStackTrace();
                }
    
                return false;
            }
    
            @Override
            public void describeTo(final Description description) {
                description.appendText(fieldName + ": " + expectedFieldValue);
            }
        }
    }