/* Copyright (c) 2000-2006 hamcrest.org */ package org.hamcrest.beans; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import org.hamcrest.Condition; import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; import static org.hamcrest.Condition.matched; import static org.hamcrest.Condition.notMatched; import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS; /** * Matcher that asserts that a JavaBean property on an argument passed to the * mock object meets the provided matcher. This is useful for when objects * are created within code under test and passed to a mock object, and you wish * to assert that the created object has certain properties. * <p/> * <h2>Example Usage</h2> * Consider the situation where we have a class representing a person, which * follows the basic JavaBean convention of having get() and possibly set() * methods for it's properties: * <pre> * public class Person { * private String name; * public Person(String person) { * this.person = person; * } * public String getName() { * return name; * } * }</pre> * * And that these person objects are generated within a piece of code under test * (a class named PersonGenerator). This object is sent to one of our mock objects * which overrides the PersonGenerationListener interface: * <pre> * public interface PersonGenerationListener { * public void personGenerated(Person person); * }</pre> * * In order to check that the code under test generates a person with name * "Iain" we would do the following: * <pre> * Mock personGenListenerMock = mock(PersonGenerationListener.class); * personGenListenerMock.expects(once()).method("personGenerated").with(and(isA(Person.class), hasProperty("Name", eq("Iain"))); * PersonGenerationListener listener = (PersonGenerationListener)personGenListenerMock.proxy();</pre> * * If an exception is thrown by the getter method for a property, the property * does not exist, is not readable, or a reflection related exception is thrown * when trying to invoke it then this is treated as an evaluation failure and * the matches method will return false. * <p/> * This matcher class will also work with JavaBean objects that have explicit * bean descriptions via an associated BeanInfo description class. See the * JavaBeans specification for more information: * <p/> * http://java.sun.com/products/javabeans/docs/index.html * * @author Iain McGinniss * @author Nat Pryce * @author Steve Freeman */ public class HasPropertyWithValue<T> extends TypeSafeDiagnosingMatcher<T> { private static final Condition.Step<PropertyDescriptor,Method> WITH_READ_METHOD = withReadMethod(); private final String propertyName; private final Matcher<Object> valueMatcher; public HasPropertyWithValue(String propertyName, Matcher<?> valueMatcher) { this.propertyName = propertyName; this.valueMatcher = nastyGenericsWorkaround(valueMatcher); } @Override public boolean matchesSafely(T bean, Description mismatch) { return propertyOn(bean, mismatch) .and(WITH_READ_METHOD) .and(withPropertyValue(bean)) .matching(valueMatcher, "property '" + propertyName + "' "); } @Override public void describeTo(Description description) { description.appendText("hasProperty(").appendValue(propertyName).appendText(", ") .appendDescriptionOf(valueMatcher).appendText(")"); } private Condition<PropertyDescriptor> propertyOn(T bean, Description mismatch) { PropertyDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean); if (property == null) { mismatch.appendText("No property \"" + propertyName + "\""); return notMatched(); } return matched(property, mismatch); } 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(); } } }; } @SuppressWarnings("unchecked") private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher) { return (Matcher<Object>) valueMatcher; } private static Condition.Step<PropertyDescriptor,Method> withReadMethod() { return new Condition.Step<PropertyDescriptor, java.lang.reflect.Method>() { @Override public Condition<Method> apply(PropertyDescriptor property, Description mismatch) { final Method readMethod = property.getReadMethod(); if (null == readMethod) { mismatch.appendText("property \"" + property.getName() + "\" is not readable"); return notMatched(); } return matched(readMethod, mismatch); } }; } /** * Creates a matcher that matches when the examined object has a JavaBean property * with the specified name whose value satisfies the specified matcher. * <p/> * For example: * <pre>assertThat(myBean, hasProperty("foo", equalTo("bar"))</pre> * * @param propertyName * the name of the JavaBean property that examined beans should possess * @param valueMatcher * a matcher for the value of the specified property of the examined bean */ @Factory public static <T> Matcher<T> hasProperty(String propertyName, Matcher<?> valueMatcher) { return new HasPropertyWithValue<T>(propertyName, valueMatcher); } }