I have a piece of code in a test that checks that a list of results contains certain properties, using Hamcrest 2.2:
assertThat(result.getUsers(), hasItem(
hasProperty("name", equalTo(user1.getName()))
));
assertThat(result.getUsers(), hasItem(
hasProperty("name", equalTo(user2.getName()))
));
This worked perfectly fine when NameDto
was a normal class. But after I changed it to a Record
, Hamcrest's hasProperty
complains about there being no property named name
:
java.lang.AssertionError:
Expected: a collection containing hasProperty("name", "Test Name")
but: mismatches were: [No property "name", No property "name"]
Is there some other matcher I can use to achieve the same matching as before? Or some other workaround I can use to get it to work with records?
The accessor method of a record field does not follow the regular JavaBeans convention, so the User
record (say public record User (String name) {}
) will have an accessor method whose name is name()
instead of getName()
.
I suspect this is why Hamcrest considers there is no property. I don't think there is a way out-of-the-box in Hamcrest other than to write a custom Matcher.
Here's a custom HasRecordComponentWithValue
inspired by the existing HasPropertyWithValue
. The main utility leveraged here is Java's Class.getRecordComponents()
:
public static class HasRecordComponentWithValue<T> extends TypeSafeDiagnosingMatcher<T> {
private static final Condition.Step<RecordComponent,Method> WITH_READ_METHOD = withReadMethod();
private final String componentName;
private final Matcher<Object> valueMatcher;
public HasRecordComponentWithValue(String componentName, Matcher<?> valueMatcher) {
this.componentName = componentName;
this.valueMatcher = nastyGenericsWorkaround(valueMatcher);
}
@Override
public boolean matchesSafely(T bean, Description mismatch) {
return recordComponentOn(bean, mismatch)
.and(WITH_READ_METHOD)
.and(withPropertyValue(bean))
.matching(valueMatcher, "record component'" + componentName + "' ");
}
private Condition.Step<Method, Object> withPropertyValue(final T bean) {
return new Condition.Step<Method, Object>() {
@Override
public Condition<Object> apply(Method readMethod, Description mismatch) {
try {
return matched(readMethod.invoke(bean, NO_ARGUMENTS), mismatch);
} catch (Exception e) {
mismatch.appendText(e.getMessage());
return notMatched();
}
}
};
}
@Override
public void describeTo(Description description) {
description.appendText("hasRecordComponent(").appendValue(componentName).appendText(", ")
.appendDescriptionOf(valueMatcher).appendText(")");
}
private Condition<RecordComponent> recordComponentOn(T bean, Description mismatch) {
RecordComponent[] recordComponents = bean.getClass().getRecordComponents();
for(RecordComponent comp : recordComponents) {
if(comp.getName().equals(componentName)) {
return matched(comp, mismatch);
}
}
mismatch.appendText("No record component \"" + componentName + "\"");
return notMatched();
}
@SuppressWarnings("unchecked")
private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher) {
return (Matcher<Object>) valueMatcher;
}
private static Condition.Step<RecordComponent,Method> withReadMethod() {
return new Condition.Step<RecordComponent, java.lang.reflect.Method>() {
@Override
public Condition<Method> apply(RecordComponent property, Description mismatch) {
final Method readMethod = property.getAccessor();
if (null == readMethod) {
mismatch.appendText("record component \"" + property.getName() + "\" is not readable");
return notMatched();
}
return matched(readMethod, mismatch);
}
};
}
@Factory
public static <T> Matcher<T> hasRecordComponent(String componentName, Matcher<?> valueMatcher) {
return new HasRecordComponentWithValue<T>(componentName, valueMatcher);
}
}