diff --git a/.gitignore b/.gitignore index 31a9c703..9b37d0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /build /classes +/test-output .idea/dictionaries .idea/scopes .idea/workspace.xml diff --git a/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java new file mode 100644 index 00000000..6196ecfb --- /dev/null +++ b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java @@ -0,0 +1,89 @@ +package org.hamcrest.beans; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Factory; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; + +/** + *

Matches multiple attributes of an object within a single assertion

+ * + *

How to use it:

+ * + * // Static imports
+ * import static org.hamcrest.beans.BeanHas.has;
+ * import static org.hamcrest.beans.BeanProperty.property;
+ * import static org.hamcrest.MatcherAssert.assertThat;
+ * import static org.hamcrest.Matchers.equalTo;
+ * import static org.hamcrest.Matchers.greaterThan;
+ *

+ * Person person = new Person();
+ * person.setFirstName("Sandro");
+ * person.setAge(35);
+ *

+ *

+ * Country uk = new Country();
+ * uk.setName("United Kingdom");
+ *

+ *

+ * Address address = new Address();
+ * address.setPostcode("1234556");
+ * address.setCity("London");
+ * address.setCountry(uk);
+ *

+ *

+ * person.setAddress(address);
+ *

+ *

+ * assertThat(person, has(
+ *                property("firstName", equalTo("Sandro")),
+ *                property("age", greaterThan(18)),
+ *                property("address.city", equalTo("London")),
+ *                property("address.postcode", equalTo("1234556")),
+ *                property("address.country.name", equalTo("United Kingdom"))));
+ *

+ * + * @author Sandro Mancuso + */ +public class BeanHas extends BaseMatcher { + + private BeanProperty[] propertyMatchers; + private Description expectedDescription = new StringDescription(); + private Description mismatchDescription = new StringDescription(); + + public BeanHas(BeanProperty... propertyMatchers) { + this.propertyMatchers = propertyMatchers; + } + + @Factory + public static BeanHas has(BeanProperty... propertyMatchers) { + return new BeanHas(propertyMatchers); + } + + public boolean matches(Object item) { + boolean matches = true; + for (BeanProperty matcher : propertyMatchers) { + if (!matcher.matches(item)) { + matches = false; + appendDescriptions(item, matcher); + } + } + return matches; + } + + public void describeTo(Description description) { + description.appendText(expectedDescription.toString()); + } + + @Override + public void describeMismatch(Object item, Description description) { + description.appendText(mismatchDescription.toString()); + } + + private void appendDescriptions(Object item, Matcher matcher) { + matcher.describeTo(expectedDescription); + matcher.describeMismatch(item, mismatchDescription); + } + +} diff --git a/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java new file mode 100644 index 00000000..c8d0a0ab --- /dev/null +++ b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java @@ -0,0 +1,127 @@ +package org.hamcrest.beans; + +import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS; +import static org.hamcrest.beans.PropertyUtil.getPropertyDescriptor; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; + +import org.hamcrest.Description; +import org.hamcrest.Factory; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +/** + * Property matcher that should be used in conjunction with {@link BeanHas}. + * + * @see BeanHas + * + * @author Sandro Mancuso + */ +public class BeanProperty extends TypeSafeDiagnosingMatcher { + + private String matchingPropertyName; + private Matcher valueMatcher; + + public BeanProperty(String propertyName, Matcher valueMatcher) { + this.matchingPropertyName = propertyName; + this.valueMatcher = valueMatcher; + } + + @Factory + public static BeanProperty property(String propertyName,Matcher value) { + return new BeanProperty(propertyName, value); + } + + @Override + public boolean matchesSafely(T bean, Description mismatchDescription) { + try { + return matchesSafely(bean, matchingPropertyName, mismatchDescription); + } catch (Exception e) { + mismatchDescription.appendValue(e); + return false; + } + } + + private boolean matchesSafely(Object bean, String propertyName, Description mismatchDescription) + throws Exception { + Object parentObject = bean; + if (isComposedProperty(propertyName)) { + String memberObjectProperty = getMemberObjectProperty(propertyName); + Object memberObject = getPropertyValue(parentObject, memberObjectProperty); + String nextProperty = getNextProperty(propertyName); + return matchesSafely(memberObject, nextProperty, mismatchDescription); + } else { + return matchProperty(bean, propertyName, mismatchDescription); + } + } + + private String getNextProperty(String composedPropertyName) { + return composedPropertyName.substring(composedPropertyName.indexOf(".") + 1); + } + + private Object getPropertyValue(Object parentObject, String memberObjectProperty) + throws Exception { + PropertyDescriptor property = getPropertyDescriptor(memberObjectProperty, parentObject); + Method readMethod = property.getReadMethod(); + return readMethod.invoke(parentObject, NO_ARGUMENTS); + } + + private boolean isComposedProperty(String propertyName) { + return propertyName.contains("."); + } + + private String getMemberObjectProperty(String composedPropertyName) { + return composedPropertyName.substring(0, composedPropertyName.indexOf(".")); + } + + private boolean matchProperty(Object bean, String propertyName, Description mismatchDescription) + throws Exception { + Method readMethod = findReadMethod(bean, propertyName, mismatchDescription); + return (readMethod != null) + ? matchPropertyValue(bean, readMethod, mismatchDescription) + : false; + } + + private boolean matchPropertyValue(Object bean, Method readMethod, Description mismatchDescription) + throws Exception { + Object propertyValue = readMethod.invoke(bean, NO_ARGUMENTS); + boolean valueMatches = valueMatcher.matches(propertyValue); + if (!valueMatches) { + appendSeparatorTo(mismatchDescription); + mismatchDescription.appendText("property \'" + matchingPropertyName + "\' "); + valueMatcher.describeMismatch(propertyValue, mismatchDescription); + } + return valueMatches; + } + + private void appendSeparatorTo(Description description) { + if (description.toString().length() > 0) { + description.appendText(", "); + } + } + + private Method findReadMethod(Object argument, String propertyName, Description mismatchDescription) + throws IllegalArgumentException { + PropertyDescriptor propertyDescriptor = getPropertyDescriptor(propertyName, argument); + if (null == propertyDescriptor) { + mismatchDescription.appendText("No property \"" + matchingPropertyName + "\""); + return null; + } + Method readMethod = propertyDescriptor.getReadMethod(); + if (null == readMethod) { + mismatchDescription.appendText("property \"" + matchingPropertyName + "\" is not readable"); + } + return readMethod; + } + + public void describeTo(Description description) { + appendSeparatorTo(description); + description.appendText("property "); + description.appendValue(matchingPropertyName); + description.appendText(" = "); + description.appendDescriptionOf(valueMatcher); + description.appendText(" "); + } + +} diff --git a/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java b/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java index 0032d821..b2fda6d0 100644 --- a/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java +++ b/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java @@ -33,6 +33,12 @@ public static void assertMismatchDescription(String expected, Matcher void assertMismatchDescription(String expected, Matcher matcher, T arg, Description description) { + Assert.assertFalse("Precondtion: Matcher should not match item.", matcher.matches(arg)); + matcher.describeMismatch(arg, description); + Assert.assertEquals("Expected mismatch description", expected, description.toString().trim()); + } + public void testIsNullSafe() { // should not throw a NullPointerException createMatcher().matches(null); diff --git a/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java new file mode 100644 index 00000000..977f8e9c --- /dev/null +++ b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java @@ -0,0 +1,123 @@ +package org.hamcrest.beans; + +import static org.hamcrest.Matchers.anything; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import junit.framework.TestCase; + +import org.hamcrest.Description; +import org.hamcrest.StringDescription; + +public class BeanHasTest extends TestCase { + + private static final String MISMATCH_DESCRIPTION = "MISMATCH DESCRIPTION"; + private static final String OTHER_MISMATCH_DESCRIPTION = "OTHER MISMATCH DESCRIPTION"; + private static final String EXPECTED_DESCRIPTION = "EXPECTED DESCRIPTION"; + private static final String OTHER_EXPECTED_DESCRIPTION = "OTHER EXPECTED DESCRIPTION"; + private static final boolean MATCHES = true; + private static final boolean DOES_NOT_MATCH = false; + + private Object bean; + private BeanProperty unmatchingProperty; + private BeanProperty otherUnmatchingProperty; + private BeanProperty matchingProperty; + + private Description expectedDescription; + private Description mismatchDescription; + + @Override + protected void setUp() { + bean = new Object(); + matchingProperty = new MockBeanProperty(MATCHES, EXPECTED_DESCRIPTION, MISMATCH_DESCRIPTION); + unmatchingProperty = new MockBeanProperty(DOES_NOT_MATCH, EXPECTED_DESCRIPTION, MISMATCH_DESCRIPTION); + otherUnmatchingProperty = new MockBeanProperty(DOES_NOT_MATCH, OTHER_EXPECTED_DESCRIPTION, OTHER_MISMATCH_DESCRIPTION); + expectedDescription = new StringDescription(); + mismatchDescription = new StringDescription(); + } + + public void testNoExpectedDescriptionWhenPropertyIsAMatch() { + BeanHas beanHas = BeanHas.has(matchingProperty); + + beanHas.matches(bean); + beanHas.describeTo(expectedDescription); + + assertThat(expectedDescription.toString(), isEmptyString()); + } + + public void testNoMismatchingDescriptionsWhenPropertyIsAMatch() { + BeanHas beanHas = BeanHas.has(matchingProperty); + + beanHas.matches(bean); + beanHas.describeMismatch(bean, mismatchDescription); + + assertThat(mismatchDescription.toString(), isEmptyString()); + } + + public void testPopulateExpectedDescriptionWhenPropertiesDoNotMatch() { + BeanHas beanHas = BeanHas.has(unmatchingProperty); + + beanHas.matches(bean); + beanHas.describeTo(expectedDescription); + + assertThat(expectedDescription.toString(), is(EXPECTED_DESCRIPTION)); + } + + public void testPopulateMismatchDescriptionWhenPropertiesDoNotMatch() { + BeanHas beanHas = BeanHas.has(unmatchingProperty); + + beanHas.matches(bean); + beanHas.describeMismatch(bean, mismatchDescription); + + assertThat(mismatchDescription.toString(), is(MISMATCH_DESCRIPTION)); + } + + public void testExpectedDescriptionIsAppendedWhenMultiplePropertiesFail() { + BeanHas beanHas = BeanHas.has(unmatchingProperty, + matchingProperty, + otherUnmatchingProperty); + + beanHas.matches(bean); + beanHas.describeTo(expectedDescription); + + assertThat(expectedDescription.toString(), is(EXPECTED_DESCRIPTION + OTHER_EXPECTED_DESCRIPTION)); + } + + public void testMismatchDescriptionIsAppendedWhenMultiplePropertiesFail() { + BeanHas beanHas = BeanHas.has(unmatchingProperty, + matchingProperty, + otherUnmatchingProperty); + + beanHas.matches(bean); + beanHas.describeMismatch(bean, expectedDescription); + + assertThat(expectedDescription.toString(), is(MISMATCH_DESCRIPTION + OTHER_MISMATCH_DESCRIPTION)); + } + + private class MockBeanProperty extends BeanProperty { + + private boolean matches; + private String expectedDescription; + private String mismatchDescription; + + public MockBeanProperty(boolean matches, String expectedDescription, String mismatchDescription) { + super("any property", anything()); + this.matches = matches; + this.expectedDescription = expectedDescription; + this.mismatchDescription = mismatchDescription; + } + + @Override + public boolean matchesSafely(T bean, Description mismatchDescription) { + mismatchDescription.appendText(this.mismatchDescription); + return matches; + } + + @Override + public void describeTo(Description description) { + description.appendText(expectedDescription); + } + + } + +} diff --git a/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanPropertyTest.java b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanPropertyTest.java new file mode 100644 index 00000000..37d4a4de --- /dev/null +++ b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanPropertyTest.java @@ -0,0 +1,114 @@ +package org.hamcrest.beans; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.beans.BeanProperty.property; +import static org.hamcrest.core.IsAnything.anything; +import static org.hamcrest.core.IsEqual.equalTo; + +import org.hamcrest.AbstractMatcherTest; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; +import org.hamcrest.beans.HasPropertyWithValueTest.BeanWithInfo; +import org.hamcrest.beans.HasPropertyWithValueTest.BeanWithoutInfo; +import org.hamcrest.core.IsEqual; + +public class BeanPropertyTest extends AbstractMatcherTest { + + private final BeanWithoutInfo shouldMatch = new BeanWithoutInfo("is expected"); + private final BeanWithoutInfo shouldNotMatch = new BeanWithoutInfo("not expected"); + + private final BeanWithInfo beanWithInfo = new BeanWithInfo("with info"); + private final ComposedBean composedBean = new ComposedBean("with info", beanWithInfo); + + @Override + protected Matcher createMatcher() { + return property("irrelevant", anything()); + } + + public void testMatchesInfolessBeanWithMatchedNamedProperty() { + assertMatches("with property", property("property", equalTo("is expected")), shouldMatch); + assertMismatchDescription("property 'property' was \"not expected\"", + property("property", equalTo("is expected")), shouldNotMatch); + } + + public void testMatchesBeanWithInfoWithMatchedNamedProperty() { + assertMatches("with bean info", property("property", equalTo("with info")), beanWithInfo); + assertMismatchDescription("property 'property' was \"with info\"", + property("property", equalTo("without info")), beanWithInfo); + } + + public void testDoesNotMatchInfolessBeanWithoutMatchedNamedProperty() { + assertMismatchDescription("No property \"nonExistentProperty\"", + property("nonExistentProperty", anything()), shouldNotMatch); + } + + public void testDoesNotMatchWriteOnlyProperty() { + assertMismatchDescription("property \"writeOnlyProperty\" is not readable", + property("writeOnlyProperty", anything()), shouldNotMatch); + } + + public void testDescribeTo() { + assertDescription("property \"property\" = ", property("property", equalTo(true))); + } + + public void testAccumulateMismatchDescriptions() { + Description mismatchDescription = new StringDescription(); + mismatchDescription.appendText("previous mismatch"); + assertMismatchDescription("previous mismatch, property 'property' was \"with info\"", + property("property", equalTo("without info")), beanWithInfo, mismatchDescription); + } + + public void testMatchesPropertyAndValue() { + assertMatches("property with value", property( "property", anything()), beanWithInfo); + } + + public void testMatchesPropertyAndValueForMemberObject() { + assertMatches("property with value", property("beanWithInfo.property", anything()), composedBean); + } + + public void testDoesNotWriteMismatchIfPropertyMatches() { + Description description = new StringDescription(); + property( "property", anything()).describeMismatch(beanWithInfo, description); + assertEquals("Expected mismatch description", "", description.toString()); + } + + public void testDescribesMissingPropertyMismatch() { + assertMismatchDescription("No property \"honk\"", property( "honk", anything()), shouldNotMatch); + } + + public void testCanAccessAnAnonymousInnerClass() { + class X implements IX { + @Override + public int getTest() { + return 1; + } + } + + assertThat(new X(), HasPropertyWithValue.hasProperty("test", IsEqual.equalTo(1))); + } + + interface IX { + int getTest(); + } + + public class ComposedBean { + + private String propertyValue; + private BeanWithInfo beanWithInfo; + + public ComposedBean(String propertyValue, BeanWithInfo beanWithInfo) { + this.propertyValue = propertyValue; + this.beanWithInfo = beanWithInfo; + } + + public String getProperty() { + return this.propertyValue; + } + + public BeanWithInfo getBeanWithInfo() { + return this.beanWithInfo; + } + } + +}