在JUnit测试中检查深度相等

问题描述

| 我正在为克隆,序列化和/或写入XML文件的对象编写单元测试。在这三种情况下,我都想验证生成的对象是否与原始对象“相同”。我在方法中经历了几次迭代,发现所有迭代都存在问题,想知道其他人做了什么。 我的第一个想法是在所有类中手动实现equals方法,并使用assertEquals。在确定覆盖等于对可变对象执行深度比较是一件坏事之后,我放弃了这种方法,因为您几乎总是希望集合对包含的可变对象使用引用相等性[1]。 然后我想我可以将方法重命名为contentEquals或其他名称。但是,经过深思熟虑之后,我意识到这并不能帮助我找到想要的回归。如果程序员添加一个新的(可变)字段,却忘记了将其添加到clone方法中,那么他可能也将忘记将其添加到contentEquals方法中,而我正在编写的所有这些回归测试都将一文不值。 然后,我编写了一个漂亮的assertContentEquals函数,该函数使用反射来检查对象的所有(非瞬态)成员的值,并在必要时进行递归检查。这避免了上面的手动比较方法的问题,因为它认情况下假定必须保留所有字段并且程序员必须显式声明要跳过的字段。但是,在某些合理的情况下,克隆后某个字段实际上不应该是相同的[2]。我输入了一个额外的参数toassertContentEquals,该参数列出了要忽略的字段,但是由于此列表是在单元测试中声明的,因此在进行递归检查时,它会变得非常丑陋。 因此,我现在正在考虑回到在每个要测试的类中包含一个contentEquals方法,但是这次使用类似于上述assertContentsEquals的辅助函数来实现。这样,当递归操作时,豁免将在每个单独的类中定义。 任何意见?您过去如何处理此问题? 编辑以阐述我的想法: [1]我从本文中得出了不覆盖可变类上的均等值的理由。一旦将可变对象粘贴到Set / Map中,如果字段更改,则其哈希值将更改,但其存储桶将不会更改,从而破坏了事物。因此,这些选项是不要在可变对象上覆盖equals / getHash,或者具有一旦将可变对象放入集合后就永远不要更改其策略。 我没有提到我正在现有的代码库上实现这些回归测试。在这种情况下,更改equals的定义,然后必须查找所有可能更改软件行为的实例的想法使我感到恐惧。我觉得我可以轻松解决更多问题。 [2]我们代码库中的一个示例是图形结构,其中每个节点在最终写入XML时需要一个唯一的标识符来链接节点XML。当我们克隆这些对象时,我们希望标识符不同,但是其他所有东西都保持不变。经过更多的思考之后,似乎问题“这个对象是否已经在这个集合中”和“这些对象是否定义相同”,在这种情况下使用根本不同的相等概念。第一个询问身份,如果要进行深度比较,我希望包含ID,第二个询问相似性,我不希望包含ID。这使我更倾向于实施equals方法。 你们是否同意这个决定,还是您认为实现平等是更好的选择?     

解决方法

我将使用反射方法,并使用RetentionPolicy.RUNTIME定义自定义Annotation,以允许测试类的实现者标记在克隆后可能会更改的字段。然后,您可以使用反射检查批注,并跳过标记的字段。 这样,您可以使测试代码保持通用和简单,并具有直接标记代码中异常的便捷方法,而不会影响需要测试的代码的设计或运行时行为。 注释可能如下所示:
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ChangesOnClone
{
}
这是如何在要测试的代码中使用它的方法:
class ABC
{
     private String name;

     @ChangesOnClone
     private Cache cache;
}
最后是测试代码的相关部分:
for ( Field field : fields )
{
    if( field.getAnnotation( ChangesOnClone.class ) )
        continue;
    // else test it
}
    

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...