一个单元应该如何测试hashCode-equals合约?

时间:2008-10-09 17:29:04

标签: java unit-testing oop

简而言之,hashCode契约,根据Java的object.hashCode():

  1. 除非影响equals()的更改
  2. ,否则哈希码不应更改
  3. equals()表示哈希码为==
  4. 让我们主要关注不可变数据对象 - 它们的信息在构造之后永远不会改变,所以假定#1成立。留下#2:问题只是确认等于隐含代码==。

    显然,我们无法测试每个可想到的数据对象,除非该集合很小。那么,编写可能会遇到常见情况的单元测试的最佳方法是什么?

    由于此类的实例是不可变的,因此构造此类对象的方法有限;如果可能的话,这个单元测试应该涵盖所有这些。在我的脑海中,入口点是构造函数,反序列化和子类的构造函数(应该可以简化为构造函数调用问题)。

    [我打算通过研究来回答我自己的问题。其他StackOverflowers的输入是这个过程的一个受欢迎的安全机制。]

    [这可能适用于其他OO语言,因此我正在添加该标记。]

8 个答案:

答案 0 :(得分:69)

EqualsVerifier是一个相对较新的开源项目,它在测试equals合同方面做得非常好。它没有来自GSBase的issues EqualsTester。我肯定会推荐它。

答案 1 :(得分:11)

我的建议是考虑为什么/如何不成立,然后写一些针对这些情况的单元测试。

例如,假设您有一个自定义Set类。如果两个集合包含相同的元素,则它们是相等的,但如果这些元素以不同的顺序存储,则两个相等集合的基础数据结构可能不同。例如:

MySet s1 = new MySet( new String[]{"Hello", "World"} );
MySet s2 = new MySet( new String[]{"World", "Hello"} );
assertEquals(s1, s2);
assertTrue( s1.hashCode()==s2.hashCode() );

在这种情况下,集合中元素的顺序可能会影响它们的散列,具体取决于您实现的散列算法。所以这就是我要编写的那种测试,因为它测试的情况我知道某些散列算法可能会为我定义的两个对象产生不同的结果。

您应该使用与您自己的自定义类相似的标准,无论是什么。

答案 2 :(得分:6)

值得使用junit插件。查看类EqualsHashCodeTestCase http://junit-addons.sourceforge.net/你可以扩展它并实现createInstance和createNotEqualInstance,这将检查equals和hashCode方法是否正确。

答案 3 :(得分:4)

我会推荐GSBase的EqualsTester。它基本上是你想要的。我有两个(小的)问题:

  • 构造函数完成所有工作,我认为这不是一个好习惯。
  • 当A类的实例等于A类的子类的实例时失败。这不一定违反了equals合同。

答案 4 :(得分:2)

[在撰写本文时,还发布了其他三个答案。]

重申一下,我的问题的目的是找到标准的测试案例,以确认hashCodeequals是否达成一致。我对这个问题的处理方法是想象程序员在编写有问题的类时所采用的通用路径,即不可变数据。例如:

  1. 在不编写equals()的情况下写了hashCode()这通常意味着将等式定义为两个实例的字段相等。
  2. 写了hashCode()而没有写equals()这可能意味着程序员正在寻求更有效的哈希算法。
  3. 在#2的情况下,我似乎不存在这个问题。没有其他实例equals(),因此不需要其他实例来拥有相同的哈希码。在最坏的情况下,哈希算法可能会产生较差的哈希映射性能,这超出了本问题的范围。

    在#1的情况下,标准单元测试需要创建同一对象的两个实例,并将相同的数据传递给构造函数,并验证相同的哈希码。假阳性怎么样?有可能选择恰好在一个不合理的算法上产生相同哈希码的构造函数参数。倾向于避免这些参数的单元测试将满足这个问题的精神。这里的快捷方式是检查equals()的源代码,仔细思考,并根据它进行测试,但在某些情况下这可能是必要的,但也可能有常见的测试可以解决常见问题 - 等等测试也符合这个问题的精神。

    例如,如果要测试的类(称为Data)具有接受String的构造函数,并且从equals()的字符串构造的实例产生了equals()的实例,那么一个好的测试可能会测试:

    • new Data("foo")
    • 另一个new Data("foo")

    我们甚至可以检查new Data(new String("foo"))的哈希码,以强制不要实现字符串,尽管这更可能产生一个正确的哈希码,而不是Data.equals(),以产生正确的结果,我的意见。

    Eli Courtwright的回答是一个基于equals规范知识来打破哈希算法的一个例子。特殊集合的示例很好,因为用户自己的Collection有时会出现,并且很容易在哈希算法中出现muckup。

答案 5 :(得分:1)

这是我在测试中有多个断言的唯一情况之一。由于您需要测试equals方法,因此您还应该同时检查hashCode方法。因此,在每个equals方法测试用例中也检查hashCode契约。

A one = new A(...);
A two = new A(...);
assertEquals("These should be equal", one, two);
int oneCode = one.hashCode();
assertEquals("HashCodes should be equal", oneCode, two.hashCode());
assertEquals("HashCode should not change", oneCode, one.hashCode());

当然,检查一个好的hashCode是另一个练习。老实说,我不打算做双重检查以确保hashCode在同一次运行中没有改变,通过在代码审查中捕获它并帮助开发人员理解为什么这不是一个好方法可以更好地处理这种问题编写hashCode方法。

答案 6 :(得分:1)

答案 7 :(得分:0)

如果我有一个类Thing,和其他大多数人一样,我会编写一个类ThingTest,它包含该类的所有单元测试。每个ThingTest都有一个方法

 public static void checkInvariants(final Thing thing) {
    ...
 }

如果Thing类重写hashCode并且等于它有一个方法

 public static void checkInvariants(final Thing thing1, Thing thing2) {
    ObjectTest.checkInvariants(thing1, thing2);
    ... invariants that are specific to Thing
 }

该方法负责检查所有不变量,这些不变量旨在保存在任何Thing对象之间。它委派的ObjectTest方法负责检查任何对象之间必须存在的所有不变量。由于equalshashCode是所有对象的方法,因此该方法会检查hashCodeequals是否一致。

然后我有一些测试方法可以创建Thing个对象,并将它们传递给成对checkInvariants方法。我使用等价分区来决定哪些对值得测试。我通常只在一个属性中创建每个对,另外还有一个测试两个等效对象的测试。

我有时也有一个3参数checkInvariants方法,虽然我发现它在findinf缺陷中不太有用,所以我不经常这样做