2

short question: I have the situation that I want to compare value objects in my JUnit tests. Those value objects only have a few fields of different types (but mostly primitive types). I create one object from an xml file and the other from a dataset from my database.

I solved it by overriding the equals method in my value object class and then compare those two by assertEquals(o1, o2). I want to know if there is another solution for this task. Maybe a solution with which I don't have to write equals methods for every value class (there are a few...)

I already experimented with Hamcrest, but not really successful. I tried assertThat(o2, is(equalTo(o1))); If the object is equal, the JUnit test is successful, but if not, the test is not failing but exits with an exception (don't think that's how it should work, right?)

I also thought of something with reflection to automatically compare all fields of the classes, but I don't know how to start.

Do you have a suggestion how something like this is solved the most elegant way?

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
  • 1
    equals is the right way, or you could compare individual fields of those objects using getters. – SMA Jan 06 '15 at 09:06
  • 1
    This SO post might contain some solutions: http://stackoverflow.com/q/12147297/1291150 – Bohuslav Burghardt Jan 06 '15 at 09:06
  • 1
    Value objects should always override the equals method, because to say an object is a value object is to say that there can exist a different object that is equivalent to this object, and the equals method indicates which other objects are equivalent. – Raedwald Jan 06 '15 at 09:09

3 Answers3

2

You actually mentioned the 3 most popular techniques: 1. implement "good" equals method and use it from unit test as well. 2. use series of assertEquals() 3. Use assertThat()

I personally used all these techniques found all of them pretty useful; the choice depends on concrete requirements. Sometimes I created comaprison utility class that contained series of assert methods for my value objects. This helps to re-use assertion code in different unit tests.

According to my experience it is good idea to implement good equals(), hashCode() and toString() for all value objects. This is not too difficult. You can use EqualsBuilder, HashCodeBuilder and ToSringBuilder from Apache commons or utilities of Object introduced in java 7. This is up to you.

If performance is not an issue (IMHO correct for 99.999% of real applications) use reflection based builders (from apache commons). This makes the implementations very simple and maintenance-less.

In most cases using equals() in unit test is good enough. However if it is not good, use assertThat() or seriece of assertEquals() according to your choice.

AlexR
  • 114,158
  • 16
  • 130
  • 208
  • Thanks for the answers guys, I think the EqualsBuilder is exactly what I was looking for =) Problem solved, and thanks for the additional explanations. – Matthias Nicklisch Jan 06 '15 at 09:20
0

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;
}
Fried Hoeben
  • 3,247
  • 16
  • 14
0

The library Hamcrest 1.3 Utility Matchers has a special matcher that uses reflection instead of equals.

assertThat(obj1, reflectEquals(obj2));
Stefan Birkner
  • 24,059
  • 12
  • 57
  • 72