JUnit - 比较值对象

JUnit - Compare Value Objects

简短的问题:我想在我的 JUnit 测试中比较值对象。这些值对象只有几个不同类型的字段(但主要是原始类型)。我从 xml 文件创建一个对象,从我的数据库中的数据集创建另一个对象。

我通过覆盖值对象 class 中的 equals 方法解决了这个问题,然后通过 assertEquals(o1, o2) 比较这两个方法。 我想知道这个任务是否有另一种解决方案。也许是一个解决方案,我不必为每个值 class(有几个...)

编写 equals 方法

我已经尝试过 Hamcrest,但不是很成功。我试过 assertThat(o2, is(equalTo(o1))); 如果对象相等,则 JUnit 测试成功,但如果不相等,则测试不是失败而是异常退出(不要认为它应该这样工作,对吧?)

我也想到了用反射自动比较classes的所有字段的东西,但是不知道怎么下手

你有什么建议可以用最优雅的方式解决吗?

您实际上提到了 3 种最流行的技术: 1. 实施 "good" equals 方法并在单元测试中使用它。 2. 使用 assertEquals() 系列 3.使用assertThat()

我亲自使用了所有这些技术,发现它们都很有用;选择取决于具体要求。有时我创建了 comaprison 实用程序 class,其中包含用于我的值对象的一系列 assert 方法。这有助于在不同的单元测试中重用断言代码。

根据我的经验,为所有值对象实现良好的 equals()hashCode()toString() 是个好主意。这并不太难。您可以使用 Apache Commons 中的 EqualsBuilderHashCodeBuilderToSringBuilder 或 java 7 中引入的 Object 实用程序。这取决于您。

如果性能不是问题(恕我直言,99.999% 的实际应用程序都是正确的)使用基于反射的构建器(来自 apache commons)。这使得实现非常简单且无需维护。

在大多数情况下,在单元测试中使用 equals() 就足够了。但是,如果不好,请根据您的选择使用 assertThat()assertEquals() 系列。

就像 AlexR 的回答指出的那样,实现好的 equals()hashCode()toString() 方法是个好主意,如果性能不是最好的,Apache commons 有很好的帮助重要的关注点(对于单元测试而言不是)。

我过去有过类似的测试要求,我不仅想听到 值对象不同,而且还想知道 哪些属性是different(不仅是第一个,而且是所有)。我创建了一个助手(使用 Spring 的 BeanWrapper)来做到这一点,它在以下位置可用:https://bitbucket.org/fhoeben/hsac-test 并允许您在单元测试中调用 UnitTestHelper.assertEqualsWithDiff(T expected, T actual)

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

Hamcrest 1.3 Utility Matchers 有一个特殊的匹配器,它使用反射而不是 equals。

assertThat(obj1, reflectEquals(obj2));