Like the AlexR's answer states it is a good idea to implement good equals()
, hashCode()
and toString()
methods and Apache commons has great helpers for that if performance is not the most important concern (and for unit testing it is not).
I had a similar test requirement in the past and there I did not just want to hear that the value object was different but also see which properties were different (and not just the first but all). I created a helper (using Spring's BeanWrapper) to do just that, it is available in: https://bitbucket.org/fhoeben/hsac-test and allows you to call UnitTestHelper.assertEqualsWithDiff(T expected, T actual)
in you unit tests
/**
* Checks whether expected and actual are equal, and if not shows which
* properties differ.
* @param expected expected object.
* @param actual actual object
* @param <T> object type.
*/
public static <T> void assertEqualsWithDiff(T expected, T actual) {
Map<String, String[]> diffs = getDiffs(null, expected, actual);
if (!diffs.isEmpty()) {
StringBuilder diffString = new StringBuilder();
for (Entry<String, String[]> diff : diffs.entrySet()) {
appendDiff(diffString, diff);
}
fail(diffs.size() + " difference(s) between expected and actual:\n" + diffString);
}
}
private static void appendDiff(StringBuilder diffString, Entry<String, String[]> diff) {
String propertyName = diff.getKey();
String[] value = diff.getValue();
String expectedValue = value[0];
String actualValue = value[1];
diffString.append(propertyName);
diffString.append(": '");
diffString.append(expectedValue);
diffString.append("' <> '");
diffString.append(actualValue);
diffString.append("'\n");
}
private static Map<String, String[]> getDiffs(String path, Object expected, Object actual) {
Map<String, String[]> diffs = Collections.emptyMap();
if (expected == null) {
if (actual != null) {
diffs = createDiff(path, expected, actual);
}
} else if (!expected.equals(actual)) {
if (actual == null
|| isInstanceOfSimpleClass(expected)) {
diffs = createDiff(path, expected, actual);
} else if (expected instanceof List) {
diffs = listDiffs(path, (List) expected, (List) actual);
} else {
diffs = getNestedDiffs(path, expected, actual);
}
if (diffs.isEmpty() && !(expected instanceof JAXBElement)) {
throw new IllegalArgumentException("Found elements that are not equal, "
+ "but not able to determine difference, "
+ path);
}
}
return diffs;
}
private static boolean isInstanceOfSimpleClass(Object expected) {
return expected instanceof Enum
|| expected instanceof String
|| expected instanceof XMLGregorianCalendar
|| expected instanceof Number
|| expected instanceof Boolean;
}
private static Map<String, String[]> listDiffs(String path, List expectedList, List actualList) {
Map<String, String[]> diffs = new LinkedHashMap<String, String[]>();
String pathFormat = path + "[%s]";
for (int i = 0; i < expectedList.size(); i++) {
String nestedPath = String.format(pathFormat, i);
Object expected = expectedList.get(i);
Map<String, String[]> elementDiffs;
if (actualList.size() > i) {
Object actual = actualList.get(i);
elementDiffs = getDiffs(nestedPath, expected, actual);
} else {
elementDiffs = createDiff(nestedPath, expected, "<no element>");
}
diffs.putAll(elementDiffs);
}
for (int i = expectedList.size(); i < actualList.size(); i++) {
String nestedPath = String.format(pathFormat, i);
diffs.put(nestedPath, createDiff("<no element>", actualList.get(i)));
}
return diffs;
}
private static Map<String, String[]> getNestedDiffs(String path, Object expected, Object actual) {
Map<String, String[]> diffs = new LinkedHashMap<String, String[]>(0);
BeanWrapper expectedWrapper = getWrapper(expected);
BeanWrapper actualWrapper = getWrapper(actual);
PropertyDescriptor[] descriptors = expectedWrapper.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : descriptors) {
String propertyName = propertyDescriptor.getName();
Map<String, String[]> nestedDiffs =
getNestedDiffs(path, propertyName,
expectedWrapper, actualWrapper);
diffs.putAll(nestedDiffs);
}
return diffs;
}
private static Map<String, String[]> getNestedDiffs(
String path,
String propertyName,
BeanWrapper expectedWrapper,
BeanWrapper actualWrapper) {
String nestedPath = propertyName;
if (path != null) {
nestedPath = path + "." + propertyName;
}
Object expectedValue = getValue(expectedWrapper, propertyName);
Object actualValue = getValue(actualWrapper, propertyName);
return getDiffs(nestedPath, expectedValue, actualValue);
}
private static Map<String, String[]> createDiff(String path, Object expected, Object actual) {
return Collections.singletonMap(path, createDiff(expected, actual));
}
private static String[] createDiff(Object expected, Object actual) {
return new String[] {getString(expected), getString(actual)};
}
private static String getString(Object value) {
return String.valueOf(value);
}
private static Object getValue(BeanWrapper wrapper, String propertyName) {
Object result = null;
if (wrapper.isReadableProperty(propertyName)) {
result = wrapper.getPropertyValue(propertyName);
} else {
PropertyDescriptor propertyDescriptor = wrapper.getPropertyDescriptor(propertyName);
Class<?> propertyType = propertyDescriptor.getPropertyType();
if (Boolean.class.equals(propertyType)) {
String name = StringUtils.capitalize(propertyName);
Object expected = wrapper.getWrappedInstance();
Method m = ReflectionUtils.findMethod(expected.getClass(), "is" + name);
if (m != null && m.getReturnType().equals(Boolean.class)) {
result = ReflectionUtils.invokeMethod(m, expected);
} else {
throw new IllegalArgumentException(createErrorMsg(wrapper, propertyName));
}
} else {
throw new IllegalArgumentException(createErrorMsg(wrapper, propertyName));
}
}
return result;
}
private static String createErrorMsg(BeanWrapper wrapper, String propertyName) {
return propertyName + " can not be read on: " + wrapper.getWrappedClass();
}
private static <T> BeanWrapper getWrapper(T instance) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(instance);
wrapper.setAutoGrowNestedPaths(true);
return wrapper;
}