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