Sunday, August 20, 2006, 18:07
After using EasyMock for a few weeks it usually turns out that explanations given in case of test failures (unexpected calls to mock objects) are useless and you need to debug the test to see the real reason of failure. In 90% of cases the reason is argument's value mismatch during comparison to expected argument. You can assure better faiulre explanations with the following:
(1)
implementing toString()
in classes used as arguments
(2)
correctly implementing appendTo(StringBuffer)
in your matcher
Unfortunatelly implementing
appendTo(StringBuffer)
means creating a text representation of matcher's logic already written in matches(Object)
. At this point it's quite obvious that there should be a way to make it a little more intelligent. Moreover in this scenario failure explanations still don't give you the most precize information - you need to investigate to see which argument didn't match (often by analysing argument's properties values, one by one).
My solution is an argument matcher builder. Matchers created with this class contain generic implementation of
matches(Object)
and appendTo(StringBuffer)
. There is one common method to configure both matcher's logic and string representation - append(String, Object)
. Also, the text appended to StringBuffer
contains information about exact property which caused the test to fail. A sample implementation is as simple as the following:
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.beanutils.PropertyUtils;
import org.easymock.EasyMock;
import org.easymock.IArgumentMatcher;
public class ArgumentMatcherBuilder<T> {
private static class Expectation {
private String propertyName;
private Object expectedValue;
public Object getExpectedValue() {
return expectedValue;
}
public String getPropertyName() {
return propertyName;
}
public Expectation(String propertyName, Object expectedValue) {
this.propertyName = propertyName;
this.expectedValue = expectedValue;
}
}
private static class FailedExpectation extends Expectation {
private Object actualValue;
public FailedExpectation(String propertyName, Object expectedValue,
Object actualValue) {
super(propertyName, expectedValue);
this.actualValue = actualValue;
}
public Object getActualValue() {
return actualValue;
}
}
private final List<Expectation> expectations = new ArrayList<Expectation>();
private final List<FailedExpectation> failedExpectations = new ArrayList<FailedExpectation>();
public ArgumentMatcherBuilder<T> append(String propertyName,
Object expectedValue) {
expectations.add(new Expectation(propertyName, expectedValue));
return this;
}
public T registerMatcher() {
EasyMock.reportMatcher(new IArgumentMatcher() {
final public void appendTo(final StringBuffer buffer) {
buffer.append("<");
for (FailedExpectation failedExpectation : failedExpectations) {
buffer.append(failedExpectation.getPropertyName()
+ "[exp: "
+ failedExpectation.getExpectedValue().toString()
+ " ; act: " + failedExpectation.getActualValue()
+ "] ");
}
buffer.append(">");
}
final public boolean matches(Object obj) {
failedExpectations.clear();
boolean match = true;
for (Expectation expectation : expectations) {
Object actualValue;
try {
actualValue = PropertyUtils.getProperty(obj,
expectation.getPropertyName());
} catch (Exception e) {
throw new RuntimeException(
"Problem while accessing property, " +
"check property name, " +
"getter signature and access modifier",
e);
}
if (!actualValue.equals(expectation.getExpectedValue())) {
match = false;
failedExpectations.add(new FailedExpectation(
expectation.getPropertyName(), expectation
.getExpectedValue(), actualValue));
}
}
return match;
}
});
return null;
}
}
| permalink