2011-05-19 17 views
14

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

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

然后我想我可以重新命名方法contentEquals什么的。但是,经过深思熟虑之后,我意识到这并不能帮助我找到我期待的那种回归。如果一个程序员添加一个新的(可变的)字段,并忘记将其添加到克隆方法中,那么他可能会忘记将它添加到contentEquals方法中,而我写的所有这些回归测试都将毫无价值。

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

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

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

编辑来阐述我的想法:

[1]我得到了理性不等于压倒一切从这个article可变类。一旦将可变对象粘贴在Set/Map中,如果一个字段发生变化,那么它的哈希值将会改变,但其桶不会变化,从而破坏事物。因此,选项不会覆盖可变对象上的equals/getHash,或者在将可变对象放入集合后不会改变它。

我没有提到我正在对现有代码库执行这些回归测试。在这种情况下,改变equals的定义,然后必须找到所有可能改变软件行为的实例,这些想法会让我感到害怕。我觉得我可以轻松地打破,而不是我修好。

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

。你们这个决定不服的,或者你认为实现平等是更好的方式去?

+0

我决定重写等于在可变对象上执行深度比较后放弃了这种方法是一个坏事情,因为你几乎总是希望集合为它们包含的可变对象使用引用相等。 这不是真的 - 所有可变集合类都会覆盖相等。尽管个人课程的责任是重写equals方法。 – kuriouscoder 2011-05-19 22:31:41

+1

只是顺便说一句,你可能想看看org.apache.commons.lang.builder.EqualsBuilder#reflectionEquals,听起来非常相似,你与你的contentEquals方法来实现的。 – artbristol 2011-05-19 22:40:09

+1

@ artbristol的建议是一个很好的,并与EqualsBuilder您可以指定哪些字段应在equals方法被跳过。我已经在我们的项目中实现了这一点,它已经抓住了一些东西。有一点需要注意。在我写的一个快速性能测试中,我比较了equals实现,reflectionEquals只要pojomatic需要4倍,而只要eclipse生成equals方法就需要25倍。你也可以使用EqualsBuilder.append,它在pojomatic和eclipse之间生成。 – digitaljoel 2011-05-19 22:49:11

回答

5

我会去的思考方法,并定义与RetentionPolicy.RUNTIME一个自定义的注释,让测试类的实施者,以纪念那些有望克隆后更改的字段。然后,您可以使用反射来检查注释并跳过标记的字段。

这种方式可以让你的测试代码的通用和简单,有一个方便的手段来标记直接在代码中的异常,而不会影响需要进行测试的代码设计或运行时行为。

注释看起来是这样的:

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 
} 
+0

如果您正在使用反射走,我绝对不会实现自己的版本,因为已经有能力,测试的实施有像EqualsBuilder和Pojomatic。 – digitaljoel 2011-05-19 22:49:53

+1

使用注释是一个非常好的主意。它还将让我处理克隆“平等”和序列化“平等”不同的情况。 – pavon 2011-05-20 16:01:36

+0

是的,注释方法的+1。 – 2011-05-20 17:08:24