检查JUnit测试中的深度相等性

时间:2011-05-19 22:19:38

标签: java junit equality

我正在为克隆,序列化和/或写入XML文件的对象编写单元测试。在所有三种情况下,我想验证结果对象是否与原始对象“相同”。我在我的方法中经历了几次迭代,并且发现了所有人的错误,想知道其他人做了什么。

我的第一个想法是在所有类中手动实现equals方法,并使用assertEquals。在决定覆盖equals以对可变对象执行深度比较之后,我放弃了这种方法是一件坏事,因为你几乎总是希望集合对它们包含的可变对象使用引用相等[1]。

然后我想我可以将方法重命名为contentEquals或其他东西。然而,经过更多的思考,我意识到这无助于我找到我正在寻找的那种回归。如果程序员添加了一个新的(可变的)字段,并忘记将它添加到克隆方法中,那么他可能会忘记将它添加到contentEquals方法中,而我写的所有这些回归测试都将毫无价值。

然后我写了一个漂亮的assertContentEquals函数,该函数使用反射来检查对象的所有(非瞬态)成员的值,必要时递归。这避免了上面的手动比较方法的问题,因为它默认假定必须保留所有字段并且程序员必须显式声明要跳过的字段。但是,有一些合法的情况,克隆后字段确实不应该相同[2]。我输入了一个额外的参数toassertContentEquals,它列出了要忽略的字段,但由于这个列表是在单元测试中声明的,所以在递归检查的情况下它真的很难实现。

所以我现在想回到在每个被测试的类中包含一个contentEquals方法,但这次使用类似于上面描述的assertContentsEquals的辅助函数实现。这种方式在递归操作时,将在每个单独的类中定义豁免。

有何评论?你过去如何处理这个问题?

编辑阐述我的想法:

[1]我从这个article获得了可变类的不等于等于的理性。一旦你将一个可变对象粘贴在一个Set / Map中,如果一个字段发生变化,那么它的散列会改变但是它的存储桶不会改变它。因此,选项是不对可变对象重写equals / getHash,或者具有一个策略,一旦将可变对象放入集合中,就永远不会更改它。

我没有提到我正在现有的代码库上实现这些回归测试。在这种情况下,改变equals的定义,然后必须找到它可以改变软件行为的所有实例的想法吓坏了我。我觉得我可以轻松打破比我修复更多的东西。

[2]我们的代码库中的一个例子是图结构,其中每个节点都需要一个唯一的标识符,用于在最终写入XML时链接节点XML。当我们克隆这些对象时,我们希望标识符不同,但其他所有内容保持不变。在对它进行反复思考后,似乎问题“这个对象已经存在于此集合中”和“这些对象是否定义相同”,在这种情况下使用基本不同的相等概念。第一个是询问身份,如果进行深度比较,我希望包括ID,而第二个是询问相似性,我不希望包含ID。这使我更倾向于实施equals方法。

你们是否同意这个决定,或者你认为实施平等是更好的方式?

1 个答案:

答案 0 :(得分:5)

我会使用反射方法并使用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
}