在 JUnit 测试中检查深度相等

2022-09-04 04:32:17

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

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

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

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

因此,我现在正在考虑在测试的每个类中包含 contentEquals 方法,但这次使用类似于上面描述的 assertContentsEquals 的帮助器函数实现。这样,当递归操作时,将在每个单独的类别中定义豁免。

任何意见?您过去是如何处理这个问题的?

编辑以阐述我的想法:

[1]我从本文中得到了不覆盖可变类的等式的合理。一旦你将一个可变对象粘贴到 Set/Map 中,如果一个字段发生了变化,那么它的哈希值就会改变,但它的桶不会改变,从而破坏一切。因此,选项是不覆盖可变对象上的 equals/getHash,或者具有在将可变对象放入集合后从不更改可变对象的策略。

我没有提到我正在现有代码库上实现这些回归测试。在这种情况下,改变等式的定义,然后必须找到所有可以改变软件行为的实例的想法让我感到恐惧。我觉得我可以很容易地打破比我修复的更多。

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

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


答案 1

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

答案 2

AssertJ提供了一个递归比较函数:

assertThat(new).usingRecursiveComparison().isEqualTo(old);

有关详细信息,请参阅 AssertJ 文档:https://assertj.github.io/doc/#basic-usage

使用 AssertJ 的先决条件:

进口:

import static org.assertj.core.api.Assertions.*;

maven dependency:

    <!-- test -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.19.0</version>
        <scope>test</scope>
    </dependency>