JUnit - 比较值对象

时间:2015-01-06 09:01:55

标签: java junit hamcrest

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

我通过覆盖我的值对象类中的equals方法解决了它,然后通过assertEquals(o1,o2)比较这两个方法。 我想知道是否有另一种解决方案可以完成这项任务。也许我不需要为每个值类编写等于方法的解决方案(有一些......)

我已经尝试过Hamcrest,但并没有真正成功。我试过断言(o2,是(equalTo(o1))); 如果对象相等,则JUnit测试成功,但如果没有,则测试不会失败,但会以异常退出(不要认为它应该如何工作,对吗?)

我还想过用反射来自动比较类的所有字段,但我不知道如何开始。

你有没有建议如何以最优雅的方式解决这个问题?

3 个答案:

答案 0 :(得分:1)

你实际上提到了3种最流行的技巧: 1.实现“好”等于方法并从单元测试中使用它。 2.使用assertEquals()系列 3.使用assertThat()

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

根据我的经验,为所有价值对象实施好equals()hashCode()toString()是个好主意。这不是太难。您可以使用来自Java公共中引入的EqualsBuilder的Apache公共或实用程序的HashCodeBuilderToSringBuilderObject。这取决于您。

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

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

答案 1 :(得分:0)

就像AlexR的回答所说的那样,实现良好的equals()hashCode()toString()方法是一个好主意,如果性能不是最重要的关注点,那么Apache公共有很好的帮助(对于单元测试,它不是)。

我过去有类似的测试要求,我不仅希望听到 值对象不同,而且还看到哪些属性不同(和不只是第一个而是全部)。我创建了一个帮助器(使用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;
}

答案 2 :(得分:0)

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

assertThat(obj1, reflectEquals(obj2));