如何在测试中根据它们的字段值“深入”比较两个未实现 equals 方法的对象?
原始问题(因缺乏精确性而关闭,因此不符合 SO 标准),保留用于文档目的:
我正在尝试为clone()
大型项目中的各种操作编写单元测试,我想知道某处是否有一个现有的类能够获取相同类型的两个对象,进行深入比较,并说明它们是否'是否相同?
如何在测试中根据它们的字段值“深入”比较两个未实现 equals 方法的对象?
原始问题(因缺乏精确性而关闭,因此不符合 SO 标准),保留用于文档目的:
我正在尝试为clone()
大型项目中的各种操作编写单元测试,我想知道某处是否有一个现有的类能够获取相同类型的两个对象,进行深入比较,并说明它们是否'是否相同?
Unitils有这个功能:
通过反射进行相等断言,具有不同的选项,例如忽略 Java 默认/空值和忽略集合的顺序
我喜欢这个问题!主要是因为它几乎没有被回答或回答得不好。好像还没有人想通。处女地:)
首先,不要考虑使用equals
. 的契约equals
,如 javadoc 中所定义,是等价关系(自反、对称和传递),而不是等价关系。为此,它还必须是反对称的。唯一的实现equals
是(或永远可能是)真正的相等关系是java.lang.Object
. 即使您确实使用equals
了比较图表中的所有内容,违约的风险也相当高。正如 Josh Bloch 在Effective Java中指出的,equals 的契约很容易被打破:
“根本没有办法扩展一个可实例化的类并添加一个方面,同时保留平等合同”
除了布尔方法对你有什么好处吗?将原版和克隆版之间的所有差异真正封装起来会很好,你不觉得吗?此外,我在这里假设您不想为图表中的每个对象编写/维护比较代码而烦恼,而是您正在寻找随着源代码随时间变化而扩展的东西。
Soooo,您真正想要的是某种状态比较工具。该工具的实现方式实际上取决于您的域模型的性质和您的性能限制。根据我的经验,没有通用的灵丹妙药。并且在大量迭代中它会很慢。但是为了测试克隆操作的完整性,它会很好地完成这项工作。您的两个最佳选择是序列化和反射。
你会遇到的一些问题:
XStream 非常快,结合 XMLUnit 只需几行代码即可完成这项工作。XMLUnit 很不错,因为它可以报告所有差异,或者只在找到的第一个差异处停止。它的输出包括不同节点的 xpath,这很好。默认情况下,它不允许无序集合,但可以配置为这样做。注入一个特殊的差异处理程序(称为 a DifferenceListener
)允许您指定处理差异的方式,包括忽略顺序。但是,一旦您想做任何超出最简单定制的事情,编写起来就会变得很困难,而且细节往往与特定的域对象相关联。
我个人的偏好是使用反射循环遍历所有声明的字段并深入研究每个字段,随时跟踪差异。警告词:除非您喜欢堆栈溢出异常,否则不要使用递归。使用堆栈将事物保持在范围内(使用LinkedList
或者其他的东西)。我通常会忽略瞬态和静态字段,并且会跳过已经比较过的对象对,因此如果有人决定编写自引用代码,我不会陷入无限循环(但是,无论如何,我总是比较原始包装器,因为经常重复使用相同的对象引用)。您可以预先配置以忽略集合排序并忽略特殊类型或字段,但我喜欢通过注释在字段本身上定义我的状态比较策略。恕我直言,这正是注释的目的,使关于类的元数据在运行时可用。就像是:
@StatePolicy(unordered=true, ignore=false, exactTypesOnly=true)
private List<StringyThing> _mylist;
我认为这实际上是一个非常困难的问题,但完全可以解决!一旦你有适合你的东西,它真的,真的,很方便:)
所以,祝你好运。如果你想出一些纯粹的天才,别忘了分享!
请参阅 java-util 中的 DeepEquals 和 DeepHashCode():https ://github.com/jdereg/java-util
这个类完全符合原作者的要求。
在AssertJ中,您可以执行以下操作:
Assertions.assertThat(expectedObject).isEqualToComparingFieldByFieldRecursively(actualObject);
可能它不会在所有情况下都有效,但是它会在您认为的更多情况下有效。
这是文档中的内容:
根据属性/字段的递归比较(包括继承的),断言被测对象(实际)等于给定对象。如果实际的 equals 实现不适合您,这可能很有用。递归属性/字段比较不适用于具有自定义 equals 实现的字段,即将使用重写的 equals 方法而不是逐个字段比较。
递归比较处理循环。默认情况下,浮点数与 1.0E-6 的精度进行比较,并与 1.0E-15 加倍。
您可以为每个(嵌套)字段或类型指定自定义比较器,分别使用 usingComparatorForFields(Comparator, String...) 和 usingComparatorForType(Comparator, Class)。
要比较的对象可以是不同的类型,但必须具有相同的属性/字段。例如,如果实际对象有一个名称字符串字段,则预计另一个对象也有一个。如果对象具有同名的字段和属性,则属性值将用于字段。
您可以使用EqualsBuilder.reflectionEquals()简单地覆盖类的equals()方法,如下所述:
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
只需要实现 Hibernate Envers 修改的两个实体实例的比较。我开始编写自己的不同之处,但后来发现了以下框架。
https://github.com/SQiShER/java-object-diff
您可以比较相同类型的两个对象,它会显示更改、添加和删除。如果没有变化,那么对象是相等的(理论上)。为在检查期间应忽略的 getter 提供了注释。该框架具有比相等检查更广泛的应用,即我用来生成更改日志。
它的性能还可以,在比较 JPA 实体时,一定要先将它们从实体管理器中分离出来。
我正在使用 XStream:
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
XStream xstream = new XStream();
String oxml = xstream.toXML(o);
String myxml = xstream.toXML(this);
return myxml.equals(oxml);
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
XStream xstream = new XStream();
String myxml = xstream.toXML(this);
return myxml.hashCode();
}
http://www.unitils.org/tutorial-reflectionassert.html
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
}
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);
Hamcrest 有 Matcher samePropertyValuesAs。但它依赖于 JavaBeans 约定(使用 getter 和 setter)。如果要比较的对象没有属性的 getter 和 setter,这将不起作用。
import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class UserTest {
@Test
public void asfd() {
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertThat(user1, samePropertyValuesAs(user2)); // all good
user2 = new User(1, "John", "Do");
assertThat(user1, samePropertyValuesAs(user2)); // will fail
}
}
用户 bean - 带有 getter 和 setter
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
}
如果你的对象实现了 Serializable 你可以使用这个:
public static boolean deepCompare(Object o1, Object o2) {
try {
ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
oos1.writeObject(o1);
oos1.close();
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
oos2.writeObject(o2);
oos2.close();
return Arrays.equals(baos1.toByteArray(), baos2.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
您的链接列表示例并不难处理。当代码遍历两个对象图时,它将访问过的对象放置在 Set 或 Map 中。在遍历到另一个对象引用之前,测试该集合以查看该对象是否已经被遍历。如果是这样,就没有必要再进一步了。
我同意上面所说的使用 LinkedList 的人(像 Stack 但没有同步方法,所以它更快)。使用堆栈遍历对象图,同时使用反射来获取每个字段,是理想的解决方案。编写一次,这个“外部”equals() 和“外部”hashCode() 是所有equals() 和hashCode() 方法都应该调用的。您再也不需要客户的 equals() 方法。
我编写了一些遍历完整对象图的代码,列在 Google Code 中。请参阅 json-io (http://code.google.com/p/json-io/)。它将 Java 对象图序列化为 JSON 并从中反序列化。它处理所有 Java 对象,有或没有公共构造函数、可序列化或不可序列化等。相同的遍历代码将成为外部“equals()”和外部“hashcode()”实现的基础。顺便说一句,JsonReader / JsonWriter (json-io) 通常比内置的 ObjectInputStream / ObjectOutputStream 更快。
这个 JsonReader / JsonWriter 可以用于比较,但它对哈希码没有帮助。如果你想要一个通用的 hashcode() 和 equals(),它需要它自己的代码。我也许可以用一个通用的图表访问者来解决这个问题。走着瞧。
其他注意事项 - 静态字段 - 这很容易 - 它们可以被跳过,因为所有 equals() 实例对于静态字段将具有相同的值,因为静态字段在所有实例之间共享。
至于瞬态字段 - 这将是一个可选选项。有时您可能希望瞬态不计入其他时间。“有时你觉得自己像个疯子,有时你不会。”
检查回 json-io 项目(对于我的其他项目),您将找到外部 equals() / hashcode() 项目。我还没有它的名字,但它会很明显。
我认为受Ray Hulha 解决方案启发的最简单的解决方案是序列化对象,然后深入比较原始结果。
序列化可以是字节、json、xml 或简单的 toString 等。 ToString 似乎更便宜。Lombok 为我们生成免费且易于定制的 ToSTring。请参见下面的示例。
@ToString @Getter @Setter
class foo{
boolean foo1;
String foo2;
public boolean deepCompare(Object other) { //for cohesiveness
return other != null && this.toString().equals(other.toString());
}
}
我想你知道这一点,但理论上,你应该总是覆盖 .equals 来断言两个对象是真正相等的。这意味着他们会检查其成员上被覆盖的 .equals 方法。
这种事情就是为什么在 Object 中定义 .equals 的原因。
如果始终如一地这样做,您将不会有问题。
对这种深度比较的停止保证可能是一个问题。下面应该怎么做?(如果你实现了这样一个比较器,这将是一个很好的单元测试。)
LinkedListNode a = new LinkedListNode();
a.next = a;
LinkedListNode b = new LinkedListNode();
b.next = b;
System.out.println(DeepCompare(a, b));
这是另一个:
LinkedListNode c = new LinkedListNode();
LinkedListNode d = new LinkedListNode();
c.next = d;
d.next = c;
System.out.println(DeepCompare(c, d));
Apache 为您提供了一些东西,将两个对象都转换为字符串并比较字符串,但您必须覆盖 toString()
obj1.toString().equals(obj2.toString())
覆盖 toString()
如果所有字段都是原始类型:
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this);}
如果您有非原始字段和/或集合和/或映射:
// Within class
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this,new
MultipleRecursiveToStringStyle());}
// New class extended from Apache ToStringStyle
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;
public class MultipleRecursiveToStringStyle extends ToStringStyle {
private static final int INFINITE_DEPTH = -1;
private int maxDepth;
private int depth;
public MultipleRecursiveToStringStyle() {
this(INFINITE_DEPTH);
}
public MultipleRecursiveToStringStyle(int maxDepth) {
setUseShortClassName(true);
setUseIdentityHashCode(false);
this.maxDepth = maxDepth;
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName,
Collection<?> coll) {
for(Object value: coll){
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, ?> map) {
for(Map.Entry<?,?> kvEntry: map.entrySet()){
Object value = kvEntry.getKey();
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
value = kvEntry.getValue();
if (value.getClass().getName().startsWith("java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}}