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 中的 EqualsBuilder
、HashCodeBuilder
和 ToSringBuilder
或 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));
简短的问题:我想在我的 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 中的 EqualsBuilder
、HashCodeBuilder
和 ToSringBuilder
或 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));