int expected, actual;
assertEquals(expected, actual);
assertThat(expected, is(actual));
- For arrays and maps : hasItem, hasKey, hasValue
- Numbers: closeTo – a way to specify equality with an error of margin, greaterThan, lessThan…
- Objects: nullValue, sameInstance
public class OrderMatcher extends BaseMatcher<Order> {
private final Order expected;
private final StringBuilder errors = new StringBuilder();
private OrderMatcher(Order expected) {
this.expected = expected;
}
@Override
public boolean matches(Object item) {
if (!(item instanceof Order)) {
errors.append("received item is not of Order type");
return false;
}
Order actual = (Order) item;
if (actual.getQuantity() != (expected.getQuantity())) {
errors.append("received item had quantity ").append(actual.getQuantity()).append(". Expected ").append(expected.getQuantity());
return false;
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText(errors.toString());
}
@Factory
public static OrderMatcher isOrder(Order expected) {
return new OrderMatcher(expected);
}
}
- Matcher construction can be very repetitive and boring. I needed a way to apply DRY principle to matcher code.
- I needed an unified way to access the matchers. The correct matcher should be chosen by the framework by default.
- I needed to compare objects that had reference to another objects which should have been compared with matchers (the object referencing can go as deep as you want)
- I needed to check a collection of objects using matchers without iterating that collection (doable also with the array matchers… but I wanted more J)
- I needed to have a more flexible matcher. For example, for the same object I needed to check one set of fields, but in another case another one. The out-of-box solution is to have a matcher for each case. Didn’t like that.
To deal with the #1 issue (repetitive code), the RootMatcher class contains the common code for all the matchers like methods for checking if the actual is null, or it has the same type with the expected object, or even if they are the same instance:
public boolean checkIdentityType(Object received) {
if (received == expected) {
return true;
}
if (received == null || expected == null) {
return false;
}
if (!checkType(received)){
return false;
}
return true;
}
private boolean checkType(Object received) {
if (checkType && !getClass(received).equals(getClass(expected))) {
error.append("Expected ").append(expected.getClass()).append(" Received : ").append(received.getClass());
return false;
}
return true;
}
public abstract class RootMatcher extends BaseMatcher {
protected T expected;
protected StringBuilder error = new StringBuilder("[Matcher : " + this.getClass().getName() + "] ");
@Factory
public static Matcher is(Object expected) {
return getMatcher(expected, true);
}
public static RootMatcher getMatcher(Object expected, boolean checkType) {
try {
Class matcherClass = Class.forName(expected.getClass().getName() + "Matcher");
Constructor constructor = matcherClass.getConstructor(expected.getClass());
return (RootMatcher) constructor.newInstance(expected);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
}
return (RootMatcher) new EqualMatcher(expected);
}
- The matcher for an object has the name of the object + the string Matcher
- The matcher is in the same package as the object to be matched (recommendable to be in the same package, but in the test directory)
Using this strategy I succeeded in using a single matcher: RootMatcher.is that will provide me the exact matcher that I need
public boolean checkEquality(Object expected, Object received) {
String result = checkEqualityAndReturnError(expected, received);
return result == null || result.trim().isEmpty();
}
public String checkEqualityAndReturnError(Object expected, Object received) {
if (isIgnoreObject(expected)) {
return null;
}
if (expected == null && received == null) {
return null;
}
if (expected == null || received == null) {
return "Expected or received is null and the other is not: expected " + expected + " received " + received;
}
RootMatcher matcher = getMatcher(expected);
boolean result = matcher.matches(received);
if (result) {
return null;
} else {
StringBuilder sb = new StringBuilder();
matcher.describeTo(sb);
return sb.toString();
}
}
private final static Map ignorable = new HashMap();
static {
ignorable.put(String.class, "%%%%IGNORE_ME%%%%");
ignorable.put(Integer.class, new Integer(Integer.MAX_VALUE - 1));
ignorable.put(Long.class, new Long(Long.MAX_VALUE - 1));
ignorable.put(Float.class, new Float(Float.MAX_VALUE - 1));
}
/**
* we will ignore mock objects in matchers
*/
private boolean isIgnoreObject(Object object) {
if (object == null) {
return false;
}
Object ignObject = ignorable.get(object.getClass());
if (ignObject != null) {
return ignObject.equals(object);
}
return Mockito.mockingDetails(object).isMock();
}
@SuppressWarnings("unchecked")
public static M getIgnoreObject(Class clazz) {
Object obj = ignorable.get(clazz);
if (obj != null) {
return (M) obj;
}
return (M) Mockito.mock(clazz);
}
@SuppressWarnings("unchecked")
public static M getIgnoreObject(Object obj) {
return (M) getIgnoreObject(obj.getClass());
}
import static […]RootMatcher.is;
Order expected = OrderBuilder.anOrder().withQuantity(2)
.withTimestamp(RootManager.getIgnoredObject(Long.class))
.withDescription(“specific description”).build()
assertThat(order, is(expected);
Of course that the implementation can be improved using annotation, but the core concepts still remain.
Meta: this post is part of the Java Advent Calendar and is licensed under the Creative Commons 3.0 Attribution license. If you like it, please spread the word by sharing, tweeting, FB, G+ and so on!
Cristina IFTIMI December 4, 2013
Hi, nice article, I would like to see your complete source code and give it a try 😉
Stefan Bulzan December 6, 2013
I'll update the article as soon as I have the code ready for publishing. Thanks.
Brandon October 12, 2014
Very nice work! Mind sharing your complete code? Thanks!