【问题标题】:Why am I getting an AssertionError when the expected and actual look identical?当预期和实际看起来相同时,为什么我会收到 AssertionError?
【发布时间】:2016-12-13 09:53:15
【问题描述】:

我正在尝试回答 Cracking the Coding Interview 中的以下问题。下面的代码是 GitHub 上一个项目的一部分,here

给定一棵二叉搜索树,设计一个算法,在每个深度创建一个所有节点的链表(即,如果你有一棵深度为 D 的树,你将有 D 个链表)。

作为一名优秀的小开发人员,我编写了一个单元测试来检查这一点。

@Test
public void testGetValuesAtEachLevel() {
    Integer[] treeValues = {
            1, 2, 3, 4, 5,
            6, 7, 8, 9, 10,
            11, 12, 13, 14, 15
    };

    tree = new GenericBinaryTree<>(treeValues);

    Integer[][] expectedArray = {
            { 1 },
            { 2, 3 },
            { 4, 5, 6, 7 },
            { 8, 9, 10, 11, 12, 13, 14, 15 }
    };

    List<List<Node<Integer>>> expectedList = new ArrayList<>(4);
    for (Integer[] level : expectedArray) {
        List<Node<Integer>> list = new LinkedList<>();
        for (Integer value : level) {
            list.add(new Node<>(value));
        }
        expectedList.add(list);
    }

    assertEquals(expectedList, tree.getValuesAtEachLevel());
}

这是代码。

List<List<Node<T>>> getValuesAtEachLevel() {
    List<List<Node<T>>> results = new ArrayList<>();

    List<Node<T>> firstLevel = new LinkedList<>();
    firstLevel.add(getRoot());
    results.add(firstLevel);

    loadAtLevel(results, 1);

    return results;
}

private void loadAtLevel(List<List<Node<T>>> list, int level) {
    List<Node<T>> levelList = new LinkedList<Node<T>>();

    for (Node<T> node : list.get(level - 1)) {
        if (node.left() != null) levelList.add(node.left());
        if (node.right() != null) levelList.add(node.right());
    }

    if (levelList.isEmpty()) return;

    level++;
    list.add(levelList);
    loadAtLevel(list, level);
}

想象一下当单元测试失败并出现以下错误时我的惊讶:

java.lang.AssertionError: expected: 
java.util.ArrayList<[[1], [2, 3], [4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]]> but was: 
java.util.ArrayList<[[1], [2, 3], [4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]]>

字符串表示是相同的,所以我无法弄清楚这里发生了什么。事实上,如果我将断言更改为此,测试通过:

    assertEquals(expectedList.toString(), tree.getValuesAtEachLevel().toString());

我感觉我即将学习有关接口和对象的非常有价值的东西。我在这里做错了什么?

【问题讨论】:

  • 可能数字的类型不同(int vs long)
  • 你还没有展示你的 Node 类 - 我猜它不会覆盖 equals/hashCode。
  • ^^ 乔恩说的。如果与之比较的对象也是一个长度相同的列表并且所有条目的 equals 返回 true,则 ArrayList#equals 将返回 true。因此,除非Node 定义equals,否则具有不同但等效Node 实例的两个列表将不相等。
  • 这里是Node的代码,实际上它没有定义equalshashCodegithub.com/dishbreak/cracking-coding/blob/…

标签: java collections junit


【解决方案1】:

正如@Jon Skeet 在评论中指出的那样,您需要为您的Node 类覆盖equalshashCode,以对其实例执行比较(equals)。

一个样本(使用 intelliJ 默认自动生成)可以是 -

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Node<?> node = (Node<?>) o;

    if (left != null ? !left.equals(node.left) : node.left != null) return false;
    if (right != null ? !right.equals(node.right) : node.right != null) return false;
    if (parent != null ? !parent.equals(node.parent) : node.parent != null) return false;
    return value != null ? value.equals(node.value) : node.value == null;

}

@Override
public int hashCode() {
    int result = left != null ? left.hashCode() : 0;
    result = 31 * result + (right != null ? right.hashCode() : 0);
    result = 31 * result + (parent != null ? parent.hashCode() : 0);
    result = 31 * result + (value != null ? value.hashCode() : 0);
    return result;
}

另一方面,为什么

assertEquals(expectedList.toString(),tree.getValuesAtEachLevel().toString());

有效。

这是因为您最终在这里要断言的是比较两个 String 实例,它们的 @Overriden equals 定义 here

【讨论】:

    【解决方案2】:

    很可能是因为

    new Node().equals(new Node()) == false
    

    Node 中覆盖equals()hashCode()

    【讨论】:

    • 正确。如果没有任何参数为空,assertEquals 最终会调用 expected.equals(actual)。 ArrayList 的 equals() 按顺序比较元素,因此如果它不起作用,可能是 Node 没有正确的 equals 实现,因此它获取 Object 提供的默认(身份)equals。 Node 类需要实现 equals(和 hashCode),其中包括它自己的值和所有子项的值(如果这些在数组列表中,您只需在子列表上执行 equals)。
    【解决方案3】:

    断言两个对象的字符串表示相等并且这些对象相等是两个不同的事情。
    对象相等依赖于Object 类中的boolean equals(Object o) 方法。
    int hashcode() 如果以对称方式覆盖equals(Object o) 方法而不是在任何情况下返回唯一值(默认情况下),则可以提高性能) 但对于断言,它不会改变结果。一个好的做法是在覆盖 equals() 时覆盖两者。

    在您的情况下,您有多种处理方式。我给你推荐3种常用方法:

    • 覆盖equals()hashcode() 它的属性允许以定义对象身份的方式定义它们。覆盖 equals()hashcode() 以在单元测试中成功断言不足以覆盖该方法。
      此外,equals() 覆盖可能适合这个测试,而不适合另一个测试。
      equals()hashcode() 方法的实现必须符合识别对象的方式以及在应用程序中使用此类的方式,而不仅仅是单元测试断言。
      这是另一个提问的帖子:Should one override equals method for asserting the object equality in a unit test?

    • 在循环中执行断言。

    例如,循环实际结果并使用您为预期结果创建的二维数组逐个节点进行断言:

    int i=0;
    
    for (List<Node<Integer>>> listOfNodes :  tree.getValuesAtEachLevel()) {
    
        int j=0;
    
        for (Node node : listOfNodes ) {
            list.add(new Node<>(value));
           // I called the method getValue() because I don't know the getter
            Assert.assertEquals(expectedArray[i][j], node.getValue(); 
             // You could also display a JUnit failure message if you want
            j++; 
        }
    
        i++;
    }
    

    如果您必须在其他几个场景中为您的 List 列表进行断言并且进行断言的方式非常不同,那么这种处理方式可能会很笨拙且耗时。但是通过适当的代码重构,我们可以在许多场景中以可重用的方式提取 util 断言方法。

    • 我非常喜欢的另一种解决方案是对断言的反射。

    Unitils 库提供此功能和其他功能。

    http://www.unitils.org/tutorial-reflectionassert.html

    使用 Unitils,您可以:

    ReflectionAssert.assertReflectionEquals(expectedList, tree.getValuesAtEachLevel());
    

    不覆盖 equals(Object o)hashcode()
    Unitils 的另一个优点是,如果断言失败,它会提供相当友好的消息。

    【讨论】:

      猜你喜欢
      • 2014-09-19
      • 2022-07-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-15
      • 2019-07-24
      • 1970-01-01
      • 2021-07-03
      • 1970-01-01
      相关资源
      最近更新 更多