Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/build
/classes
/test-output
.idea/dictionaries
.idea/scopes
.idea/workspace.xml
89 changes: 89 additions & 0 deletions hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>Matches multiple attributes of an object within a single assertion</p>
*
* <p>How to use it: <p>
*
* // Static imports <br/>
* import static org.hamcrest.beans.BeanHas.has; <br/>
* import static org.hamcrest.beans.BeanProperty.property; <br/>
* <b>import static org.hamcrest.MatcherAssert.assertThat;</b><br/>
* import static org.hamcrest.Matchers.equalTo;<br/>
* import static org.hamcrest.Matchers.greaterThan;<br/>
* <p>
* Person person = new Person();<br/>
* person.setFirstName("Sandro");<br/>
* person.setAge(35);<br/>
* </p>
* <p>
* Country uk = new Country();<br/>
* uk.setName("United Kingdom");<br/>
* </p>
* <p>
* Address address = new Address();<br/>
* address.setPostcode("1234556");<br/>
* address.setCity("London");<br/>
* address.setCountry(uk);<br/>
* </p>
* <p>
* person.setAddress(address);<br/>
* </p>
* <p>
* assertThat(person, has(<br/>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; property("firstName", equalTo("Sandro")),<br/>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; property("age", greaterThan(18)),<br/>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; property("address.city", equalTo("London")),<br/>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; property("address.postcode", equalTo("1234556")),<br/>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; property("address.country.name", equalTo("United Kingdom"))));<br/>
* </p>
*
* @author Sandro Mancuso
*/
public class BeanHas<T> extends BaseMatcher<T> {

private BeanProperty<?>[] propertyMatchers;
private Description expectedDescription = new StringDescription();
private Description mismatchDescription = new StringDescription();

public BeanHas(BeanProperty<?>... propertyMatchers) {
this.propertyMatchers = propertyMatchers;
}

@Factory
public static <T> BeanHas<T> has(BeanProperty<?>... propertyMatchers) {
return new BeanHas<T>(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);
}

}
127 changes: 127 additions & 0 deletions hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java
Original file line number Diff line number Diff line change
@@ -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<T> extends TypeSafeDiagnosingMatcher<T> {

private String matchingPropertyName;
private Matcher<?> valueMatcher;

public BeanProperty(String propertyName, Matcher<?> valueMatcher) {
this.matchingPropertyName = propertyName;
this.valueMatcher = valueMatcher;
}

@Factory
public static <T> BeanProperty<T> property(String propertyName,Matcher<?> value) {
return new BeanProperty<T>(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(" ");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public static <T> void assertMismatchDescription(String expected, Matcher<? supe
Assert.assertEquals("Expected mismatch description", expected, description.toString().trim());
}

public static <T> void assertMismatchDescription(String expected, Matcher<? super T> 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);
Expand Down
123 changes: 123 additions & 0 deletions hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java
Original file line number Diff line number Diff line change
@@ -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<Object> unmatchingProperty;
private BeanProperty<Object> otherUnmatchingProperty;
private BeanProperty<Object> matchingProperty;

private Description expectedDescription;
private Description mismatchDescription;

@Override
protected void setUp() {
bean = new Object();
matchingProperty = new MockBeanProperty<Object>(MATCHES, EXPECTED_DESCRIPTION, MISMATCH_DESCRIPTION);
unmatchingProperty = new MockBeanProperty<Object>(DOES_NOT_MATCH, EXPECTED_DESCRIPTION, MISMATCH_DESCRIPTION);
otherUnmatchingProperty = new MockBeanProperty<Object>(DOES_NOT_MATCH, OTHER_EXPECTED_DESCRIPTION, OTHER_MISMATCH_DESCRIPTION);
expectedDescription = new StringDescription();
mismatchDescription = new StringDescription();
}

public void testNoExpectedDescriptionWhenPropertyIsAMatch() {
BeanHas<Object> 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<Object> beanHas = BeanHas.has(unmatchingProperty);

beanHas.matches(bean);
beanHas.describeTo(expectedDescription);

assertThat(expectedDescription.toString(), is(EXPECTED_DESCRIPTION));
}

public void testPopulateMismatchDescriptionWhenPropertiesDoNotMatch() {
BeanHas<Object> beanHas = BeanHas.has(unmatchingProperty);

beanHas.matches(bean);
beanHas.describeMismatch(bean, mismatchDescription);

assertThat(mismatchDescription.toString(), is(MISMATCH_DESCRIPTION));
}

public void testExpectedDescriptionIsAppendedWhenMultiplePropertiesFail() {
BeanHas<Object> 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<Object> 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<T> extends BeanProperty<T> {

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);
}

}

}
Loading